Skip to content

ADR-0051: Sysadmin legt Organisationen an; Control-DB nur Metadaten

  • Status: accepted
  • Datum: 2026-09-02
  • Phase: Multi-Tenant
  • Bezug: ADR-0031, ADR-0032
  • Ändert: ADR-0032 (Selbstanlage, impliziter Plattform-Operator)

Kontext

ADR-0032 band den Konto-Lebenszyklus an Selbstanlage: ein User ohne tenant_id legt per POST /tenants eine Organisation an und wird Org-Admin. Der Plattform-Operator war nur role=admin plus tenant_id null. Lokal setzte LINETRA_SHARED_DB=true tenants.dbname auf die Control-DB linetra, sodass GIS, Projekte und föderierte Entities neben app_users landeten.

Gewollt ist: ein Bootstrap-Sysadmin (Env) legt Organisationen an und bestimmt Org-Admins; Org-Admins verwalten nur die eigene Orga; die Control-DB hält nur Web-Metadaten.

Entscheidung

  1. app_users.is_system_admin ist das Plattform-Flag. Org-Rolle bleibt role (admin / editor / reader plus Fachrollen).
  2. Bootstrap: leere app_users → ein Sysadmin aus LINETRA_BOOTSTRAP_ADMIN_* (ohne tenant_id). Keine automatische Organisation und kein Org-Admin aus der Env.
  3. POST /tenants nur Sysadmin. Body enthält den ersten Org-Admin (Username, Passwort, Vorname, Nachname, E-Mail). Keine Selbstanlage.
  4. Weitere Sysadmins legt nur ein Sysadmin an (POST /users mit is_system_admin). Der letzte aktive Sysadmin bleibt unantastbar.
  5. Mitgliedschaft bleibt 1:1 (app_users.tenant_id). Sysadmins dürfen tenant_id null haben und per Session / X-Tenant-Id eine Orga wählen.
  6. Control-DB linetra: tenants, app_users, editor_access_tokens, client_cert_grants. Fachdaten (Projekte, GIS, Schema-CMS, föderierte Records, Akteure) nur in linetra_<slug>.
  7. Kein stilles Shared-Fallback außerhalb von Tests: tenant.dbname == POSTGRES_DB ist ein Fehler, wenn LINETRA_SHARED_DB aus ist. Pytest behält Shared-DB.

Begründung (für dieses Setup)

Zwei Ebenen (Plattform / Organisation) reichen: Isolationseinheit bleibt die Orga-DB (ADR-0030/0031). Ein explizites Sysadmin-Flag verhindert, dass jeder Org-Admin Plattformrechte erbt. Personfelder am Org-Admin gehören in persons der Tenant-DB, nicht auf app_users. Die bestehende Tenant-Session (get_db_session) schreibt Fachdaten bereits gegen tenants.dbname — die Lücke war Shared-DB, nicht die Route.

Verworfene Alternativen

Alternative Warum verworfen
Selbstanlage (ADR-0032 Punkt 2) Orgs entstehen durch Sysadmins, nicht durch den ersten Login
role=system_admin statt Flag Flag + Role-Code bleiben getrennt; Bootstrap legt nur Sysadmin an
M:N User ↔ Org Ein Mitarbeiter gehört einer Orga
Drei Ebenen Tenant → Orgs Organisation = Tenant = eine DB
Casbin / Oso Vier klare Stufen, Principal existiert schon
RLS / Schema-pro-Tenant Isolation war ADR-0030/0031
Namensfelder auf app_users Vorname/E-Mail sind Akteur (persons)
Control-Alembic-Geschichte umschreiben Fach-DDL bleibt historisch; neue Fach-DDL nur unter alembic/tenant/

Folgen

  • ADR-0032 Punkte 2 und 7 gelten nicht mehr. Föderierter Stamm und GIS-project_id bleiben.
  • Principal: can_manage_tenants / can_view_platform_settings nur Sysadmin; can_manage_users für Org-Admin oder Sysadmin; can_create_project für Org-Admin oder Sysadmin im Org-Kontext.
  • Settings → System (Sysadmin): weitere Sysadmins, Org-Katalog. Settings → Organisation: kein Selbst-Create.
  • QGIS /auth/me.qgis_connection.database ist linetra_<slug>, nicht linetra.
  • Optionaler Ops-Schritt nach dem Cutover: scripts/strip_control_fachdaten.sh (Keep-Liste inkl. client_cert_grants).
  • Teardown (Sysadmin): GET /tenants/{id}/backup liefert optional ein ZIP (pg_dump der Tenant-DB + Control-Metadaten + Projektdateien) — nie ein Dump der Control-DB. DELETE /tenants/{id} mit confirm_slug löscht Control-Zeilen und droppt nur linetra_<slug> (+ {slug}_edit/_read). LINETRA_SHARED_DB=true (nur Pytest) blockiert Löschen. Legacy-Zeilen mit dbname=POSTGRES_DB löschen nur den Katalog, nicht die Control-DB. Restore bleibt manuell aus dem ZIP.
  • Shared-DB: nur unter Pytest (conftest). Compose-Dev und Produktion provisionieren echte linetra_<slug>-DBs; LINETRA_SHARED_DB außerhalb von Pytest führt zum Fail-fast beim Start.