Skip to content

ADR-0031: Control-Plane und Mandanten-Datenbanken

Kontext

ADR-0030 verlangt eine Postgres-Datenbank pro Linetra-Mandant. Der MVP nutzte eine gemeinsame DB linetra und die Cluster-Rollen linetra_edit / linetra_read. Postgres-Rollen sind clusterweit: dasselbe Cert-CN auf allen Mandanten-DBs würde bedeuten, dass ein Client den Datenbanknamen wechseln und fremde Fachdaten öffnen kann.

organizations sind Stammdaten (Firma, Gemeinde) in einer Mandanten-DB, nicht die Isolationseinheit. Die Web-App braucht eine globale Login- und Katalog-DB.

Entscheidung

  1. Control-Plane bleibt die bestehende Datenbank linetra: tenants, app_users (mit tenant_id), Sessions/Cert-Metadaten, Editor-Bearer-Tokens. FastAPI-Login spricht nur diese DB.
  2. Mandant ist eine Control-Zeile tenants (nicht organizations). Fachdaten liegen in linetra_<slug>.
  3. DB-Rollen pro Mandant: Cluster-Logins {slug}_edit und {slug}_read. CONNECT nur auf linetra_<slug>. Cert-CN = Rollenname. Control-Rollen linetra_edit / linetra_read behalten CONNECT nur auf linetra.
  4. PgBouncer: [databases] Wildcard * = host=postgres port=5432 plus explizites linetra. Auth über auth_user + auth_query (Funktion pgbouncer.get_auth liefert HMAC-Klartext, nicht pg_authid.rolpassword), nicht über eine wachsende userlist.txt. Nach außen bleibt auth_type=cert.
  5. Alembic: Control-Revisionen weiter unter alembic/versions/ gegen linetra. Mandanten-Revisionen unter alembic/tenant/ gegen jede linetra_<slug>. Neue Mandanten: CREATE DATABASE von Template oder Kopie, dann Tenant-Stamp; Fach-Upgrades per scripts/alembic_upgrade_all.sh.
  6. FastAPI: Control-Engines für Auth; Tenant-Engine-Cache keyed by tenant_id (Pool erst beim ersten Request). Session-Cookie trägt tenant_id. Celery-Tasks bekommen tenant_id als primitiven Arg.
  7. QGIS: nach API-Login liefert /auth/me (und Token-user) Host, Port, dbname, CN. Das Plugin schreibt das Profil; keine Fallbacks linetra / linetra_edit.
  8. Person-Bindung: app_users.person_id ist eine schwache UUID (kein FK nach persons). Personen leben in der Mandanten-DB.

Lokal darf LINETRA_SHARED_DB=true denselben Cluster-DSN für Control und einen logischen Dev-Mandanten nutzen (pytest, frische Compose ohne Superuser-Provisioning). Produktion provisioniert echte DBs.

Begründung (für dieses Setup)

Database-per-tenant war in ADR-0030 gewählt. Ein Rollenpaar pro Mandant schließt dbname-Guessing. Control-Plane in der bestehenden linetra-DB vermeidet eine dritte Datenbank nur für Login. PgBouncer und mTLS bleiben (ADR-0002); nur das Mapping ändert sich. HMAC-Passwörter aus LINETRA_TENANT_SECRET speichern keine Rollenpasswörter in tenants.

Verworfene Alternativen

Alternative Warum verworfen
organizations als Mandant Stammdaten-Kind; zweite Mandantenart würde die Tabelle umbenennen müssen
Globales linetra_edit auf allen DBs Jedes Edit-Cert kann jede Mandanten-DB öffnen
Postgres-Rolle pro Person GRANT-Pflege; Widerruf bleibt das Zertifikat (ADR-0002)
Schema-per-tenant / RLS ADR-0030
Eine PgBouncer-ini-Zeile pro Mandant + Reload Ops-lastig; Wildcard ist der PgBouncer-Weg für N DBs
Statische userlist.txt für N Rollen Muss bei jedem Provisioning neu geschrieben werden
Ein Alembic-Head für Control und Mandant Würde app_users in Mandanten-DBs anlegen
Subdomain pro Mandant Kein DNS-Modell; Session reicht
Rollenpasswörter in tenants speichern Secret-Drift; HMAC aus LINETRA_TENANT_SECRET reicht intern

Folgen

  • ADR-0002: CN ist nicht mehr global linetra_edit / linetra_read, sondern {slug}_edit / {slug}_read.
  • ADR-0020: Satz „keine extra tenants-Tabelle in v1“ gilt nur für die Shared-DB; Isolationseinheit ist tenants.
  • ADR-0030 Punkt 5 (MVP eine DB) ist durch diese ADR abgelöst; Punkt 4 (Ziel) bleibt.
  • Lastenheft offener Punkt 7: kein DB-User pro Person; ein edit/read-Paar pro Mandant.