docs: ADR-0019 Organisationsverwaltung + Datenmodell nachgezogen

Dokumentiert die Session-Arbeit vom 20.08.2026: Kontakt-/Stammdaten je
Organisation, neue organisationsgebundene Rolle ist_organisationsadmin
(strikt getrennt von is_superuser - Kernentscheidung dieses ADRs), den
live gefundenen und gefixten bytes-vs-str-psycopg-Bug bei der
Kontolöschung.

09-data-model-v1.md um die fünf neuen Organization-Kontaktfelder
ergänzt. benutzer/ist_organisationsadmin bewusst nicht dort aufgenommen
- lebt in kundenplattforms eigener Datenbank, war schon vorher nicht
Teil dieses Dokuments (ADR-0011).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Thomas Stallinger 2026-08-20 07:55:41 +02:00
parent 6074cfd284
commit a35c7e4d54
2 changed files with 127 additions and 0 deletions

View File

@ -0,0 +1,115 @@
# ADR-0019: Organisations-Stammdaten + organisationsgebundene Kontenrolle
**Status:** Beschlossen
**Datum:** 20.08.2026
## Kontext
`/organisation` im Kundenportal erlaubte bislang ausschließlich das Ändern
des Namens — die `organizations`-Tabelle auf anode hatte sonst keine
Felder, es gab nichts, das eine Kundin dort „mit Informationen befüllen"
konnte. Parallel dazu gab es für die Kontenverwaltung **keinen einzigen
Self-Service-Weg**: Konten anlegen ging ausschließlich über den internen
Bereich (`/admin/kunden/{id}/konten`). Eine Organisation, die eine weitere
Kollegin einloggen lassen oder ein Passwort zurücksetzen wollte, brauchte
dafür immer das Tuxflotte-Team.
**Entscheidendes Sicherheitsdetail, das die Lösung bestimmt hat:**
`benutzer.is_superuser` bedeutet im bestehenden Code nicht „Admin der
eigenen Organisation", sondern „organisationsübergreifendes, internes
Tuxflotte-Personal mit Zugriff auf `/admin/*`" (siehe das Formular-Label
in `admin_kunden.py`: „Admin-Zugriff (organisationsübergreifend)"). Eine
naive Self-Service-Kontenverwaltung, die dieses Flag wiederverwendet
hätte, hätte einer Kundin ermöglicht, sich selbst (oder eine Kollegin)
versehentlich zur internen Tuxflotte-Admin mit Zugriff auf *alle*
Organisationen zu machen — kein Stilproblem, sondern eine echte
Rechteausweitung über Organisationsgrenzen hinweg.
Umfang bewusst begrenzt (mit dem User abgestimmt): Stammdaten = einfache
Kontakt-/Adressdaten (Ansprechpartner, Adresse, Telefon, Kontakt-E-Mail,
Notizfeld) — **keine** Richtlinien-/Sicherheitsfelder. Die in
`03-organization-model.md` skizzierten schwereren Konzepte
(Sicherheitsanforderungen, Softwarequellen, Netzwerkvorgaben, Branding)
bleiben unangetastet; `Network Profile`/`Secret Reference` sind laut
ADR-0001/`09-data-model-v1.md` weiterhin bewusst unspezifiziert.
## Entscheidung
**Stammdaten:** `organizations` bekommt fünf neue, nullable Textspalten
(`kontakt_name`, `adresse`, `telefon`, `kontakt_email`, `notizen`). Der
bestehende `PATCH /api/v1/organizations/{id}`-Endpoint wird nur breiter
(keine neue Route) — sowohl die Self-Service-Seite (`/organisation`) als
auch der interne Admin-Bereich (neue „Organisationsdaten"-Sektion in
`kunden_detail.html`) rufen denselben Endpoint auf und sehen dieselben
Daten, statt zweier getrennter Wahrheiten.
**Neue, organisationsgebundene Rolle `ist_organisationsadmin`** in
`kundenplattform`s eigener `benutzer`-Tabelle, strikt getrennt von
`is_superuser`:
- `is_superuser` bleibt exklusiv internes Personal, unverändert.
- `ist_organisationsadmin` gilt nur innerhalb der eigenen Organisation und
kann über Self-Service **nie** auf `is_superuser` hinauswirken —
`POST /organisation/konten` liest `organization_id` und `is_superuser`
nie aus dem Formular, sondern setzt `organization_id` fest auf die
Session des anfragenden Kontos und `is_superuser` immer auf `False`.
Live mit einem gezielten Manipulationsversuch geprüft (zusätzliches,
von der Route nicht deklariertes `is_superuser=on`-Feld im POST-Body
mitgeschickt): blieb wirkungslos, `is_superuser` weiterhin `False`.
- Neue Dependency `require_org_admin` (`is_superuser OR
ist_organisationsadmin`) statt der bestehenden `require_admin`
(`is_superuser` allein) für alle organisationsgebundenen
Verwaltungsaktionen — internes Personal bleibt dadurch bewusst nicht
ausgesperrt, falls Support nötig ist.
- Bootstrap: internes Personal kann beim Anlegen des allerersten Kontos
einer neuen Organisation (`/admin/kunden/{id}/konten`) direkt die neue
Checkbox „Admin dieser Organisation" setzen, getrennt von der
bestehenden Checkbox für `is_superuser` — ohne diesen Schritt hätte eine
neue Organisation keinen Weg, selbst mit der Self-Service-
Kontenverwaltung anzufangen.
**Sichtbarkeit vs. Verwaltung:** Die Kolleg:innen-Liste ist für **jedes**
eingeloggte Konto der Organisation sichtbar (Transparenz — wer hat
Zugriff), Anlegen-/Löschen-Formulare werden aber nur für
`is_superuser`/`ist_organisationsadmin` angezeigt und serverseitig über
`require_org_admin` erzwungen (nicht nur über UI-Ausblendung). Live
geprüft: ein Konto ohne Rechte bekommt bei einem direkten POST-Versuch
`403`, unabhängig davon, was die UI anzeigt.
**Selbstlöschung ausgeschlossen:** `POST /organisation/konten/{id}/loeschen`
verweigert das Löschen des eigenen, gerade eingeloggten Kontos explizit
(eigene Fehlermeldung) — verhindert, sich versehentlich aus der eigenen
Organisation auszusperren, ohne dass eine komplexere „letzter Admin"-
Prüfung nötig wäre.
## Konsequenzen
`fetch_benutzer_fuer_organisation()` wurde von `admin_kunden.py` nach
`auth.py` verschoben und wird jetzt von beiden Routern importiert (Reuse-
Pattern wie `fetch_gruppen()` aus `routers/gruppen.py`, siehe ADR-0017) —
keine doppelte SQL-Abfrage für dieselbe Kontenliste mehr.
**Live gefundener und gefixter Bug, festgehalten als wiederkehrendes
Muster:** `psycopg` liefert TEXT-Spalten über bestimmte
Verbindungspfade teils als `bytes` statt `str` zurück — ein bereits
bekanntes, nicht deterministisches Verhalten (siehe der bestehende
Kommentar in `_row_to_user()`, das ursprünglich die E-Mail-Anzeige im
Header verunstaltete). Dieselbe Eigenart traf jetzt die neue
`fetch_benutzer_fuer_organisation()`: ein daraus weitergereichter
`bytes`-Wert brachte eine nachgelagerte async-SQL-Abfrage
(`user_manager.get_by_email()`) mit `operator does not exist: text =
bytea` zum Absturz (`500`), als ein Org-Admin versuchte, ein fremdes
Konto zu löschen. Zweifach behoben: `konto_loeschen()` lädt das Zielkonto
jetzt über `user_manager.get(id)` (rein async, umgeht das Problem
strukturell) statt über den E-Mail-Umweg, und
`fetch_benutzer_fuer_organisation()` selbst bekam denselben defensiven
`bytes`-Decode wie `_row_to_user()`, damit dieselbe Eigenart nicht an
einer dritten Stelle erneut zuschlägt. Für künftigen Code, der Werte aus
einer sync-`psycopg`-Verbindung in eine async-Abfrage weiterreicht, ist
das ein bekanntes Risiko, keine Überraschung mehr.
Details zum Datenmodell: `09-data-model-v1.md`, dort um die neuen
Organization-Kontaktfelder ergänzt. `benutzer` (inkl. der neuen
`ist_organisationsadmin`-Spalte) bleibt dort bewusst unerwähnt — die
Tabelle lebt in `kundenplattform`s eigener, von `provisioning-server`
getrennter Datenbank (siehe ADR-0011) und war schon vorher nicht
Bestandteil dieses Dokuments.

View File

@ -37,12 +37,19 @@ Eine Organization beschreibt eine organisatorische Einheit innerhalb der Tuxflot
Eine Organization besitzt eigene Geräte, Workspaces, Netzwerkprofile und Richtlinien. Eine Organization besitzt eigene Geräte, Workspaces, Netzwerkprofile und Richtlinien.
Zusätzlich zu den fachlichen Beziehungen trägt eine Organization einfache Kontakt-/Stammdaten (siehe ADR-0019) — bewusst nur Adress-/Ansprechpartner-Angaben, keine Richtlinien- oder Sicherheitsfelder; Letztere bleiben Teil des weiterhin unspezifizierten `Network Profile`/`Secret Reference`-Themas.
Attribute: Attribute:
- id - id
- name - name
- created_at - created_at
- updated_at - updated_at
- kontakt_name (optional)
- adresse (optional)
- telefon (optional)
- kontakt_email (optional)
- notizen (optional)
Beziehungen: Beziehungen:
@ -498,6 +505,11 @@ Spalten:
- name - name
- created_at - created_at
- updated_at - updated_at
- kontakt_name (nullable)
- adresse (nullable)
- telefon (nullable)
- kontakt_email (nullable)
- notizen (nullable)
### organisationseinheiten ### organisationseinheiten