# 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 --after ` 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 --out ` 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 `, 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 ` 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.