# Review — Omurga Chunk 2 (Tenancy, RLS, Tenant Context) Implementasyonu — 2026-07-26 · r2

> **Review type:** impl-review (re-review)
> **Previous review:** `docs/reviews/review-omurga-chunk2-impl-2026-07-26.md` — REJECTED, 5 bulgu (F-1…F-5) + 3 açık soru
> **Upstream document(s) read:** `docs/plans/omurga-plan_chunk2.md` (§2.8.1 düzeltme notu dahil, yeniden okundu) · r1'deki tam upstream seti geçerli
> **Other cross-referenced documents:** `docs/issues.md` (yeni chunk2 kalemleri) · `vendor/laravel/framework/src/Illuminate/Database/DatabaseManager.php` (R2-1'in doğrulaması için birincil kaynak)
> **Stage locks in effect:** r1 ile aynı; ek olarak §2.8.1 düzeltme notu (Sets onayı beyanıyla, 26.07.2026) plana işlendi.

**Kanıt tabanı:** tek düzeltme commit'i `ada42f1` (16 dosya, +465/−26; `93ebe81..HEAD` diff'i satır satır okundu). TeamCity **#700: SUCCESS — 120 passed, 0 failed, 0 ignored** (73 `omurga` + 47 `omurga-rls`; artış tutarlı: +2 omurga, +11 rls = GuardCoverage 8 + ConnectionSafety 3). Pint `--test`: passed; PHPStan level 6: 0 hata (ikisi de bu review sırasında lokal koşuldu).

---

## Önceki bulguların kapanış matrisi

| r1 bulgusu | Durum | Kanıt |
|---|---|---|
| **F-1** — şablon uygulamayı superuser'a bağlıyordu | ✓ (kalıntı: R2-2; ek mekanizmada R2-1) | `.env.example:52-56` `DB_USERNAME=3eegitim_app` + gerekçe yorumu; `DB_APP_PASSWORD` kaldırılıp `config/tenancy.php:56-59` `role_passwords.application` → `env('DB_PASSWORD')` (tek değer, kayma imkânsız). Çalışma zamanı kontrolü: `TenancyServiceProvider.php:103-136` guardlı bağlantılarda `rolsuper`/`rolbypassrls` doğrulaması; ihlal testi `ConnectionSafetyTest.php:24-40` (pgsql'i admin kimliğiyle açıp exception'ı kanıtlıyor) |
| **F-2** — migrate yolu `pgsql_admin`'e bağlı değildi | ✓ | `composer.json` `setup` → `migrate --database=pgsql_admin --force`, yeni `migrate` script'i + açıklaması; `MigrationsStarted` dinleyicisi `TenancyServiceProvider.php:82-87, 148-159` — etkin bağlantı `pgsql_admin` değilse migration başlamıyor (event `--database` bayrağını taşımadığı için `Migrator::usingConnection`'ın default'u geçici değiştirmesi üzerinden okunuyor — doğru okuma; `DatabaseManager::setDefaultConnection` `database.default`'u yazar). Deny/allow testleri: `ConnectionSafetyTest.php:42-58` |
| **F-3** — var olmayan arch denetimine atıf | ✓ | Denetim yazıldı: `tests/Arch/TenancyTest.php:76-108` — `Illuminate\Foundation\Testing\{RefreshDatabase, DatabaseTruncation, DatabaseMigrations}` importu `tests/Concerns/` dışında yasak; docblock düzeltildi (`tests/Concerns/RefreshDatabase.php:22-23`). `TestDisciplineTest`'teki 14 silinen satır test değil, `testFiles()` helper'ının `tests/Pest.php:145-150`'ye taşınması — silinen test yok |
| **F-4** — panel `EnsureTenantContext` taşımıyordu | ✓ | `BackofficePanelProvider.php:66` middleware yığınına eklendi (yorum güncel); kilit testi `HostRoutingTest.php:63-74` (`filament.backoffice.pages.dashboard` rotasının gathered middleware'inde) |
| **F-5** — §2.8.1 sapması kayıtsızdı | ✓ | Plan §2.8.1'e düzeltme notu (Sets onayı beyanı, 26.07.2026): eski geçici çözüm üstü çizili, ara dönem davranışı ("ofis kimliği boş kapsam alır, ilk sorguda düşer"), gerekçe ve chunk5/6 implementer'ına açık uyarı ("`SimpleScopeResolver` diye bir sınıf yoktur"); `docs/issues.md`'ye kalem olarak girdi |
| **Açık soru 1** — bekçi regex kaçakları | ✓ | `TenantContextGuard::tablesIn` `:97-131`: blok+satır yorumu temizleme, virgüllü join açılımı, alias ayıklama, şema nitelikli adın fail-closed tutulması; 8 testle kilitli (`GuardCoverageTest.php` — kaçak şekiller + muaf tablonun ve katalog sorgularının serbest kaldığı). Kalan bilinen sınırlar `docs/issues.md`'de |
| **Açık soru 2** (`set_config`) ve **3** (`audit.tables`↔`exempt_tables`) | ✓ | `docs/issues.md` "chunk2 implementasyonunda açılan kalemler": chunk9 ve chunk3'e adreslenmiş |

**Regresyon taraması:** r1'de ✓ olan matrix satırlarına dokunan değişiklik yalnız üç dosyada (provider — eklemeli; guard — `tablesIn` sıkılaştırması; `Pest.php` — helper taşıma). Tam suite (73+47) yeşil; guard sıkılaştırmasının framework'ün ürettiği SQL'de yanlış pozitif üretmediğinin kanıtı tam koşumun kendisi + `GuardCoverageTest:70-80`. r1 matrisinin diğer satırları geçerliliğini koruyor.

## ✅ What works

- **F-1'in kapanışı bulgunun ötesine doğru katmanda gitti:** şablon düzeltmesi yeni kurulumları, çalışma zamanı kontrolü mevcut `.env`'i elle bozan kişiyi hedefliyor; kontrol ihlal testiyle (gerçekten superuser kimliğiyle bağlanıp) kanıtlanmış. Şifre teklemesi (`DB_APP_PASSWORD` → `DB_PASSWORD`) doğru sadeleştirme.
- **`MigrationsStarted` denetimi doğru mekanizmaya yaslanıyor** — `--database` bayrağının event'te görünmediğini deneyerek bulup default-connection semantiğinden okuma kararı doğru (framework kaynağıyla uyumlu) ve hem deny hem allow yönü testli.
- **F-3'te atıf silmek yerine denetimi yazmak** doğru refleks; üç framework trait'ini birden kapsaması (yalnız `RefreshDatabase` değil) bulgunun ruhunu genişleterek kapatıyor.
- **F-4 probe'unun "düzeltme geçiyor ama test yok" durumunu yakalaması** ve testin bunun üzerine eklenmesi — ihlal-probe disiplininin tam amacı bu.
- **Bekçi sıkılaştırması ölçülü:** kaçak şekiller kapanırken muaf tablo ve katalog yolunun serbest kaldığı da testli ("bekçi insanların etrafından dolaşacağı bir şeye dönüşmemeli" — doğru kaygı).
- Plan düzeltme notu chunk5/6 implementer'ının bakış açısıyla yazılmış — "ne bekleme" cümlesi gelecekteki yanlış beklentiyi peşinen kesiyor.

## ⚠️ Findings & Risks (in-scope for this review type)

**R2-1 — Superuser çalışma zamanı kontrolü, en çok gerektiği süreç türünde ilk hatadan sonra sessizce devreden çıkıyor.** `TenancyServiceProvider.php:103-136` kontrolü yalnız `ConnectionEstablished` anında koşuyor ve iki mekanizma birleşince başarısız doğrulama kalıcı olarak susuyor:

1. Laravel bağlantıyı event dispatch edilmeden **önce** manager'a yazar (`vendor/.../DatabaseManager.php:101-107`): dinleyici exception fırlatsa da kullanılabilir superuser bağlantısı `$connections`'ta kalır; aynı süreçteki bir sonraki `DB::connection('pgsql')` event'i yeniden tetiklemeden onu döndürür ve sorgu superuser ile **çalışır**.
2. `$verified[$key] = true` sorgudan ve fırlatmadan **önce** set ediliyor (`:115`); yani bağlantı purge edilip aynı kimlikle yeniden açılsa (`ConnectionEstablished` yeniden gelse) bile — ki Laravel'in lost-connection reconnect yolu tam bunu yapar — kontrol atlanıyor. Kodun kendi yorumu ("a connection reopened ... answering it from a cache would be exactly the kind of 'it was fine last time' that this check exists to refuse") başarısızlık yolunda tersine çalışıyor: cache "geçen sefer **kötüydü**" cevabını da yutuyor.

FPM web'de statikler istek başına sıfırlandığı için her istek fırlatır — orada kontrol amaçlandığı gibi çalışır. Ama Horizon/queue worker ve Octane gibi uzun ömürlü süreçlerde senaryo şu: yanlış `DB_USERNAME` ile worker kalkar → ilk sorgu fırlatır, job 1 düşer, worker yaşamaya devam eder → job 2'den itibaren tüm sorgular superuser ile, RLS'siz, **sessizce** akar. Bu, kontrolün var olma sebebi olan sessiz modun ta kendisidir ve "uygulamayı durduruyor" iddiası bu süreç türü için doğru değildir. (Doğrulama: framework kaynağı + kod okuması; `ConnectionSafetyTest` yalnız ilk fırlatmayı kanıtlıyor, tekrar davranışını kanıtlayan test yok.)

**R2-2 — `DB_APP_PASSWORD` kaldırıldı ama referansları kaldırılmadı.** Değişken artık hiçbir şeyi beslemiyor (`config/tenancy.php:56-59` → `DB_PASSWORD`), fakat: `deployment/pgbouncer/userlist.txt.example:27` ("Passwords match ... DB_APP_PASSWORD and DB_FINANCIAL_PASSWORD") ve `:31` (`"SET_TO_DB_APP_PASSWORD"` placeholder'ı), `deployment/pgbouncer/README.md:16` ("sifreleri .env'deki DB_APP_PASSWORD / DB_FINANCIAL_PASSWORD ile ayni yapin"), `.env.testing.example:41` (`DB_APP_PASSWORD=` — set edilse sessizce yok sayılan ölü anahtar). Kurulumu README'den yürüten kişi var olmayan bir değişkeni arayacak; ilk elden riski, PgBouncer userlist şifresinin rolün gerçek şifresiyle (artık `DB_PASSWORD`) eşleşmemesi ve "bağlansın diye" yapılan düzeltmelerin — bu chunk'ın en çok uyardığı yolun — yeniden davet edilmesi. F-1 remediasyonunun kendi süpürme eksiği; küçük ama tam da bu dosyaların uyarı görevi gördüğü yerde.

## ❓ Open Questions (to be answered before next stage)

1. **`DB_CONNECTION=pgsql_admin` vektörü.** Superuser kontrolü bağlantı **adıyla** anahtarlı ve `pgsql_admin` bilinçli olarak kapsam dışı (migration meşru olarak superuser). Bunun simetrik sonucu: `.env`'de `DB_CONNECTION=pgsql_admin` yazan biri uygulamanın varsayılan bağlantısını hem bekçisiz hem superuser-kontrolsüz admin bağlantısına taşır — migration denetimi şikâyet etmez (admin'dedir), çalışma zamanı kontrolü hiç bakmaz, RLS superuser'ı bağlamaz. R2-1 kapatılırken bu vektörün nerede reddedileceği (ör. HTTP/queue bağlamında default bağlantının admin olamayacağına dair bir kontrol, ya da chunk9 §9.5 taramasına kural) karara bağlanmalı.
2. `DB_USERNAME` ↔ `DB_APP_ROLE` artık aynı değeri taşımak zorunda (bağlantı vs. rol oluşturma/hata mesajı) — `.env.example` yorumu bunu söylüyor; ayrışırlarsa migration rolü `DB_APP_ROLE` adıyla `DB_PASSWORD` şifresiyle açar, uygulama başka adla bağlanır. Bugün varsayılanlar tutarlı; kalıcı bir kontrol gerekip gerekmediği R2-1'in kapanışıyla birlikte değerlendirilebilir (not — bulgu değil).

## Verdict: ❌ REJECTED

Beş bulgunun beşi de özüyle kapatılmış, ikisi bulgunun ötesine geçmiş; doğrulama disiplini (ihlal probe'ları, CI #700 120/0/0, framework semantiğinin deneyle okunması) yüksek. Ancak F-1'in "ötesi" olarak eklenen çalışma zamanı kontrolü, kendi tasarım beyanıyla çelişen bir fail-open moda sahip (R2-1): koruduğunu iddia ettiği sessiz senaryo, Horizon/Octane süreçlerinde ilk hatadan sonra aynen geri geliyor. R2-2 küçük ama ops-yolu üzerinde. İki bulgu kapatılıp re-review talep edilmelidir; kapsam dar olduğu için r3'ün hızlı olması beklenir.
