Skip to main content

ADR: Kullanıcı Hesabı, Kişisel Workspace ve Kurumsal Tenant İzolasyon Modeli

Karar (Human-CEO, 3 Tem 2026, birebir): "İnsan CEO - Alternatif B. ACCEPTED." — AI-CEO 2-tur red-team (v1 PASS-WITH-CHANGES → v2 decision-ready) sonrası. R3.

Karar-Kapsamı (bu kabul NEYİ yetkilendirir?)

Kabul = HEDEF-MİMARİ kabulüdür. ŞUNLARI YETKİLENDİRMEZ: veri-migration'ı · users.tenant_id kaldırma/nullable-flip · RLS/SetTenantDbContext aktivasyonu · production-deploy · legacy-tenant retire. Her biri AYRI gate (Aşama-2, CEO-onaylı implementation-brief'leriyle).

0. Temel hüküm

Kullanıcı tenant'sız doğar; fakat bağlamsız ve sahipsiz doğmaz. Kişisel workspace kullanıcının çalışma bağlamıdır. Organization işletme kimliğidir. Tenant yalnız teknik izolasyon sınırıdır.

1. Karar sorusu

Her yeni kullanıcıya kayıt-anında otomatik kişisel-tenant açan mevcut model mi korunmalı, yoksa kullanıcı bağımsız bir hesap-principal'ı olarak doğup organizasyona sonradan mı katılmalı — ve kişisel-veri izolasyonu tenant OLMADAN nasıl güvenceye alınmalı?

2. Bağlam — yer-gerçeği (salt-okuma derin-dalış, 3 Tem; dosya:satır kanıtları raporda)

  • users = kişi+hesap birleşik tek-tablo; ayrı Person YOK; hesap-durum makinesi YOK.
  • user_identities+user_emails şema-VAR / runtime-YAZILMIYOR; auth bugün users.email+password; çoklu-kimlik şema-hazır/kod-yok.
  • users.tenant_id NOT NULL + signup koşulsuz taze-Tenant::create → 1 kullanıcı = 1 kişisel-tenant.
  • Sürpriz-1: SetTenantDbContext DORMANT (hiçbir route'a bağlı değil) → tenant_id→RLS-GUC zinciri web'de fiilen çalışmıyor.
  • Sürpriz-2: paylaşımlı-tenant şemaca-mümkün (unique-yok; AdminUserSeeder zaten yapıyor); tek-kilit NOT-NULL + signup-mint.
  • ~22 $user->tenant_id okuma-noktası (3'ü null-intolerant); user_role_assignments.tenant_id CASCADE; RLS 10-tablo + 6-izolasyon-testi.

3. KARAR METNİ (Alternatif B — bağlayıcı)

EventInn'de kullanıcı hesabı tenant'a ait bir alt-kayıt olarak oluşturulmaz.
Yeni kullanıcı: bağımsız user/account principal olarak oluşturulur · otomatik
kişisel-tenant ALMAZ · kendisine ait TEK bir personal workspace alır.
Tenant: kullanıcı-hesabı değil, workspace değil; kurumsal-veri için teknik
izolasyon konteyneri; en az bir owning-organization ilişkisi taşır.
Kullanıcının tenant + kurumsal-workspace erişimi: organization-membership +
role/capability + resource-scope + policy üzerinden TÜRETİLİR.
`users.tenant_id` yetkilendirme-otoritesi olmaktan çıkarılır; kontrollü geçiş
sonunda KALDIRILIR.
Kişisel kaynaklar personal-workspace-ownership; kurumsal kaynaklar
workspace+tenant+membership politikasıyla FAIL-CLOSED izole edilir.
Legacy ADR-099 bu kararın otoritesi DEĞİL; adopted-in-part/reconciled referans.

Personal workspace oluşturulması, kullanıcıya bir tenant veya organizasyon
üyeliği verilmesi anlamına GELMEZ. (Örtülü-kişisel-tenant yorumu engellenir.)

4. İki-aşamalı karar (bağlayıcı sıralama)

  • Aşama-1 (BU KABUL, mimari-karar): kullanıcı≠tenant · signup tenant-üretmez · tenant=kurumsal-izolasyon-konteyneri · kişisel-bağlam=ayrı-workspace-modeli. Dormant-middleware ertelemek için değil, güvenli-değiştirmek için fırsattır.
  • Aşama-2 (koordineli-uygulama, AYRI gate'ler): kişisel-workspace + authorization + RLS hazır olmadan production'a UYGULANMAZ. Migration, Dilim-1B'nin organization/membership sözleşmesiyle KOORDİNELİ. RLS ancak yeni-modelin negatif-izolasyon-testleri geçince aktive edilir. Dormant SetTenantDbContext, bu ADR'nin Aşama-2'si + RLS-testleri bitmeden AKTİVE EDİLMEZ.

5. Hedef kavramsal model (bağlayıcı iskelet)

  • User / Account Principal: EventInn hesabı; tenant'a-ait-değil; hesap-yaşam-döngüsü; N-authentication-identity taşıyabilir.
  • Authentication Identity: password / Google-OIDC / (gelecek). 1 account → N identity.
  • User Email: doğrulanmış-iletişim; primary/secondary; identity-değil.
  • Organization: şirket/ajans/mekan-işletmesi — işletme-kimliği.
  • Membership: kullanıcı↔organizasyon ilişkisi.
  • Tenant: teknik-veri-izolasyon konteyneri (hesap-değil, workspace-değil).
  • Workspace: aktif-çalışma-bağlamı (personal / organization / ileride project/venue).
  • Assignment/Capability/Scope: hangi-workspace/kaynakta-ne-yapabilir.

5.1 Kişisel workspace — invariant'lar

Her aktif kullanıcı → tam olarak BİR personal workspace
personal workspace → tenant_id = null · owner_user_id ZORUNLU
kurumsal workspace → organization_id ZORUNLU · tenant_id ZORUNLU
tenant_id = null → ASLA "global/public/RLS-dışı" DEĞİL
workspace XOR → aynı-anda hem-personal hem-org OLAMAZ (personal:
owner_user_id-var/organization_id-null · org:
organization_id-var/owner-null) — DB-XOR-constraint
kişisel-erişim → workspace.owner_user_id = current_user_id
kurumsal-erişim → aktif-membership + role/capability + resource-scope
+ workspace/tenant-eşleşme

Personal workspace oluşturulması tenant/org-üyeliği vermez (örtülü-kişisel-tenant yorumu engellenir).

5.2 Organization ↔ Tenant kardinalite

Organization = işletme-kimliği · Tenant = teknik-izolasyon · Workspace = işlem-bağlamı. Pilot-1 invariant'ı: her tenant'ın tam bir owning-organization'ı olur · kullanıcı tenant'a DOĞRUDAN üye-olmaz, organizasyona membership'le bağlanır · tenant-erişimi membership+scope'tan türetilir. Tam N:N Pilot-1'de zorunlu-değil (tablo hazırlanabilir; Pilot-1-kuralları tek-owning-org üzerinden).

5.3 Kaynak sahiplik-modeli

resource → workspace_id · workspace → personal-owner VEYA organization-owner · organization-workspace → tenant. Invariant: her scope'lu domain-kaydı bir workspace'e aittir; CI/DB-constraint: resource.tenant_id = resource.workspace.tenant_id.

5.4 users.tenant_id kaderi

Hiçbir authorization-kararında otorite OLAMAZ. Nihai: KALDIRILIR; aktif-bağlam = last_active_workspace_id (veya oturum-bağlamı); "default_tenant_id" KULLANILMAZ (varsayılan-erişim iması riskli). Geçici uyumluluk: nullable + deprecated + authorization-sorgularında CI-yasağı + yalnız-migration-uyumu.

5.5 Person/Account nüansı

Pilot-1'de users account-principal + temel-profil taşır; ayrı Person-tablosu AÇILMAZ; ama authentication-identity + org-membership + doğrulanmış-kişi-profili account'tan KAVRAMSAL-ayrı tutulur (gelecekte verified-person/contact/employee/legal-rep ihtiyacını engellemez). "Person≡Account her zaman" YAZILMAZ.

6. Silme-politikası (CASCADE YASAK — yapısal-guard)

Tenant/membership/rol normal-akışta hard-delete EDİLMEZ: tenant soft-delete/retired/closed · kritik-FK ON DELETE RESTRICT · rol/membership-kapanışı explicit-service-transaction · audit+geçmiş-assignment KORUNUR · CASCADE yalnız türetilmiş/yeniden-üretilebilir geçici-veride. CASCADE ile ASLA kaybedilmez: role-assignments · membership-history · approval-history · audit-events · resource-ownership · devir-kayıtları · güvenlik-olayları.

7. RLS fail-closed ilkeleri (bağlayıcı, 10)

  1. tenant-context-yokluğu kurumsal-veri-erişimi VERMEZ. 2. tenant_id IS NULL global/public DEĞİL. 3. personal-workspace yalnız owner-user okur. 4. kurumsal-workspace aktif-membership+capability ister. 5. askıya-alınmış/kapanmış-membership erişim-vermez. 6. workspace-değişimi backend'de her-istekte kapsam-doğrular. 7. URL/ID/payload değişikliğiyle başka-workspace'e geçilemez. 8. RLS-context kurulamazsa istek FAIL-CLOSED kapanır. 9. service/admin-bypass ayrı-capability+audit ister. 10. cross-tenant negatif-testler her-deployment koşar.

7.1 RLS uygulama-sözleşmeleri 〔AMD-2〕

P5 Pre-Audit bulguları üstüne iki bağlayıcı uygulama-sözleşmesi (CEO-blok-onay, 5 Tem 2026):

Delta-A — Bağlam-çözümleme düzlemi RLS-HARİÇ (bilinçli-istisna). workspaces / memberships / organizations / membership_invites tablolarına tenant-GUC-RLS uygulanmaz. Gerekçe (bootstrap-problemi): TenantContextResolver tenant-context KURULMADAN bu tabloları okur — RLS'lenirse çözücü kendini kilitler; ayrıca personal-workspace tenant_id=NULLtenant_isolation-policy altında hiç kimseye görünmez olurdu; memberships zaten tenant_id taşımaz (workspace-türevli). Bu düzlemin izolasyonu app-layer'dadır (§5.1-invariant'lar + pozitif-ownership + membership-sorguları). Pozitif-kural: runtime-yüzeyi doğan her kurumsal işlem-tablosu doğarken RLS'li doğar (tenant_specialty-deseni).

Delta-B — Null=DENY kurumsal-lane sözleşmesi. tenant.db.context route-grubu = kurumsal-işlem-lane'dir; tenant-context'in tek-meşru kaynağı = TenantContextResolver::fromWorkspace (workspace-türevli türetme). Resolver null → 403 FAIL-CLOSED (önceki null-skip davranışı kalkar — "skip" public-lane'in işidir; public-lane bu middleware'i hiç almaz). Personal-bağlam = GUC-unset (sentinel-YOK; current_setting(...,true) null → NULLIF-policy → 0-satır = ilke-2'nin DB-ifadesi). Personal-workspace ince-izolasyonu app-layer'da kalır; FV1 kişisel-kaynak tabloları doğarken owner-RLS ayrı-karar olarak yeniden değerlendirilir.

8. Mevcut-veri migration planı (Aşama-2 çerçevesi)

Sınıflandırma: personal_empty · personal_with_data · organization_owned · shared · unknown_or_conflicting. Zorunlu adımlar: (1) her-kullanıcıya personal-workspace (2) kişisel-tenant-içi kaynak-envanteri (3) kişisel-kaynak→personal-workspace bağlama (4) org-verili-tenant→owning-org eşleme (5) çok-kullanıcılı-tenant incelemesi (6) eski-tenant→yeni-workspace/org manifesti (7) ilk-aşamada HİÇBİR tenant/veri hard-delete edilmez (8) migration-sonrası sayım+hash karşılaştırma (9) Verify-sonrası boş-legacy-personal-tenant retire (10) rollback ters-eşleme-manifesti.

9. Değerlendirilen alternatifler

  • A) KORU: sıfır-değişiklik; ama kişi-başı-tenant kalıcı-maliyet + "kişisel=tenant" kavram-karışıklığı + referans-desenlere aykırı.
  • B) DEĞİŞTİR (KABUL EDİLEN): yukarıdaki hedef-model.
  • C) HİBRİT (nullable+opsiyonel-kişisel-tenant): iki-model-taşıma karmaşıklığı → red.

10. Karar-dayanak sırası

(1) EventInn işletme-senaryosu+use-case (2) güvenlik+izolasyon-invariant'ları (3) mevcut-kod-gerçekliği (4) migration-maliyeti (5) legacy-EventInn-kararları (6) dış-ürün/CMS. Cvent "Account" ≠ EventInn user-account → kurumsal-kod EventInn'de organization_code.

11. Kapsam-DIŞI (ayrı R3-ADR'ler)

Google-OIDC-linking + mükerrer-önleme (UCG-003) · session/token yaşam-döngüsü (UCG-002) · audit/güvenlik-olayları. Bu ADR yalnız veri-modeli-iskeleti + izolasyon-politikası.

12. legacy-ADR-099 ilişkisi

supersedes DEĞİL (legacy, yeni-Canon-lifecycle'ından geçmedi) → reconciles_legacy: {ADR-099: adopted_in_part}; legacy-registry'ye karşı-not düşüldü.

13. Kabul-kriterleri (20 — Aşama-2'nin ölçüm-seti)

  1. signup yeni-tenant-üretmez. 2. signup personal-workspace-üretir/idempotent-bulur. 3. retry ikinci-user/workspace-üretmez. 4. kullanıcı tenant'sız-oturum-açabilir. 5. kurumsal-tenant-erişimi yalnız-aktif-membership'ten. 6. users.tenant_id authz-kaynağı-değil. 7. personal-workspace başka-kullanıcı-okuyamaz. 8. org-workspace başka-tenant-kullanıcısı-okuyamaz. 9. null-tenant-context kurumsal-erişim-vermez. 10. tüm-mevcut-kayıt migration-manifestinde-sınıflı. 11. migration-sonrası sayım/sahiplik-kaybı-yok. 12. mevcut-personal-tenant'lar hard-delete-edilmedi. 13. tenant-hard-delete normal-yolda-bloke. 14. role-assignment/audit'te CASCADE-yok. 15. personal→org-transfer explicit+audit'li. 16. migration/rollback prod-benzeri-snapshot'ta-test. 17. 6-izolasyon-testi yeniden-yazıldı+negatif-genişletildi. 18. exact-SHA bağımsız-Verify-doğruladı. 19. OIDC/session-ADR'leri bu-modele-çelişmeden-bağlanır. 20. panel user/workspace/organization/tenant'ı AYRI-gösterir.

İzlenebilirlik

upstream: CAN-REQ-SCN-001 (senaryo-kapsamı) + CAN-REQ-UCG-004 (bu ADR'nin cevapladığı açık-soru sahibi use-case-kümesi; lint-kuralı gereği accepted-use_case-upstream). governed_by: CAN-GOV-UCDEP-001. resolves (extensions): CAN-REQ-UCG-004 kişisel-bağlam + bireysel-tenant AÇIK-SORUSU bu kararla kapanır. Kaynak: v2-taslak (AI-CEO 2-tur red-team işlenmiş) + v1-taslak + CEO-chat — otorite bu Canon-kaydındadır.


CAN-DEC-ADR-002 · decisions/normative · governance_status: accepted (Human-CEO R3-kabulü, 3 Tem 2026 — "Alternatif B. ACCEPTED"; yürürlük/uygulama AYRI kapılar) · reconciles legacy-ADR-099 (adopted_in_part).