42 KiB
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:
clipboard-> entfernen. Clipboard bleibt ausschliesslich ueberhosts.clipboard_enabled(host-global) steuerbar, keine Pro-Nutzer- Ebene.Jumphost_Konzept.md4.6 wurde entsprechend korrigiert (warb vorher faelschlich mit rollenbasierter Steuerung).session_recording_view-> entfernen. Sitzungs-Playback bleibt dauerhaft ausschliesslichrequire_global_adminvorbehalten.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 inuser_hostgroup_roles/group_hostgroup_rolesfuer die drei Rollen, dann dieroles-Zeilen selbst). Unabhaengig gegen einen synthetischen Voll-Migrationslauf (0001-0015) getestet: keine FK- Verletzungen,rolesenthaelt danach nur nochssh_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) -- jetztlet ROLE_NAMES = [], wird beim ersten Aufruf vonloadRolesTab()perGET /admin/roles/namesgefuellt (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.pyfilterte schon vor dieser Session korrekt nachh.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 zentralenrbac.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_rolezusaetzlich_assert_user_group_in_scope(conn, payload.user_group_id)-- analog zu den bereits vorhandenen Pruefungen ingrant_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.pybekam eine neue Funktionuser_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.pybaut 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.pyist 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 (sieheFORTSETZUNG_Teil_C.mdAbschnitt 3). Keine Regression.tests/test_rbac.pyund die catalog-bezogenen Tests intests/test_phase9.pylaufen 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:
app/db/migrations/0016_ssh_password_credential_objects.sql(D.4 Punkt 4, Vorbedingung fuer Achse B): bautssh_password_credentialsvon einer reinenhost_id-Zuordnungstabelle zu einem eigenstaendigen Objekt mit eigener ID um, analog zu Migration 0012 (rdp_credentials/host_rdp_credential_map). Die alte Tabelle wird zussh_password_credentials_legacyumbenannt und dauerhaft aufbewahrt (Projektkonvention, nicht droppen), die neuessh_password_credentials(id, label, username, password_enc, created_at, rotated_at) plushost_ssh_password_credential_map(host_id PK, ssh_password_credential_id) werden befuellt, Korrelation Alt->Neu ueberpassword_encals natuerlichen Unique-Schluessel (jede Verschluesselung nutzt einen frischen Nonce). Begleitende Code-Anpassungen:app/admin/routes.py(set_ssh_password_credentials/delete_ssh_password_credentialssowie 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).app/db/migrations/0017_personal_groups.sql(D.4 Punkte 1-2): fuegtuser_groups.is_personal(INTEGER NOT NULL DEFAULT 0) hinzu, dazu einenBEFORE INSERT ON user_group_members-Trigger (enforce_personal_group_single_member), der fuer persoenliche Gruppen ab dem zweiten Mitglied mitRAISE(ABORT, ...)hart abbricht (D.7: "Rechteausweitung ueber persoenliche Gruppen" -- serverseitig erzwungen, nicht nur Konvention). Anschliessend legt die Migration in einemBEGIN IMMEDIATE ... COMMIT-Block fuer jeden aktiven User mit mindestens eineruser_hostgroup_roles-Zeile eine persoenliche Gruppe an ('Persoenlich: ' || username || ' (#' || id || ')'), macht ihn zum einzigen Mitglied und spiegelt alle seine Direktvergaben 1:1 nachgroup_hostgroup_roles(inkl.granted_by/granted_at/expires_atverbatim, keine Neuvergabe). Bewusst OHNEPRAGMA 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 zweitesINSERT INTO user_group_membersfuer dieselbe persoenliche Gruppe.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 aususer_group_id+ Credential-ID,granted_by/granted_at/expires_at, Indizes auf beiden FK-Spalten,ON DELETE CASCADEaufuser_group_id). Befuellt rechteneutral perINSERT OR IGNORE ... SELECT DISTINCT: jede Gruppe, die auf der Hostgruppe eines Hosts mit zugeordnetem Credential die passende Connect-Rolle (ssh_connectbzw.rdp_connect) ingroup_hostgroup_roleshaelt, 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.pyund die Proxies lesen diese Tabellen noch nicht, das ist Schritt 4. Mit einem synthetischen Mehrgruppen-Szenario verifiziert (Team-Gruppe "devs" +ssh_connectauf 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 OHNErdp_connect-Vergabe) -- Ergebnis: keine FK-Verletzungen, SSH-Key- und SSH-Passwort-Freigaben korrekt fuer beide Gruppen erzeugt,group_rdp_credential_grantskorrekt leer geblieben.- Echter
pytest-Lauf gegen die volle Kette 0001-0018 (aufgeteilt in vier serielle Gruppen ueber getrenntedevice_bash-Aufrufe mit je eigenemJUMPHOST_DATA_DIR, wegen der ~120s-Aufrufgrenze; ein einzelnerpytest -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 inFORTSETZUNG_Teil_C.mdAbschnitt 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-- ruftPOST /admin/roles/grantauf 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 inuser_hostgroup_roles; diese Tabelle heisst seit Migration 0019user_hostgroup_roles_legacy, dasINSERTscheitert daher jetzt mitsqlite3.OperationalError: no such table: user_hostgroup_rolesstatt (wie unter Schritt 4) still zu laufen. War bei den 12 Ursache-A-Fehlschlaegen aus Abschnitt 1b NICHT gelistet, weil die Testaussage (403 bei fehlendemfile_transfer) zufaellig unabhaengig davon war, ob die separat geseedetessh_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.pys
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)
Neue Spalte/Kennzeichen "ist persoenliche Gruppe" aufFERTIG --user_groupsapp/db/migrations/0017_personal_groups.sql(user_groups.is_personal), inkl. serverseitigem Trigger gegen ein zweites Mitglied. Siehe Abschnitt 1a.Fuer jeden User mit >=1 Zeile inFERTIG -- Teil derselben Migration 0017. Siehe Abschnitt 1a.user_hostgroup_roles: persoenliche Gruppe anlegen ...Drei neue Achse-B-TabellenFERTIG --app/db/migrations/0018_credential_group_grants.sql(group_ssh_key_grants,group_rdp_credential_grants,group_ssh_password_credential_grants). Siehe Abschnitt 1a.Vorher noetig:FERTIG --ssh_password_credentials... umbauenapp/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 inapp/admin/routes.pyundapp/ssh_proxy/proxy.py. Siehe Abschnitt 1a.Achse B rechteneutral vorbefuellenFERTIG -- Teil von Migration 0018 (INSERT OR IGNORE ... SELECT DISTINCT, gegen ein Mehrgruppen-Szenario verifiziert). Siehe Abschnitt 1a.Verpflichtend (D.7): Vorher/Nachher-Diff der effektiven Rechte je BenutzerFERTIG -- neues, dauerhaftes Werkzeugscripts/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 ausapp.models.schemas.ROLE_NAMEdie Menge der Hostgruppen-IDs ueberapp.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 (einegroup_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
FERTIG.app/rbac.py::user_has_role()auf einen Zweig kuerzenapp/rbac.pykomplett neu geschrieben:user_has_role(),user_has_role_for_host()unduser_host_group_ids_with_any_role()lesen nur nochgroup_hostgroup_roles-- deruser_hostgroup_roles-Teil des fruehrerenUNIONist vollstaendig entfallen. Sicher, weil Migration 0017 (Schritt 3) jede vormals direkte Vergabe bereits 1:1 in eine persoenliche Gruppe gespiegelt hat.Zwei neue Funktionen inFERTIG:app/rbac.pyuser_can_use_credential(conn, *, user_id, kind, credential_id)("darf Benutzer X Credential Y nutzen") undresolve_credential_for_user_on_host(conn, *, user_id, host_id, kind)("welches Credential gilt fuer Benutzer X auf Host Z"), beide generisch ueberkind: 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 wirftresolve_credential_for_user_on_host()eine neueAmbiguousCredentialError(mithost_id,kind,credential_ids) statt still auszuwaehlen. Kann strukturell nur beikind="ssh_key"auftreten (n:m-Zuordnung ueberhost_ssh_key_map) -- RDP- und SSH-Passwort-Map haben laut Schema PK aufhost_id, also hoechstens einen Treffer.FERTIG.app/ssh_proxy/proxy.py... auf die neue Auflösung umstellenload_ssh_key_credential_for_host()undload_ssh_password_credential_for_host()(sowie die davon abgeleiteteload_private_key_for_host()) nehmen jetzt ein PFLICHT-Keyword-Argumentuser_idund loesen ueberresolve_credential_for_user_on_host()auf, statt blind per Host-ID zu selektieren.connect_to_host()nimmt ebenfallsuser_identgegen und reicht es durch; die drei Aufrufer (app/ssh_proxy/terminal_ws.py,app/ssh_proxy/sftp.pyx2) uebergebenuser_id=user.id.AmbiguousCredentialErrorist inSSH_SETUP_ERRORSaufgenommen (vonterminal_ws.py/sftp.pybereits gemeinsam behandelt) und hat eine eigene Klartextmeldung indescribe_connection_error().FERTIG. Die vormals direkteapp/rdp_proxy/ws_tunnel.py... auf die neue Auflösung umstellenSELECT ... FROM host_rdp_credential_map JOIN rdp_credentials ... WHERE host_id = ?-Abfrage ist durchresolve_credential_for_user_on_host(..., kind="rdp_credential")plus eine anschliessende ID-Abfrage ersetzt.AmbiguousCredentialErrorwird defensiv abgefangen (strukturell nicht erreichbar, siehe oben) und wie der "kein Credential"-Fall ueber_reject()sauber beantwortet, statt unbehandelt durchzuschlagen.app/catalog/routes.pybrauchte keine weitere Aenderung -- war seit Schritt 1 (S13) bereits vollstaendig aufuser_host_group_ids_with_any_role()umgestellt und profitiert von der Zweig-Kuerzung automatisch. Die D.5-Empfehlung,can_connectzusaetzlich 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 Werkzeugscripts/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 jedessh_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-KeyFERTIG -- 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
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 aufPOST /admin/roles/grant+/revokeauf HTTP 410/admin/group-roles/grant|revoke. Die Auth-Dependency bleibt aktiv (kein anonymer Zugriff auf den Endpunktnamen).FERTIG. Liest jetztGET /admin/roleszur abgeleiteten Sicht umbauengroup_hostgroup_roles JOIN user_groups JOIN user_group_members JOIN users (deleted_at IS NULL) JOIN host_groups JOIN roles, gefiltert auf nicht abgelaufeneexpires_at. Neue Feldervia_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 NULLfiltert Karteileichen aus (S9).S4 loesenFERTIG, aber NUR fuermap_ssh_key_to_hostundassign_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=...), wennnot admin.is_admin and not admin.is_token(neues FeldCurrentUser.is_token, gesetzt inapp/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_credentialsbekommen 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 keinencredential_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_credentialintests/test_teil_d_schritt5.py): Mitglied mitcredentials_managehaengt einen freigegebenen Key erfolgreich an, ein zweiter, existierender aber nicht freigegebener Key wird mit 403 abgelehnt.Neue Achse-B-EndpunkteFERTIG. 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 SchemasGroupCredentialGrantRequest/GroupCredentialRevokeRequest(app/models/schemas.py) -- Struktur bewusst analog zuGroupRoleGrantRequest. Dazu neuGET /admin/ssh-password-credentials(globale Liste, analog zulist_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.FERTIG.POST /admin/user-groups/{id}/membersS10add_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 alsgained_rights/lost_rightsin diedetails_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 entfernenFERTIG. Panel "Rolle(n) an Benutzer vergeben" (templates/admin.html, vormals ~Zeilen 498-529) vollstaendig entfernt;#role-grants-tableist 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 FunktionenpopulateCredentialSelect()(befuellt den Zugangsdatensatz-Dropdown je gewaehlter Art aus den bereits vorhandenen CachescachedSshKeys/cachedRdpCredentialsplus neucachedSshPasswordCredentials) undrefreshGroupCredentialGrants()(holt alle drei Arten parallel, mergt fuer eine gemeinsame Tabelle).ROLE_NAMESkam schon seit Schritt 1 ausGET /admin/roles/names-- hier keine Aenderung noetig. Verifiziert:node --checkauf beiden JS-Dateien,tests/test_csp_compliance.pygruen (CSP-Konformitaet der neuen Inline-freien Struktur),GET /adminliefert das neue Panel und NICHT mehr den alten Text "Rolle(n) an Benutzer vergeben".FERTIG -- Migration 0019 (user_hostgroup_rolesleeren/umbenennenALTER TABLE user_hostgroup_roles RENAME TO user_hostgroup_roles_legacy), NICHT gedroppt (Projektkonvention siehe0010:9-14,0012:12-17). Synthetisch gegen die volle Kette 0001-0019 verifiziert: keine FK-Verletzungen, alter Tabellenname verschwunden,user_hostgroup_roles_legacyvorhanden 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 mitis_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 mitgroup_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 identischerssh_connect-Rolle landen im selben Cluster, eine dritte ohne jede Rolle bleibt allein).restriktives Aufraeumen der Achse-B-VorbefuellungBEWUSST NICHT als automatischer Entzug umgesetzt -- als Report-Werkzeug FERTIG, siehe Begruendung unten.GET /admin/reports/unconfirmed-credential-grantslistet alle Achse-B-Freigaben mitgranted_by IS NULL-- exakt die Kennzeichnung, die Migration 0018 fuer ihre rechteneutrale Vorbefuellung verwendet (ein manueller Grant ueberPOST /admin/group-credentials/{kind}/grantsetzt immeradmin.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:
FERTIG. Alle drei Tests seeden jetzt uebertests/test_rbac.py... vollstaendig neu geschriebenuser_groups/user_group_members/group_hostgroup_rolesstattuser_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.pytest_admin_groups_tokens.pywar 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()(nurtest_phase9.py, fuer den S4-Fall) ersetzenPOST /admin/roles/grant.FERTIG. Beitests/test_pentest_security.py(2 Tests),tests/test_phase10.py(1 Test),tests/test_phase12.py(5 Tests)test_phase10.py/test_phase12.pyzusaetzlich zur Gruppen-Freigabe das neue Pflicht-Keyworduser_idan allenconnect_to_host()/load_private_key_for_host()-Aufrufen ergaenzt;test_phase12.pys_make_db()-Hilfsfunktion seedet jetzt immer einen Benutzer samt Gruppe und (beiwith_key=True) einegroup_ssh_key_grants-Freigabe -- genau das zuvor als "kein Notloesungs-user_id=1" zurueckgestellte echte Achse-B-Seeding aus Abschnitt 1b.FERTIG, aber NICHT durch einen Ersatztest fuer den Einzel-User-Pfad --tests/test_admin_crud.py(1 Test)test_multi_role_grant_for_individual_userwurde zutest_direct_multi_role_grant_for_individual_user_is_retired: bestaetigt, dassPOST /admin/roles/grantmitrole_namesjetzt 410 liefert UND folgenlos bleibt (GET /admin/roleszeigt keine Rolle). Kein Ersatz fuer die Mehrfachauswahl-Faehigkeit selbst noetig -- die deckttest_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 (sieheFORTSETZUNG_Teil_C.mdAbschnitt 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 jedempytest-LaufJUMPHOST_DATA_DIR/JUMPHOST_ENV/JUMPHOST_DEV_KEK/JUMPHOST_DEV_SESSION_SECRETexportieren (siehe dortiger Codeblock). - Migrations-Dateinamen sind alphabetisch sortiert entscheidend
(
app/db.py::_apply_migrations,sorted(MIGRATIONS_DIR.glob("*.sql"))) -- naechste freie Nummer nach0018_credential_group_grants.sqlist0019. - 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 mitcontent.count(old) == 1-Assert-dann-Replace-Muster, danachpy_compile/node --checkUND einen echtenpytest-Lauf zur Verifikation (jetzt moeglich, siehe Teil-C-Dokument Abschnitt 0) -- keine Aenderung mehr nur "statisch" verifizieren, wenn ein echter Test- Lauf verfuegbar ist.