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:
parent
6074cfd284
commit
a35c7e4d54
115
adr/0019-organisationsverwaltung-stammdaten-und-kontenrollen.md
Normal file
115
adr/0019-organisationsverwaltung-stammdaten-und-kontenrollen.md
Normal 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.
|
||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user