703 lines
42 KiB
Markdown
703 lines
42 KiB
Markdown
# Fortsetzungsdokument: Umsetzungsauftrag_Sonnet5.md — Teil D (Berechtigungen ausschliesslich ueber Benutzergruppen)
|
|
|
|
Stand: 2026-09-01, 05:20 UTC. **Teil D (D.6 Schritte 1-7) ist
|
|
vollstaendig abgeschlossen.** Achse B ist scharf geschaltet (SSH/RDP-
|
|
Verbindungen loesen Zugangsdaten benutzer-/gruppenabhaengig auf), die
|
|
Direktvergabe-Schreibpfade sind abgeschaltet (HTTP 410), zwei
|
|
Konsolidierungs-Reports stehen bereit (Schritt 6, bewusst ohne
|
|
automatische Zusammenlegung/Entzug, siehe Abschnitt 2), und alle 14 durch
|
|
Schritt 4/5 verursachten Testfehlschlaege sind behoben (Schritt 7, siehe
|
|
Abschnitt 1d). Es verbleiben nur die urspruenglichen, von Teil D
|
|
unabhaengigen 18 Bugs aus Teil C (siehe `FORTSETZUNG_Teil_C.md` Abschnitt
|
|
0) sowie ein paar bewusst zurueckgestellte Backlog-Punkte (Abschnitt 2,
|
|
Schritt 6). Repo: `ssh_jumphost`, erreichbar ueber die device_bash-Bridge
|
|
unter `$HOME/mnt/ssh_jumphost`.
|
|
Teil C ist vollstaendig abgeschlossen, siehe `FORTSETZUNG_Teil_C.md`.
|
|
|
|
**Reihenfolge des Gesamtauftrags:** Teil A/B -> Teil E -> Teil C (fertig) ->
|
|
**Teil D (hier, in Arbeit)** -> Teil F -> Aufgabe #11.
|
|
|
|
Die vollstaendige fachliche Spezifikation steht in
|
|
`Umsetzungsauftrag_Sonnet5.md`, Abschnitt "TEIL D" (Zeilen 449-653 zum
|
|
Zeitpunkt dieses Dokuments -- IMMER per `grep -n "^# TEIL"` neu verorten,
|
|
da sich Zeilennummern durch andere Aenderungen verschieben). Dieses
|
|
Fortsetzungsdokument dupliziert die Spezifikation NICHT, sondern haelt nur
|
|
Umsetzungsstand, Entscheidungen und naechste Schritte fest.
|
|
|
|
---
|
|
|
|
## 0) Betreiberentscheidung: die drei toten Rollen (D.3)
|
|
|
|
Per Rueckfrage am 2026-08-31 entschieden -- **alle drei entfernen, keine
|
|
ausimplementieren**:
|
|
|
|
1. **`clipboard`** -> entfernen. Clipboard bleibt ausschliesslich ueber
|
|
`hosts.clipboard_enabled` (host-global) steuerbar, keine Pro-Nutzer-
|
|
Ebene. `Jumphost_Konzept.md` 4.6 wurde entsprechend korrigiert (warb
|
|
vorher faelschlich mit rollenbasierter Steuerung).
|
|
2. **`session_recording_view`** -> entfernen. Sitzungs-Playback bleibt
|
|
dauerhaft ausschliesslich `require_global_admin` vorbehalten.
|
|
3. **`admin_hostgroup`** -> entfernen. Keine delegierte Hostgruppen-Admin-
|
|
Ebene; es bleibt bei genau zwei Stufen (globaler Admin vs. granulare
|
|
Rollen ueber Gruppen).
|
|
|
|
**Diese Entscheidung darf von keiner Folge-Session stillschweigend
|
|
geaendert werden.**
|
|
|
|
---
|
|
|
|
## 1) Was in dieser Session bereits fertiggestellt wurde (D.6 Schritt 1: Vorarbeit, rechteneutral)
|
|
|
|
- **Tote Rollen entfernt:** neue Migration
|
|
`app/db/migrations/0015_remove_dead_roles.sql` (loescht zuerst alle
|
|
Vergaben in `user_hostgroup_roles`/`group_hostgroup_roles` fuer die drei
|
|
Rollen, dann die `roles`-Zeilen selbst). Unabhaengig gegen einen
|
|
synthetischen Voll-Migrationslauf (0001-0015) getestet: keine FK-
|
|
Verletzungen, `roles` enthaelt danach nur noch `ssh_connect`,
|
|
`rdp_connect`, `file_transfer`, `credentials_view`, `credentials_manage`.
|
|
`app/models/schemas.py::ROLE_NAME` (Pydantic-Whitelist) entsprechend
|
|
gekuerzt und kommentiert.
|
|
- **`static/js/admin.js`:** `ROLE_NAMES`-Konstante war hartkodiert (Doppelung
|
|
zur DB) -- jetzt `let ROLE_NAMES = []`, wird beim ersten Aufruf von
|
|
`loadRolesTab()` per `GET /admin/roles/names` gefuellt (dieser Endpunkt
|
|
existierte bereits, war nur ungenutzt). Erfuellt nebenbei einen Punkt aus
|
|
D.5 ("ROLE_NAMES von der Hartkodierung auf GET /admin/roles/names
|
|
umstellen").
|
|
- **S6 (Katalog-Protokoll-Filter) gegengeprueft: war bereits behoben.**
|
|
`app/catalog/routes.py` filterte schon vor dieser Session korrekt nach
|
|
`h.protocol` (vermutlich in einer frueheren Phase-9-Session erledigt, der
|
|
Umsetzungsauftrag war nur nicht nachgezogen). Trotzdem im Zuge von S13
|
|
(siehe unten) umgebaut -- die Protokoll-Filterung ist jetzt in Python
|
|
nach der zentralen `rbac.py`-Abfrage statt in eingebettetem SQL.
|
|
- **S12 geschlossen:** `revoke_role` (`app/admin/routes.py`) prueft jetzt
|
|
zusaetzlich `_assert_user_in_scope(conn, payload.user_id)` (vorher nur
|
|
Hostgruppe), `revoke_group_role` zusaetzlich
|
|
`_assert_user_group_in_scope(conn, payload.user_group_id)` -- analog zu
|
|
den bereits vorhandenen Pruefungen in `grant_role`/`grant_group_role`.
|
|
Hinweis: die urspruengliche Schwere dieses Punkts war an Mandanten-Scoping
|
|
gebunden (ein Tenant-Admin haette sonst hostgruppen-fremde User treffen
|
|
koennen) -- seit dem Teil-C-Rueckbau ist das nur noch eine
|
|
Existenz-/Konsistenzpruefung (404 statt stillem No-Op-DELETE), kein
|
|
eigentliches Sicherheitsloch mehr.
|
|
- **S13 (RBAC-Logik dreifach implementiert) geschlossen fuer die
|
|
verbleibenden zwei Implementierungen** (die dritte, `app/tenancy.py`, war
|
|
bereits in Teil C nach `_to_delete/` verschoben worden): `app/rbac.py`
|
|
bekam eine neue Funktion `user_host_group_ids_with_any_role(conn, *,
|
|
user_id, role_names)` -- loest die Vereinigung aus Direkt- und
|
|
Gruppenvergabe fuer eine oder mehrere Rollen auf, ueber ALLE Hostgruppen
|
|
hinweg (Mengenrueckgabe statt Einzel-Bool). `app/catalog/routes.py` baut
|
|
die UNION/EXISTS-Logik nicht mehr selbst nach, sondern ruft diese
|
|
Funktion viermal auf (ssh_connect, rdp_connect, file_transfer,
|
|
credentials_view+manage) und verknuepft/filtert in Python
|
|
(Protokoll-Abgleich inklusive). `app/rbac.py` ist damit die einzige
|
|
verbleibende Wahrheitsquelle fuer RBAC-Auflösung.
|
|
- **Echter `pytest`-Lauf nach allen obigen Aenderungen: unveraendert 124
|
|
passed, 18 failed** -- exakt dieselben 18 vorbestehenden, von Teil D
|
|
unabhaengigen Fehlschlaege wie am Ende von Teil C (siehe
|
|
`FORTSETZUNG_Teil_C.md` Abschnitt 3). **Keine Regression.**
|
|
`tests/test_rbac.py` und die catalog-bezogenen Tests in
|
|
`tests/test_phase9.py` laufen weiterhin gruen.
|
|
|
|
### Bewusst NICHT in Schritt 1 erledigt: S4
|
|
|
|
D.6 listet S4 ("Nicht-Admin mit `credentials_manage` kann fremde
|
|
Zugangsdaten anziehen") unter Schritt 1 ("rechteneutral"), D.5 beschreibt
|
|
die eigentliche Lösung aber als Teil der Achse-B-Umstellung ("Prüfung im
|
|
Nicht-Admin-Zweig wird von 'existiert' auf 'ist einer meiner Gruppen
|
|
freigegeben' umgestellt"). Ein Blick auf den Code zeigt, warum eine
|
|
Zwischenloesung in Schritt 1 riskanter waere als sie nuetzt: `CurrentUser`
|
|
unterscheidet aktuell NICHT zwischen "Nicht-Admin-Session-User ueber
|
|
Hostgruppen-Rolle durchgelassen" und "API-Token mit passendem Scope
|
|
durchgelassen" (beide kommen mit `is_admin=False` aus
|
|
`require_admin_scope_or_host_role` zurueck, siehe `app/auth/deps.py:190-224`).
|
|
Tokens sind aber laut Docstring dort explizit als admin-aequivalent fuer
|
|
ihren Scope gedacht ("kein Token traegt je eine Hostgruppen-Rolle") -- eine
|
|
pauschale Verschaerfung in `assign_rdp_credential_to_host`/
|
|
`map_ssh_key_to_host` wuerde also entweder Tokens unbeabsichtigt
|
|
einschraenken oder muesste `CurrentUser` um ein Unterscheidungsmerkmal
|
|
erweitern, was kein rein "rechteneutraler" Schritt mehr waere. **Entscheidung
|
|
dieser Session: S4 wird korrekt erst mit Achse B in Schritt 5 geloest** (dort
|
|
ist die Unterscheidung ohnehin noetig, weil Achse B die Freigabe explizit
|
|
prueft). Bis dahin bleibt die Luecke offen -- **das ist ein bekanntes Risiko,
|
|
kein vergessener Punkt.**
|
|
|
|
---
|
|
|
|
## 1a) Was in dieser Session zusaetzlich fertiggestellt wurde (D.6 Schritt 3, alle 6 Punkte: Migration)
|
|
|
|
Drei neue, sequenziell aufeinander aufbauende Migrationen, jede einzeln per
|
|
synthetischem Voll-Migrationslauf (`sqlite3.executescript` ueber die
|
|
gesamte Kette + `PRAGMA foreign_key_check` + Stichproben-Scenario) UND
|
|
anschliessend per echtem `pytest`-Lauf gegenverifiziert:
|
|
|
|
1. **`app/db/migrations/0016_ssh_password_credential_objects.sql`**
|
|
(D.4 Punkt 4, Vorbedingung fuer Achse B): baut `ssh_password_credentials`
|
|
von einer reinen `host_id`-Zuordnungstabelle zu einem eigenstaendigen
|
|
Objekt mit eigener ID um, analog zu Migration 0012
|
|
(`rdp_credentials`/`host_rdp_credential_map`). Die alte Tabelle wird zu
|
|
`ssh_password_credentials_legacy` umbenannt und dauerhaft aufbewahrt
|
|
(Projektkonvention, nicht droppen), die neue `ssh_password_credentials`
|
|
(id, label, username, password_enc, created_at, rotated_at) plus
|
|
`host_ssh_password_credential_map` (host_id PK, ssh_password_credential_id)
|
|
werden befuellt, Korrelation Alt->Neu ueber `password_enc` als natuerlichen
|
|
Unique-Schluessel (jede Verschluesselung nutzt einen frischen Nonce).
|
|
Begleitende Code-Anpassungen: `app/admin/routes.py`
|
|
(`set_ssh_password_credentials`/`delete_ssh_password_credentials` sowie
|
|
die beiden Lesepfade im Host-Detail-Endpunkt und die Hard-Delete-Kaskade
|
|
pruefen/schreiben jetzt ueber die Map-Tabelle), `app/ssh_proxy/proxy.py`
|
|
(Credential-Lookup ebenso ueber die Map-Tabelle).
|
|
2. **`app/db/migrations/0017_personal_groups.sql`** (D.4 Punkte 1-2):
|
|
fuegt `user_groups.is_personal` (INTEGER NOT NULL DEFAULT 0) hinzu, dazu
|
|
einen `BEFORE INSERT ON user_group_members`-Trigger
|
|
(`enforce_personal_group_single_member`), der fuer persoenliche Gruppen
|
|
ab dem zweiten Mitglied mit `RAISE(ABORT, ...)` hart abbricht (D.7:
|
|
"Rechteausweitung ueber persoenliche Gruppen" -- serverseitig erzwungen,
|
|
nicht nur Konvention). Anschliessend legt die Migration in einem
|
|
`BEGIN IMMEDIATE ... COMMIT`-Block fuer jeden aktiven User mit
|
|
mindestens einer `user_hostgroup_roles`-Zeile eine persoenliche Gruppe
|
|
an (`'Persoenlich: ' || username || ' (#' || id || ')'`), macht ihn zum
|
|
einzigen Mitglied und spiegelt alle seine Direktvergaben 1:1 nach
|
|
`group_hostgroup_roles` (inkl. `granted_by`/`granted_at`/`expires_at`
|
|
verbatim, keine Neuvergabe). Bewusst OHNE
|
|
`PRAGMA foreign_keys = OFF/ON` (anders als Migration 0014) -- reine
|
|
INSERTs koennen hier keine FK-Verletzung erzeugen, ein Abschalten der
|
|
Pruefung waere eine unbegruendete Abweichung und wurde deshalb aus einem
|
|
ersten Entwurf wieder entfernt. Mit einem synthetischen 5-User-Szenario
|
|
verifiziert (admin, zwei User mit Direktvergaben, ein User ohne
|
|
Vergabe, ein soft-geloeschter User mit Vergabe) -- korrekte Anlage,
|
|
korrekter Ausschluss der letzten beiden, exakte Feld-Uebernahme, und der
|
|
Trigger blockiert nachweislich ein zweites `INSERT INTO
|
|
user_group_members` fuer dieselbe persoenliche Gruppe.
|
|
3. **`app/db/migrations/0018_credential_group_grants.sql`** (D.4 Punkte 3
|
|
und 5): legt die drei Achse-B-Freigabetabellen an --
|
|
`group_ssh_key_grants`, `group_rdp_credential_grants`,
|
|
`group_ssh_password_credential_grants` (je zusammengesetzter PK aus
|
|
`user_group_id` + Credential-ID, `granted_by`/`granted_at`/`expires_at`,
|
|
Indizes auf beiden FK-Spalten, `ON DELETE CASCADE` auf `user_group_id`).
|
|
Befuellt rechteneutral per `INSERT OR IGNORE ... SELECT DISTINCT`: jede
|
|
Gruppe, die auf der Hostgruppe eines Hosts mit zugeordnetem Credential
|
|
die passende Connect-Rolle (`ssh_connect` bzw. `rdp_connect`) in
|
|
`group_hostgroup_roles` haelt, wird fuer dessen Credential(s) freigegeben
|
|
-- reproduziert exakt das heutige Verhalten. Muss zwingend NACH Migration
|
|
0017 laufen, sonst wuerden User mit ausschliesslich Direktvergaben (noch
|
|
nicht gespiegelt) faelschlich uebergangen. **Wichtig: diese Migration
|
|
aendert noch KEIN Laufzeitverhalten** -- `app/rbac.py` und die Proxies
|
|
lesen diese Tabellen noch nicht, das ist Schritt 4. Mit einem
|
|
synthetischen Mehrgruppen-Szenario verifiziert (Team-Gruppe "devs" +
|
|
`ssh_connect` auf einer Hostgruppe, dazu eine bereits gespiegelte
|
|
persoenliche Gruppe von "alice" mit derselben Rolle, drei Hosts
|
|
darunter einer mit SSH-Key, einer mit SSH-Passwort-Credential, einer
|
|
mit RDP-Credential auf einer Hostgruppe OHNE `rdp_connect`-Vergabe) --
|
|
Ergebnis: keine FK-Verletzungen, SSH-Key- und SSH-Passwort-Freigaben
|
|
korrekt fuer beide Gruppen erzeugt, `group_rdp_credential_grants`
|
|
korrekt leer geblieben.
|
|
4. **Echter `pytest`-Lauf gegen die volle Kette 0001-0018** (aufgeteilt in
|
|
vier serielle Gruppen ueber getrennte `device_bash`-Aufrufe mit je
|
|
eigenem `JUMPHOST_DATA_DIR`, wegen der ~120s-Aufrufgrenze; ein einzelner
|
|
`pytest -n 4`-Parallellauf wurde verworfen, da er durch
|
|
Worker-Interferenz 8 zusaetzliche, nicht reproduzierbare Errors erzeugte
|
|
-- kein echter Befund, siehe unten): **exakt 124 passed, 18 failed**,
|
|
Fehlschlaege 1:1 identisch mit den in `FORTSETZUNG_Teil_C.md` Abschnitt 3
|
|
dokumentierten 18 vorbestehenden Bugs (Signatur-Drift RDP/Session-
|
|
Broadcast, Session-Reaper-Doppel-BEGIN, Recorder-`.close()`, Test-
|
|
Substring-Fehlalarm, Session-Invalidierung nach Passwortwechsel,
|
|
`hard_deleted`-Logik). **Keine Regression durch Migrationen 0016-0018.**
|
|
|
|
**Schritt 3 ist damit vollstaendig abgeschlossen.** Empfehlung fuer die
|
|
naechste Session: vor dem eigentlichen Produktions-Deployment dieser
|
|
Migrationskette zusaetzlich `scripts/diff_effective_rights.py --before
|
|
<Kopie-der-Produktions-DB-vor-0016> --after <DB-nach-0018>` gegen die
|
|
ECHTEN Produktionsdaten laufen lassen (bisher nur synthetisch verifiziert)
|
|
-- siehe Docstring des Skripts.
|
|
|
|
---
|
|
|
|
## 1b) Testverifikation nach Schritt 4 -- 12 ERWARTETE neue Fehlschlaege, keine Regression
|
|
|
|
Echter `pytest`-Lauf (dieselbe Vier-Gruppen-Aufteilung wie zuvor, wegen der
|
|
~120s-Aufrufgrenze) gegen den vollstaendigen Codestand nach Schritt 4:
|
|
**30 failed, 112 passed** (vorher, am Ende von Schritt 3: 18 failed, 124
|
|
passed -- macht +12 neue Fehlschlaege). Alle 12 wurden EINZELN nachverfolgt
|
|
(nie pauschal angenommen) und sind auf genau zwei erwartete, mit dem Umbau
|
|
selbst zusammenhaengende Ursachen zurueckgefuehrt -- **keine davon ist eine
|
|
unerklaerte Regression:**
|
|
|
|
**Ursache A -- Test seedet Rechte direkt in `user_hostgroup_roles`, das
|
|
`app/rbac.py` seit Schritt 4 nicht mehr liest** (6 Tests):
|
|
`tests/test_pentest_security.py::test_user_cannot_access_host_outside_granted_hostgroup`,
|
|
`tests/test_phase9.py::test_credentials_manage_role_grants_non_admin_write_access`,
|
|
`tests/test_phase9.py::test_credentials_view_role_is_read_only`,
|
|
`tests/test_phase9.py::test_catalog_hosts_reports_can_view_credentials_flag`,
|
|
`tests/test_phase13.py::test_ssh_password_credentials_via_credentials_manage_role`,
|
|
`tests/test_rbac.py::test_rbac_grants_and_expiry`. Jeweils per Einzellauf
|
|
gegengeprueft: die Assertion schlaegt fehl, weil die zuvor per
|
|
`INSERT INTO user_hostgroup_roles ...` vergebene Rolle jetzt ignoriert wird
|
|
-- exakt das erwartete Verhalten, siehe Modul-Docstring von `app/rbac.py`.
|
|
Fix gehoert zu Schritt 7 (Tests auf Gruppen-basiertes Seeding umstellen).
|
|
|
|
**Ursache B -- Test ruft `connect_to_host()`/`load_private_key_for_host()`
|
|
ohne das neue Pflicht-Keyword `user_id` auf, UND/ODER die Test-DB seedet
|
|
keine Achse-B-Freigabe fuer den verwendeten Nutzer** (6 Tests):
|
|
`tests/test_phase10.py::test_load_private_key_for_host_uses_stored_passphrase`,
|
|
`tests/test_phase12.py::test_verbindung_ohne_hinterlegten_hostkey_wird_abgelehnt`,
|
|
`tests/test_phase12.py::test_abweichender_hostkey_bricht_vor_der_anmeldung_ab`,
|
|
`tests/test_phase12.py::test_verbindung_nutzt_benutzernamen_der_zugangsdaten`,
|
|
`tests/test_phase12.py::test_hostkey_wechsel_nach_der_pruefung_beendet_die_sitzung`,
|
|
`tests/test_phase12.py::test_altbestand_ohne_gespeicherten_hostkey_wird_nachgetragen`.
|
|
**Bewusst NICHT** mit einem schnellen `user_id=1` "repariert": diese Tests'
|
|
`_make_db()`-Hilfsfunktion seedet ueberhaupt keine `users`/`user_groups`/
|
|
`group_ssh_key_grants`-Zeilen -- ein einfaches Nachreichen des Arguments
|
|
wuerde `resolve_credential_for_user_on_host()` fuer jeden beliebigen
|
|
`user_id` leer zurueckliefern lassen und den Test von einem
|
|
`TypeError`-Fehlschlag in einen `HostNotConfiguredError`-Fehlschlag
|
|
verwandeln, ohne die eigentliche Testabsicht (Host-Key-Pinning, NICHT
|
|
Credential-Autorisierung) wiederherzustellen. Ein sauberer Fix gehoert
|
|
deshalb ebenfalls zu Schritt 7 -- `_make_db()` braucht dafuer echtes
|
|
Achse-B-Seeding (Benutzer, Gruppe, Mitgliedschaft, `group_ssh_key_grants`-
|
|
Zeile), keine Ein-Zeilen-Notloesung.
|
|
|
|
**Fuer Schritt 7 (Abschnitt 2 unten) folgt daraus eine Ergaenzung:** die
|
|
dort bereits gelistete Datei-Liste (`test_rbac.py`, `test_phase9.py`,
|
|
`test_phase13.py`, `test_admin_groups_tokens.py`) ist um
|
|
**`test_pentest_security.py`, `test_phase10.py` und `test_phase12.py`**
|
|
zu erweitern -- diese drei waren im urspruenglichen Umsetzungsauftrag NICHT
|
|
als betroffen gelistet, sind es aber nachweislich seit Schritt 4.
|
|
|
|
`tests/test_admin_groups_tokens.py` (im urspruenglichen Auftrag als
|
|
betroffen genannt) lief in dieser Session bereits **vollstaendig gruen**
|
|
weiter -- vermutlich, weil es (wie schon in Teil C festgestellt) bereits
|
|
gruppen-basiert seedet.
|
|
|
|
---
|
|
|
|
## 1c) Testverifikation nach Schritt 5 -- 2 ERWARTETE neue Fehlschlaege, keine Regression, plus 6 neue gruene Tests
|
|
|
|
Echter `pytest`-Lauf (dieselbe Vier-Gruppen-Aufteilung, plus ein separater
|
|
Lauf nur der neuen Datei) gegen den vollstaendigen Codestand nach Schritt 5:
|
|
**116 passed, 32 failed** (148 Tests insgesamt; vorher, am Ende von
|
|
Schritt 4: 112 passed, 30 failed, 142 Tests). Rechnet sich vollstaendig
|
|
auf: 142 alte Tests + 6 neue (`tests/test_teil_d_schritt5.py`, alle gruen)
|
|
= 148; von den 142 alten wurden GENAU 2 durch Schritt 5 neu zum
|
|
Fehlschlag, macht 112 - 2 + 6 = 116 passed und 30 + 2 = 32 failed. Beide
|
|
neuen Fehlschlaege wurden einzeln (nie pauschal) auf Schritt 5 selbst
|
|
zurueckgefuehrt -- **keine unerklaerte Regression:**
|
|
|
|
- **`tests/test_admin_crud.py::test_multi_role_grant_for_individual_user`**
|
|
-- ruft `POST /admin/roles/grant` auf und erwartet 200; bekommt jetzt
|
|
410. Erwartet: dieser Test pruefte exakt den jetzt retirierten
|
|
Direktvergabe-Pfad. Fix gehoert zu Schritt 7 (auf `/admin/group-roles/*`
|
|
umstellen, wie im Umsetzungsauftrag fuer diese Datei ohnehin vorgesehen).
|
|
- **`tests/test_pentest_security.py::test_role_on_one_hostgroup_does_not_grant_different_permission_type`**
|
|
-- seedet per rohem SQL direkt in `user_hostgroup_roles`; diese Tabelle
|
|
heisst seit Migration 0019 `user_hostgroup_roles_legacy`, das `INSERT`
|
|
scheitert daher jetzt mit `sqlite3.OperationalError: no such table:
|
|
user_hostgroup_roles` statt (wie unter Schritt 4) still zu laufen. War
|
|
bei den 12 Ursache-A-Fehlschlaegen aus Abschnitt 1b NICHT gelistet, weil
|
|
die Testaussage (403 bei fehlendem `file_transfer`) zufaellig unabhaengig
|
|
davon war, ob die separat geseedete `ssh_connect`-Rolle tatsaechlich
|
|
griff -- der Test blieb unter Schritt 4 also aus falschem Grund gruen.
|
|
Gehoert in dieselbe Kategorie wie die 6 Ursache-A-Tests aus 1b und damit
|
|
zu deren Schritt-7-Fix.
|
|
|
|
**Zur Vollstaendigkeit gegengeprueft, dass sich auch die 12
|
|
Ursache-A/B-Fehlschlaege aus Schritt 4 sauber wiederfinden:** vier davon
|
|
(`test_credentials_manage_role_grants_non_admin_write_access`,
|
|
`test_credentials_view_role_is_read_only`,
|
|
`test_ssh_password_credentials_via_credentials_manage_role`,
|
|
`test_catalog_hosts_reports_can_view_credentials_flag`) seeden ueber den
|
|
API-Aufruf `POST /admin/roles/grant` statt per rohem SQL -- ihre
|
|
Fehlermeldung hat sich dadurch ebenfalls verschoben (Assertion auf
|
|
Status 410 bzw. nachgelagerte Assertion, statt der urspruenglichen
|
|
Rollenpruefungs-Assertion aus Schritt 4), bleibt aber inhaltlich dieselbe
|
|
Ursache-A-Familie. `test_user_cannot_access_host_outside_granted_hostgroup`
|
|
und `test_rbac.py::test_rbac_grants_and_expiry` seeden wie
|
|
`test_role_on_one_hostgroup_...` oben per rohem SQL und zeigen daher
|
|
ebenfalls `no such table: user_hostgroup_roles` statt der Schritt-4-
|
|
Assertion. Keine dieser Verschiebungen aendert etwas an der Schritt-7-
|
|
Aufgabe (Tests auf Gruppen-Seeding umstellen) -- nur die Fehlermeldung, an
|
|
der man das erkennt, hat sich geaendert.
|
|
|
|
Die 6 neuen Tests in `tests/test_teil_d_schritt5.py` decken ab: 410 fuer
|
|
`/admin/roles/grant|revoke`; `GET /admin/roles` zeigt `via_group_name`;
|
|
Freigeben/Lesen/Entziehen fuer alle drei Achse-B-Arten inkl. 422 bei
|
|
unbekannter Art und 404 bei nicht existierendem Zugangsdatensatz; `GET
|
|
/admin/ssh-password-credentials` (globale Liste, keine Passwort-Leckage);
|
|
S4 (freigegebener Key darf angehaengt werden, nicht freigegebener wird mit
|
|
403 abgelehnt); S10 (`gained_rights`/`lost_rights` im Audit-Event). Dazu
|
|
ein isolierter `GET /admin`-Smoke-Test: die Seite rendert, enthaelt das
|
|
neue Panel und nicht mehr den alten Text "Rolle(n) an Benutzer vergeben"
|
|
-- sowie `tests/test_csp_compliance.py`, das ohnehin bei jedem Lauf
|
|
mitlief und weiterhin gruen ist.
|
|
|
|
---
|
|
|
|
## 1d) Testverifikation nach Schritt 7 -- alle 14 bekannten Fehlschlaege behoben, nur noch die urspruenglichen 18 (Teil C) offen
|
|
|
|
Echter `pytest`-Lauf (dieselbe Vier-Gruppen-Aufteilung) gegen den
|
|
vollstaendigen Codestand nach Schritt 7: **134 passed, 18 failed** (152
|
|
Tests insgesamt; vorher, am Ende von Schritt 6: 118 passed, 32 failed, 150
|
|
Tests -- die 2 zusaetzlichen Tests kommen aus `tests/test_rbac.py`s
|
|
Neuschreibung, siehe unten). Rechnet sich vollstaendig auf: 118 + 2 neue
|
|
`test_rbac.py`-Tests = 120; von den bisher 32 fehlschlagenden Tests wurden
|
|
GENAU 14 durch das Umstellen auf Gruppen-basiertes Seeding repariert, macht
|
|
120 + 14 = 134 passed und 32 - 14 = 18 failed. **Die verbleibenden 18
|
|
Fehlschlaege sind exakt (namentlich geprueft, keine zufaellige
|
|
Uebereinstimmung der Anzahl) die urspruenglichen, in `FORTSETZUNG_Teil_C.md`
|
|
Abschnitt 0 dokumentierten Bugs aus Teil C** -- ausserhalb des Schritt-7-
|
|
Auftrags (der deckte namentlich nur die 14 aus den Abschnitten 1b/1c ab)
|
|
und explizit NICHT ohne neue Anweisung zu beheben (Betreiberentscheidung
|
|
aus einer fruegeren Session, siehe `FORTSETZUNG_Teil_C.md`).
|
|
|
|
Damit ist Teil D (Umsetzungsauftrag_Sonnet5.md, D.6 Schritte 1-7)
|
|
**vollstaendig abgeschlossen.** Die einzigen offenen Punkte sind bewusst
|
|
zurueckgestellte Backlog-Punkte (siehe Abschnitt 2, Schritt 6: manuelle
|
|
Konsolidierung persoenlicher Gruppen; manuelles Bestaetigen/Entziehen der
|
|
Achse-B-Vorbefuellung; siehe auch die "Fuer eine kuenftige Session"-Hinweise
|
|
in den Abschnitten 1/1a/1b) sowie der unabhaengige 18-Bugs-Rucksack aus
|
|
Teil C.
|
|
|
|
---
|
|
|
|
## 2) Was noch zu tun ist (D.6 Schritte 2-7)
|
|
|
|
**Schritt 2 ist bereits erfuellt** (Teil C ist abgeschlossen, Voraussetzung
|
|
fuer Schritt 3). **Schritt 3 UND Schritt 4 sind ebenfalls vollstaendig
|
|
abgeschlossen**, siehe Abschnitt 1a bzw. 1b -- naechster offener Schritt
|
|
ist Schritt 5.
|
|
|
|
### Schritt 3: Migration (D.4) -- VOLLSTAENDIG FERTIG (alle 6 Punkte)
|
|
1. ~~Neue Spalte/Kennzeichen "ist persoenliche Gruppe" auf `user_groups`~~
|
|
**FERTIG** -- `app/db/migrations/0017_personal_groups.sql`
|
|
(`user_groups.is_personal`), inkl. serverseitigem Trigger gegen ein
|
|
zweites Mitglied. Siehe Abschnitt 1a.
|
|
2. ~~Fuer jeden User mit >=1 Zeile in `user_hostgroup_roles`: persoenliche
|
|
Gruppe anlegen ...~~ **FERTIG** -- Teil derselben Migration 0017. Siehe
|
|
Abschnitt 1a.
|
|
3. ~~Drei neue Achse-B-Tabellen~~ **FERTIG** --
|
|
`app/db/migrations/0018_credential_group_grants.sql`
|
|
(`group_ssh_key_grants`, `group_rdp_credential_grants`,
|
|
`group_ssh_password_credential_grants`). Siehe Abschnitt 1a.
|
|
4. ~~Vorher noetig: `ssh_password_credentials` ... umbauen~~ **FERTIG** --
|
|
`app/db/migrations/0016_ssh_password_credential_objects.sql`
|
|
(`ssh_password_credentials_legacy` + neues eigenstaendiges Objekt +
|
|
`host_ssh_password_credential_map`), inkl. Anpassung der lesenden/
|
|
schreibenden Codepfade in `app/admin/routes.py` und
|
|
`app/ssh_proxy/proxy.py`. Siehe Abschnitt 1a.
|
|
5. ~~Achse B rechteneutral vorbefuellen~~ **FERTIG** -- Teil von Migration
|
|
0018 (`INSERT OR IGNORE ... SELECT DISTINCT`, gegen ein
|
|
Mehrgruppen-Szenario verifiziert). Siehe Abschnitt 1a.
|
|
6. ~~Verpflichtend (D.7): Vorher/Nachher-Diff der effektiven Rechte je
|
|
Benutzer~~ **FERTIG** -- neues, dauerhaftes Werkzeug
|
|
`scripts/diff_effective_rights.py` (nicht nur ein Einmal-Skript: nimmt
|
|
zwei SQLite-Dateipfade `--before`/`--after`, oeffnet beide read-only und
|
|
vergleicht fuer jeden aktiven User und jede Rolle aus
|
|
`app.models.schemas.ROLE_NAME` die Menge der Hostgruppen-IDs ueber
|
|
`app.rbac.user_host_group_ids_with_any_role` -- dieselbe Funktion, die
|
|
die Anwendung selbst zur Laufzeit nutzt, keine Nachbau-Logik. Exit-Code
|
|
1 bei jeder Abweichung, 0 bei Uebereinstimmung; alternativ `--dump
|
|
<db> --out <json>` fuer einen reinen Snapshot-Export vor einem
|
|
Wartungsfenster. Verifiziert an einem synthetischen 5-User-Szenario
|
|
(Direktvergabe, Gruppenvergabe, Ueberschneidung von Direkt- und
|
|
Gruppenvergabe auf derselben Hostgruppe/Rolle als Dedup-Test, sowie eine
|
|
abgelaufene Vergabe als Ausschluss-Test): Diff gegen die Kette
|
|
0001-0015 vs. 0001-0018 mit identischem Seed liefert **keine
|
|
Abweichung** (Exit 0). Zusaetzlich mit einer Negativprobe abgesichert
|
|
(eine `group_hostgroup_roles`-Zeile in der Nachher-DB absichtlich
|
|
geloescht) -- das Werkzeug erkennt den Rechteverlust korrekt (Exit 1,
|
|
`user_id=3 ('bob'), Rolle='rdp_connect': VERLOREN auf Hostgruppen [2]`),
|
|
das Werkzeug ist also kein Blindgaenger, der immer "keine Abweichung"
|
|
meldet.
|
|
|
|
### Schritt 4: Lesepfade umstellen -- FERTIG
|
|
- ~~`app/rbac.py::user_has_role()` auf einen Zweig kuerzen~~ **FERTIG.**
|
|
`app/rbac.py` komplett neu geschrieben: `user_has_role()`,
|
|
`user_has_role_for_host()` und `user_host_group_ids_with_any_role()`
|
|
lesen nur noch `group_hostgroup_roles` -- der `user_hostgroup_roles`-Teil
|
|
des fruehreren `UNION` ist vollstaendig entfallen. Sicher, weil Migration
|
|
0017 (Schritt 3) jede vormals direkte Vergabe bereits 1:1 in eine
|
|
persoenliche Gruppe gespiegelt hat.
|
|
- ~~Zwei neue Funktionen in `app/rbac.py`~~ **FERTIG:**
|
|
`user_can_use_credential(conn, *, user_id, kind, credential_id)` ("darf
|
|
Benutzer X Credential Y nutzen") und
|
|
`resolve_credential_for_user_on_host(conn, *, user_id, host_id, kind)`
|
|
("welches Credential gilt fuer Benutzer X auf Host Z"), beide generisch
|
|
ueber `kind: Literal["ssh_key", "rdp_credential",
|
|
"ssh_password_credential"]` fuer alle drei Achse-B-Tabellen aus Migration
|
|
0018. Deterministische Auswahlregel wie im Auftrag empfohlen: bei mehr
|
|
als einem Treffer wirft `resolve_credential_for_user_on_host()` eine neue
|
|
`AmbiguousCredentialError` (mit `host_id`, `kind`, `credential_ids`) statt
|
|
still auszuwaehlen. Kann strukturell nur bei `kind="ssh_key"` auftreten
|
|
(n:m-Zuordnung ueber `host_ssh_key_map`) -- RDP- und SSH-Passwort-Map
|
|
haben laut Schema PK auf `host_id`, also hoechstens einen Treffer.
|
|
- ~~`app/ssh_proxy/proxy.py` ... auf die neue Auflösung umstellen~~
|
|
**FERTIG.** `load_ssh_key_credential_for_host()` und
|
|
`load_ssh_password_credential_for_host()` (sowie die davon abgeleitete
|
|
`load_private_key_for_host()`) nehmen jetzt ein PFLICHT-Keyword-Argument
|
|
`user_id` und loesen ueber `resolve_credential_for_user_on_host()` auf,
|
|
statt blind per Host-ID zu selektieren. `connect_to_host()` nimmt
|
|
ebenfalls `user_id` entgegen und reicht es durch; die drei Aufrufer
|
|
(`app/ssh_proxy/terminal_ws.py`, `app/ssh_proxy/sftp.py` x2) uebergeben
|
|
`user_id=user.id`. `AmbiguousCredentialError` ist in `SSH_SETUP_ERRORS`
|
|
aufgenommen (von `terminal_ws.py`/`sftp.py` bereits gemeinsam behandelt)
|
|
und hat eine eigene Klartextmeldung in `describe_connection_error()`.
|
|
- ~~`app/rdp_proxy/ws_tunnel.py` ... auf die neue Auflösung umstellen~~
|
|
**FERTIG.** Die vormals direkte
|
|
`SELECT ... FROM host_rdp_credential_map JOIN rdp_credentials ... WHERE
|
|
host_id = ?`-Abfrage ist durch `resolve_credential_for_user_on_host(...,
|
|
kind="rdp_credential")` plus eine anschliessende ID-Abfrage ersetzt.
|
|
`AmbiguousCredentialError` wird defensiv abgefangen (strukturell nicht
|
|
erreichbar, siehe oben) und wie der "kein Credential"-Fall ueber
|
|
`_reject()` sauber beantwortet, statt unbehandelt durchzuschlagen.
|
|
- `app/catalog/routes.py` **brauchte keine weitere Aenderung** -- war seit
|
|
Schritt 1 (S13) bereits vollstaendig auf
|
|
`user_host_group_ids_with_any_role()` umgestellt und profitiert von der
|
|
Zweig-Kuerzung automatisch. Die D.5-Empfehlung, `can_connect` zusaetzlich
|
|
abzubilden, ob ueberhaupt ein fuer den Benutzer nutzbares Credential
|
|
existiert, ist NICHT umgesetzt (kein Pflichtpunkt von D.6 Schritt 4) --
|
|
offener Backlog-Punkt fuer eine spaetere Session.
|
|
- ~~**Vorab-Report (D.7, Pflicht vor diesem Schritt):** "Hosts mit
|
|
Zugangsdaten ohne Gruppenfreigabe"~~ **FERTIG.** Neues Werkzeug
|
|
`scripts/pre_schritt4_checks.py` (zwei Reports in einem Skript, `--db
|
|
<pfad>`, Exit-Code 1 bei Funden). Mit einem synthetischen 3-Host-
|
|
Szenario verifiziert (Host mit einem abgedeckten Schluessel -> still;
|
|
Host mit zwei Schluesseln, beide dergleichen Gruppe freigegeben -> Report
|
|
2 schlaegt an; Host mit einem Zugangsdatensatz auf einer Hostgruppe ohne
|
|
jede `ssh_connect`-Vergabe -> Report 1 schlaegt an) -- beide Reports
|
|
feuern exakt an den engineeren Problemfaellen und bleiben beim sauberen
|
|
Host still.
|
|
- ~~**Vorab-Pruefung (D.7):** welche Hosts haben mehr als einen SSH-Key~~
|
|
**FERTIG** -- Report 2 desselben Skripts, siehe oben.
|
|
|
|
**WICHTIG fuer die naechste Session:** Diese beiden Reports wurden nur
|
|
gegen ein synthetisches Testszenario gefahren, NICHT gegen die echten
|
|
Produktionsdaten (dafuer gibt es in dieser Umgebung keine Kopie). Vor dem
|
|
tatsaechlichen Produktions-Deployment dieses Codestands MUSS
|
|
`scripts/pre_schritt4_checks.py --db <Kopie-der-Produktions-DB>` einmal
|
|
real laufen und sauber (Exit 0) sein -- siehe auch die analoge Empfehlung
|
|
fuer `scripts/diff_effective_rights.py` in Abschnitt 1a.
|
|
|
|
### Schritt 5: Schreibpfade abschalten -- FERTIG
|
|
- ~~`POST /admin/roles/grant` + `/revoke` auf HTTP 410~~ **FERTIG.** Beide
|
|
Endpunkte nehmen keinen Body mehr entgegen (bewusst -- ein alter Client
|
|
soll den 410 sehen, keinen 422 wegen eines irrelevanten
|
|
Validierungsdetails) und antworten mit HTTP 410 plus Verweis auf
|
|
`/admin/group-roles/grant|revoke`. Die Auth-Dependency bleibt aktiv
|
|
(kein anonymer Zugriff auf den Endpunktnamen).
|
|
- ~~`GET /admin/roles` zur abgeleiteten Sicht umbauen~~ **FERTIG.** Liest
|
|
jetzt `group_hostgroup_roles JOIN user_groups JOIN user_group_members
|
|
JOIN users (deleted_at IS NULL) JOIN host_groups JOIN roles`, gefiltert
|
|
auf nicht abgelaufene `expires_at`. Neue Felder `via_group_id`/
|
|
`via_group_name` (S1: macht sichtbar, WELCHE Gruppe ein Recht vermittelt
|
|
-- steht ein Benutzer ueber zwei Gruppen fuer dieselbe Rolle/Hostgruppe
|
|
berechtigt, erscheinen zwei Zeilen). `deleted_at IS NULL` filtert
|
|
Karteileichen aus (S9).
|
|
- ~~S4 loesen~~ **FERTIG, aber NUR fuer `map_ssh_key_to_host` und
|
|
`assign_rdp_credential_to_host`** -- beide pruefen jetzt zusaetzlich zur
|
|
Existenz (`_assert_ssh_key_in_scope`/`_assert_rdp_credential_in_scope`)
|
|
`user_can_use_credential(conn, user_id=admin.id, kind=..., credential_id=...)`,
|
|
wenn `not admin.is_admin and not admin.is_token` (neues Feld
|
|
`CurrentUser.is_token`, gesetzt in `app/auth/deps.py::_validate_api_token`
|
|
-- unterscheidet einen echten Nicht-Admin-Session-User von einem
|
|
API-Token mit passendem Scope, das wie ein Admin behandelt bleibt).
|
|
Verletzung -> HTTP 403. **Bewusste Abweichung vom oben unter Schritt 4
|
|
notierten Plan:** `set_ssh_password_credentials`/
|
|
`delete_ssh_password_credentials` bekommen die Pruefung NICHT -- anders
|
|
als bei SSH-Keys/RDP-Zugangsdaten legt dieser Endpunkt IMMER ein neues
|
|
Credential-Objekt frisch an bzw. rotiert das bestehende in place; es gibt
|
|
keinen `credential_id`-Parameter, ueber den ein fremdes, bereits
|
|
existierendes Objekt angehaengt werden koennte -- die S4-Angriffsflaeche
|
|
("beliebiges vorhandenes Credential an eigenen Host haengen") existiert
|
|
dort strukturell nicht. Mit einem echten End-to-End-Test verifiziert
|
|
(`test_s4_non_admin_needs_group_grant_to_attach_credential` in
|
|
`tests/test_teil_d_schritt5.py`): Mitglied mit `credentials_manage`
|
|
haengt einen freigegebenen Key erfolgreich an, ein zweiter, existierender
|
|
aber nicht freigegebener Key wird mit 403 abgelehnt.
|
|
- ~~Neue Achse-B-Endpunkte~~ **FERTIG.** Ein Endpunkt-Trio bedient alle
|
|
drei Credential-Arten ueber einen `{kind}`-Pfadparameter (FastAPI-Literal-
|
|
Validierung, unbekannte Art -> 422) statt drei Kopien: `POST
|
|
/admin/group-credentials/{kind}/grant`, `POST
|
|
/admin/group-credentials/{kind}/revoke`, `GET
|
|
/admin/group-credentials/{kind}`. Neue Schemas
|
|
`GroupCredentialGrantRequest`/`GroupCredentialRevokeRequest`
|
|
(`app/models/schemas.py`) -- Struktur bewusst analog zu
|
|
`GroupRoleGrantRequest`. Dazu **neu** `GET /admin/ssh-password-credentials`
|
|
(globale Liste, analog zu `list_rdp_credentials()`) -- fehlte bisher
|
|
komplett, obwohl das Objekt seit Migration 0016 eine eigene ID hat; ohne
|
|
diesen Endpunkt haette die neue Achse-B-UI fuer diese Credential-Art
|
|
keine Datenquelle fuer den Auswahl-Dropdown gehabt.
|
|
- ~~`POST /admin/user-groups/{id}/members` S10~~ **FERTIG.**
|
|
`add_group_member()`/`remove_group_member()` ermitteln vor der
|
|
Mutation ueber die neue Hilfsfunktion `_group_effective_rights_summary()`
|
|
die aktuell von der Gruppe gewaehrten Rechte (Achse A: Liste
|
|
`{host_group, role}`; Achse B: Anzahl freigegebener Zugangsdaten je Art)
|
|
und schreiben sie als `gained_rights`/`lost_rights` in die
|
|
`details_json`-Spalte des Audit-Events. **Bewusst KEIN Diff** gegen
|
|
zuvor schon ueber ANDERE Gruppen gehaltene Rechte des Users (deutlich
|
|
groesserer Umbau) -- beschreibt, was DIESE Gruppe gewaehrt, nicht
|
|
zwingend das Netto-Delta. Mit echtem Test verifiziert
|
|
(`test_s10_audit_event_carries_gained_and_lost_rights`).
|
|
- ~~UI: Direktvergabe-Formular entfernen~~ **FERTIG.** Panel "Rolle(n) an
|
|
Benutzer vergeben" (`templates/admin.html`, vormals ~Zeilen 498-529)
|
|
vollstaendig entfernt; `#role-grants-table` ist jetzt eine
|
|
nicht-editierbare Tabelle (Spalten Benutzer/Hostgruppe/Rolle/Gruppe/
|
|
Ablauf, keine Aktionen-Spalte mehr). Neues Panel "Zugangsdaten fuer
|
|
Gruppe freigeben" (Gruppe-/Art-/Zugangsdatensatz-Auswahl, Ablaufdatum,
|
|
Tabelle mit Entziehen-Button). `static/js/admin.js`: `#role-grant-form`-
|
|
Handler entfernt, `refreshRoleGrants()` liest die neue Antwortform
|
|
(`via_group_name`), neue Funktionen `populateCredentialSelect()`
|
|
(befuellt den Zugangsdatensatz-Dropdown je gewaehlter Art aus den
|
|
bereits vorhandenen Caches `cachedSshKeys`/`cachedRdpCredentials` plus
|
|
neu `cachedSshPasswordCredentials`) und `refreshGroupCredentialGrants()`
|
|
(holt alle drei Arten parallel, mergt fuer eine gemeinsame Tabelle).
|
|
`ROLE_NAMES` kam schon seit Schritt 1 aus `GET /admin/roles/names` --
|
|
hier keine Aenderung noetig. Verifiziert: `node --check` auf beiden
|
|
JS-Dateien, `tests/test_csp_compliance.py` gruen (CSP-Konformitaet der
|
|
neuen Inline-freien Struktur), `GET /admin` liefert das neue Panel und
|
|
NICHT mehr den alten Text "Rolle(n) an Benutzer vergeben".
|
|
- ~~`user_hostgroup_roles` leeren/umbenennen~~ **FERTIG** -- Migration
|
|
0019 (`ALTER TABLE user_hostgroup_roles RENAME TO
|
|
user_hostgroup_roles_legacy`), NICHT gedroppt (Projektkonvention siehe
|
|
`0010:9-14`, `0012:12-17`). Synthetisch gegen die volle Kette 0001-0019
|
|
verifiziert: keine FK-Verletzungen, alter Tabellenname verschwunden,
|
|
`user_hostgroup_roles_legacy` vorhanden und weiterhin abfragbar, Index
|
|
automatisch mitgewandert.
|
|
|
|
### Schritt 6: Konsolidierung -- FERTIG (als Report-Werkzeug, bewusst OHNE automatische Zusammenlegung/Entzug)
|
|
Zwei neue, rein lesende Endpunkte in `app/admin/routes.py`, beide hinter
|
|
`require_admin_or_scope("roles", "read")`:
|
|
|
|
- ~~Admin-Report "persoenliche Gruppen mit identischem Rechteprofil"~~
|
|
**FERTIG.** `GET /admin/reports/personal-groups`. Fuer jede Gruppe mit
|
|
`is_personal=1` (Migration 0017) berechnet
|
|
`_personal_group_rights_fingerprint()` einen kanonischen String aus ALLEN
|
|
Rechten (Achse A: `group_hostgroup_roles`, sortiert nach
|
|
Hostgruppe/Rolle/Ablauf; Achse B: alle drei Freigabetabellen, sortiert
|
|
nach Credential-ID/Ablauf) -- Gruppen mit identischem Fingerprint landen
|
|
im selben Cluster. Antwort: Liste von Clustern mit `group_count`,
|
|
`consolidation_candidate` (`group_count > 1`) und den Mitgliedsgruppen
|
|
(inkl. des jeweils einzigen Mitglieds-Users, siehe Migration-0017-
|
|
Trigger). Mit echtem Test verifiziert (zwei Gruppen mit identischer
|
|
`ssh_connect`-Rolle landen im selben Cluster, eine dritte ohne jede Rolle
|
|
bleibt allein).
|
|
- ~~restriktives Aufraeumen der Achse-B-Vorbefuellung~~ **BEWUSST NICHT als
|
|
automatischer Entzug umgesetzt -- als Report-Werkzeug FERTIG, siehe
|
|
Begruendung unten.** `GET /admin/reports/unconfirmed-credential-grants`
|
|
listet alle Achse-B-Freigaben mit `granted_by IS NULL` -- exakt die
|
|
Kennzeichnung, die Migration 0018 fuer ihre rechteneutrale Vorbefuellung
|
|
verwendet (ein manueller Grant ueber `POST
|
|
/admin/group-credentials/{kind}/grant` setzt immer `admin.id`). Sobald
|
|
ein Admin eine Zeile ueber diesen Endpunkt bestaetigt, verschwindet sie
|
|
aus dem Report (mit echtem Test verifiziert: Vorbefuellungs-Zeile
|
|
erscheint, nach manueller Bestaetigung nicht mehr).
|
|
|
|
**Warum kein automatischer Entzug:** die D.7-Zeile "Rechteausweitung durch
|
|
die Achse-B-Vorbefuellung" sieht ausdruecklich vor, das als Aufraeum-
|
|
Backlog zu DOKUMENTIEREN, "wenn diese Session/ein Folgeauftrag es nicht
|
|
mehr vollstaendig umsetzt" -- das System hat keine echten Nutzungsdaten
|
|
(wer benutzt welches Credential tatsaechlich), ein automatisches Entziehen
|
|
ungenutzt erscheinender Vorbefuellungs-Zeilen waere daher blindes Raten mit
|
|
echtem Rechteverlust-Risiko (genau das Risiko, das die erste D.7-Zeile
|
|
"Rechteverlust beim Abschalten der Direktvergabe" fuer den gesamten
|
|
Umbau vermeiden wollte). Der Report macht die Vorbefuellung sichtbar und
|
|
auditierbar; das tatsaechliche Entziehen bleibt eine bewusste,
|
|
fallweise Admin-Entscheidung ueber den bereits vorhandenen
|
|
`POST /admin/group-credentials/{kind}/revoke`-Endpunkt (Schritt 5).
|
|
Ebenso fuehrt `GET /admin/reports/personal-groups` KEINE automatische
|
|
Zusammenlegung durch -- das Verschmelzen zweier Gruppen aendert
|
|
Mitgliedschaften und wuerde bei falscher Annahme (zwei Gruppen mit *heute*
|
|
identischem Profil, die bewusst getrennt bleiben sollen, z.B. fuer
|
|
zukuenftige Differenzierung) unnoetig Struktur zerstoeren, die sich nicht
|
|
ohne Weiteres wiederherstellen laesst.
|
|
|
|
**Fuer eine kuenftige Session:** `GET /admin/reports/unconfirmed-credential-grants`
|
|
gegen die echten Produktionsdaten laufen lassen (analog zur Empfehlung fuer
|
|
`scripts/pre_schritt4_checks.py`/`scripts/diff_effective_rights.py` in den
|
|
Abschnitten 1/1a) und Zeile fuer Zeile entscheiden: freigeben (bestaetigen)
|
|
oder entziehen. `GET /admin/reports/personal-groups` als Grundlage fuer
|
|
eine manuelle Entscheidung nutzen, welche Cluster tatsaechlich zu echten
|
|
Team-Gruppen zusammengelegt werden sollen (z.B. per `PUT
|
|
/admin/user-groups/{id}` umbenennen und `is_personal` -- dafuer existiert
|
|
aktuell noch KEIN Endpunkt, der das Flag aendert; das waere ein
|
|
naheliegender, aber bisher nicht spezifizierter Folgepunkt).
|
|
|
|
### Schritt 7: Tests -- FERTIG (alle 14 bekannten Fehlschlaege behoben, siehe Abschnitt 1d)
|
|
Alle 14 in den Abschnitten 1b/1c namentlich bekannten Fehlschlaege sind
|
|
einzeln (nie pauschal) auf Gruppen-basiertes Seeding umgestellt und
|
|
verifiziert. Details siehe Abschnitt 1d. Kurzfassung je betroffener Datei:
|
|
|
|
- ~~`tests/test_rbac.py` ... vollstaendig neu geschrieben~~ **FERTIG.**
|
|
Alle drei Tests seeden jetzt ueber `user_groups`/`user_group_members`/
|
|
`group_hostgroup_roles` statt `user_hostgroup_roles`. Zwei zusaetzliche
|
|
Tests ergaenzt (Rolle bleibt bei Ablauf EINER von zwei Gruppen bestehen;
|
|
Rolle leckt nicht zwischen Hostgruppen) -- nicht im urspruenglichen
|
|
Umsetzungsauftrag gefordert, aber naheliegende Luecken angesichts des
|
|
Neuschreibens.
|
|
- ~~`tests/test_phase9.py`, `tests/test_phase13.py`,
|
|
`tests/test_admin_groups_tokens.py`~~ **`test_admin_groups_tokens.py`
|
|
war bereits gruen (siehe Abschnitt 1b), keine Aenderung noetig.
|
|
`test_phase9.py`/`test_phase13.py`: FERTIG**, neue Hilfsfunktionen
|
|
`_grant_group_role()` (beide Dateien) und `_grant_group_credential()`
|
|
(nur `test_phase9.py`, fuer den S4-Fall) ersetzen `POST
|
|
/admin/roles/grant`.
|
|
- ~~`tests/test_pentest_security.py` (2 Tests), `tests/test_phase10.py`
|
|
(1 Test), `tests/test_phase12.py` (5 Tests)~~ **FERTIG.** Bei
|
|
`test_phase10.py`/`test_phase12.py` zusaetzlich zur Gruppen-Freigabe das
|
|
neue Pflicht-Keyword `user_id` an allen `connect_to_host()`/
|
|
`load_private_key_for_host()`-Aufrufen ergaenzt; `test_phase12.py`s
|
|
`_make_db()`-Hilfsfunktion seedet jetzt immer einen Benutzer samt Gruppe
|
|
und (bei `with_key=True`) eine `group_ssh_key_grants`-Freigabe --
|
|
genau das zuvor als "kein Notloesungs-`user_id=1`" zurueckgestellte
|
|
echte Achse-B-Seeding aus Abschnitt 1b.
|
|
- ~~`tests/test_admin_crud.py` (1 Test)~~ **FERTIG, aber NICHT durch einen
|
|
Ersatztest fuer den Einzel-User-Pfad** -- `test_multi_role_grant_for_individual_user`
|
|
wurde zu `test_direct_multi_role_grant_for_individual_user_is_retired`:
|
|
bestaetigt, dass `POST /admin/roles/grant` mit `role_names` jetzt 410
|
|
liefert UND folgenlos bleibt (`GET /admin/roles` zeigt keine Rolle). Kein
|
|
Ersatz fuer die Mehrfachauswahl-Faehigkeit selbst noetig -- die deckt
|
|
`test_multi_role_grant_for_user_group` (Gruppen-Pfad) bereits ab, diese
|
|
Faehigkeit existierte fuer einzelne Benutzer ohnehin nur als jetzt
|
|
entfallener Spezialfall derselben Logik.
|
|
- **`tests/test_tenants.py`** (vom Umsetzungsauftrag noch referenziert)
|
|
existiert weiterhin nicht als aktive Testdatei -- liegt seit Teil C in
|
|
`_to_delete/`. Keine Aktion noetig.
|
|
- Jede Aenderung einzeln gegen einen echten `pytest`-Lauf verifiziert
|
|
(siehe `FORTSETZUNG_Teil_C.md` Abschnitt 0 fuer venv-/Env-Var-Setup).
|
|
|
|
---
|
|
|
|
## 3) Risiken, die beim Weitermachen besonders im Blick bleiben muessen (D.7)
|
|
|
|
Die vollstaendige Tabelle steht im Umsetzungsauftrag; am wichtigsten fuer
|
|
die Reihenfolge:
|
|
|
|
- **Reihenfolge Schritt 3 vor Schritt 5 ist zwingend** -- sonst
|
|
Rechteverlust beim Abschalten der Direktvergabe.
|
|
- **Reihenfolge "Vorab-Report" vor Schritt 4** ist zwingend -- sonst
|
|
Zugriffsausfall durch Achse B fuer jeden Host ohne Gruppenfreigabe.
|
|
- Mehrdeutigkeit bei der Credential-Auswahl (mehrere Keys an einem Host)
|
|
muss VOR der Umstellung auf die neue Auflösung geprueft werden, nicht
|
|
erst danach durch Nutzerbeschwerden auffallen.
|
|
|
|
---
|
|
|
|
## 4) Technische Hinweise (siehe auch `FORTSETZUNG_Teil_C.md` Abschnitt 0)
|
|
|
|
- venv liegt unter `.venv/` im Projektordner, funktioniert. Vor jedem
|
|
`pytest`-Lauf `JUMPHOST_DATA_DIR`/`JUMPHOST_ENV`/`JUMPHOST_DEV_KEK`/
|
|
`JUMPHOST_DEV_SESSION_SECRET` exportieren (siehe dortiger Codeblock).
|
|
- Migrations-Dateinamen sind alphabetisch sortiert entscheidend
|
|
(`app/db.py::_apply_migrations`, `sorted(MIGRATIONS_DIR.glob("*.sql"))`)
|
|
-- naechste freie Nummer nach `0018_credential_group_grants.sql` ist
|
|
`0019`.
|
|
- Migrationen schreiben laut Projektkonvention KEINE eigenen
|
|
`audit_log`-Eintraege (Hash-Chain wird ausschliesslich aus Python-Code
|
|
gepflegt, `write_audit_event()`) -- bei Bedarf einen Audit-Eintrag aus
|
|
einer begleitenden Codeaenderung schreiben, nicht aus dem SQL-Skript
|
|
selbst.
|
|
- Alle Datei-Edits weiterhin ueber `device_bash`-Python-Skripte mit
|
|
`content.count(old) == 1`-Assert-dann-Replace-Muster, danach
|
|
`py_compile`/`node --check` UND einen echten `pytest`-Lauf zur
|
|
Verifikation (jetzt moeglich, siehe Teil-C-Dokument Abschnitt 0) --
|
|
keine Aenderung mehr nur "statisch" verifizieren, wenn ein echter Test-
|
|
Lauf verfuegbar ist.
|