18 KiB
Fortsetzungsdokument: Umsetzungsauftrag_Sonnet5.md — Teil C (Mandanten-Rueckbau)
Stand: 2026-09-02. Teil C ist vollstaendig abgeschlossen (inkl. echtem
pytest-Lauf, siehe Abschnitt 3). Update 2026-09-02: alle 7 in
Abschnitt 3 dokumentierten vorbestehenden Bugs sind jetzt behoben, voller
Regressionslauf 164/164 gruen -- der zuvor zurueckgestellte Livetest kann
jetzt als echter Abnahmetest stattfinden (siehe Abschnitt 5).
Repo: ssh_jumphost (FastAPI/SQLite SSH/RDP-Jumphost), erreichbar ueber die
device_bash-Bridge unter $HOME/mnt/ssh_jumphost.
Reihenfolge des Gesamtauftrags (verbindlich, siehe
Umsetzungsauftrag_Sonnet5.md): Teil A/B -> Teil E -> Teil C (abgeschlossen,
dieses Dokument) -> Teil D (naechster Schritt) -> Teil F, plus eine vom
Nutzer nachtraeglich hinzugefuegte Aufgabe #11 (SSH Copy-Paste +
Terminal-Farben).
Erledigt: Teil A, Teil B, Teil E, Teil C -- Teil A/B/E vollstaendig abgeschlossen in frueheren Sessions, hier nicht mehr relevant. Teil C siehe unten.
0) Wichtige Umgebungs-Erkenntnis fuer die naechste Session
Alle Sessions vor dieser hier haben ausschliesslich in einer Cloud-Sandbox
ohne Netzwerk/pip gearbeitet und konnten deshalb nie echten pytest
laufen lassen (nur py_compile + selbstgebaute Undefined-Name-/Tag-Balance-
Checker). Diese Session laeuft stattdessen ueber die device_bash-Bridge
direkt auf dem Nutzer-Rechner -- dort funktioniert Netzwerk/pip.
Das aendert die Verifikationsmoeglichkeiten grundlegend:
- Eine funktionierende venv liegt bereits unter
.venv/im Projektordner (Python 3.10, pip 22.0.2). Allerequirements.txt+requirements-dev.txt- Pakete sind installiert und mit.venv/bin/python3 -m pytestlauffaehig..venv/bin/piplaesst sich NICHT upgraden/loeschen (device_bash darf nicht loeschen, siehe Abschnitt 4 unten) -- das ist unschaedlich, die vorhandene pip-Version reicht zum Installieren. - Vor jedem
pytest-Lauf muessen diese Env-Vars gesetzt sein (sonst schlaegt JEDER Test mitPermissionError: /var/lib/jumphostfehl, weilapp/config.py::Settings.data_dirbeim ERSTEN Modul-Import aus der Umgebung gelesen wird und ohneJUMPHOST_DATA_DIRauf den Produktionspfad/var/lib/jumphostfaellt):export JUMPHOST_DATA_DIR=/tmp/jumphost-test export JUMPHOST_ENV=development export JUMPHOST_DEV_KEK=$(python3 -c "import secrets;print(secrets.token_hex(32))") export JUMPHOST_DEV_SESSION_SECRET=$(python3 -c "import secrets;print(secrets.token_hex(32))") .venv/bin/python3 -m pytest -q device_bash-Aufrufe laufen jeweils in einer frischen, isolierten Sandbox (bwrap, eigener Network-Namespace) -- Hintergrundprozesse pernohup ... & disownueberleben das Ende des Aufrufs NICHT (anders als ein normaler Shell-Hintergrundprozess auf einer echten Maschine). Lang laufende Kommandos (z.B.pip install) muessen daher als EIN Aufruf mittimeoutunterhalb des 180s-Caps laufen; bei Abbruch einfach denselben Befehl erneut ausfuehren (pip ist idempotent, laedt bereits installierte Pakete nicht neu herunter).rm -rfauf Dateien, die device_bash selbst angelegt hat (z.B. eine kaputte.venv), schlaegt mitOperation not permittedfehl (siehe Abschnitt 4) -- nicht wundern, das ist die bekannte Loesch-Restriktion, keine defekte venv.
1) Betreiberentscheidungen (Phase 0 von Teil C) -- weiterhin verbindlich
Diese drei Entscheidungen wurden vom Nutzer per Rueckfrage getroffen und duerfen von keiner Folge-Session stillschweigend geaendert werden:
tenant_admins-Behandlung -> "Zu is_admin=1 befoerdern" (jeder bestehendetenant_admins-Eintrag wird zu vollemis_admin=1befoerdert, NICHT demotet). Umgesetzt durchscripts/promote_tenant_admins.py(Phase 1, muss vor erstem Start mit neuem Code auf der echten Produktions-DB laufen, siehe Abschnitt 4 im alten Dokumentstand / weiterhin offen).users.home_tenant_id-> "Vollstaendiger Rebuild (Recommended)" (die Spalte wurde beim Tabellen-Rebuild vollstaendig entfernt). Umgesetzt in0014_drop_tenants.sql.- Bestehende API-Tokens -> "Bestehende Tokens weiterlaufen lassen" -- weicht bewusst von der Empfehlung des Umsetzungsauftrags selbst ab. Es fand KEIN Massenwiderruf statt. Diese Abweichung ist Absicht und darf nicht "korrigiert" werden.
2) Teil C: vollstaendiger Abschluss-Stand
Phase 1 (Operator-Skript) -- weiterhin offen fuer den Betreiber
scripts/promote_tenant_admins.py ist gebaut und verifiziert (py_compile),
aber noch nie gegen eine echte Produktions-DB gelaufen -- es gibt in
keiner bisherigen Session-Umgebung eine solche DB. Muss vom Betreiber vor
dem ersten Produktions-Deployment dieses Codestands einmalig ausgefuehrt
werden, idealerweise zuerst mit --dry-run.
Phase 2 (Code-Aenderungen) -- vollstaendig, in frueherer Session erledigt
Siehe Git-Historie / vorherige Version dieses Dokuments: app/tenancy.py
entfernt (nach _to_delete/ verschoben), app/auth/deps.py,
app/auth/routes.py, app/models/schemas.py, app/admin/routes.py,
app/admin/log_ws.py, templates/admin.html, static/js/admin.js,
app/db/migrations/0014_drop_tenants.sql, app/db.py::_apply_migrations()
(FK-Check nach jeder Migration) -- alles bereits fertiggestellt und jetzt
zusaetzlich durch den echten pytest-Lauf bestaetigt (siehe Abschnitt 3).
Phase 3 (Tests + README) -- in DIESER Session abgeschlossen
tests/test_phase9.py:test_sessions_list_requires_global_admin_not_tenant_adminzutest_sessions_list_requires_adminvereinfacht (normaler User statttenant_admins-Konstrukt),"tenant_id": 1aus dem rdp-credentials-Payload entfernt.tests/test_phase12.py: alle 7tenant_id-Spalten/-Werte aus denhost_groups/ssh_keys-INSERTs entfernt (auch in den_pre_0010_db- Migrationstests -- die Spalte ist dort nullable, das Entfernen aendert nichts am getesteten Verhalten von Migration 0010).tests/test_phase13.py:"tenant_id": 1aus rdp-credentials-Payload entfernt,tenant_id/tenant_nameaus der erwarteten SSH-Key-Spaltenliste entfernt,test_session_recording_entries_requires_global_adminvon einem Mandanten-Admin-Aufbau (POST /admin/tenants-- existiert nicht mehr) auf einen normalen Nicht-Admin-User umgebaut.README.md: alle im vorherigen Dokumentstand gelisteten Fundstellen bereinigt -- Phase-8-Abschnitt zu einer kurzen historischen Rueckschau umformuliert (inkl. Neu-Nummerierung der verbleibenden 6 Unterpunkte), Migrations-Cross-Referenzen von0006_tenants.sqlauf0014_drop_tenants.sqlumgestellt, Testzahlen/-beschreibungen antests/test_admin_crud.py(Nachfolger vontests/test_tenants.py) angepasst, Hinweis auf dauerhaft erhaltenetenant_*-Audit-Log-Eintraege ergaenzt, "Mandantenfaehigkeit" aus der offenen-Punkte-Aufzaehlung gestrichen.pytest.ini:norecursedirs = _to_delete .venv .gitergaenzt (sonst sammelt pytest die nach_to_delete/verschobenetest_tenants.pyweiter ein und importiert das ebenfalls verschobeneapp/tenancy.py-> Import- Error bei jedem Lauf).
Alle Aenderungen mit py_compile verifiziert; die Test-Datei-Aenderungen
zusaetzlich durch den echten Suite-Lauf (Abschnitt 3).
3) Echter pytest-Lauf: Ergebnis und Einordnung (AKTUALISIERT -- alle 7 Bugs behoben)
Update (Folgesession, auf ausdruecklichen Nutzerwunsch "livetest spaeter
machen zuerst den rest machen das das dann wirkliche abnahmetests sind"):
Alle 7 nachfolgend dokumentierten Root-Cause-Bugs hinter den urspruenglich
18 Fehlschlaegen sind jetzt behoben und einzeln per echtem pytest
verifiziert. Der abschliessende volle Regressionslauf
(.venv/bin/python3 -m pytest -q, frisches JUMPHOST_DATA_DIR +
Dev-Secrets wie in Abschnitt 0) liefert: 164 passed, 0 failed (6
harmlose, vorbestehende DeprecationWarnings in app/admin/routes.py:562
-- ausserhalb des Scopes dieser 7 Bugs, siehe Fussnote unten). Der urspruengliche
Lauf hatte 132 Tests gezaehlt (124 passed/18 failed); die Gesamtzahl ist auf
164 gestiegen, weil zwischenzeitlich in Teil F/Aufgabe #11 neue Testdateien
hinzugekommen sind (test_teil_f_schritt5.py u.a.), die alle bereits gruen
waren.
Damit ist die Grundlage fuer eine echte Abnahmetest-Session (Live-Browser/ SSH/RDP-Test) gelegt: der zuvor bekannte Bug-Rucksack existiert nicht mehr, der Live-Test in einer kuenftigen Session prueft ein tatsaechlich vollstaendig korrigiertes System und nicht eines mit bekannten offenen Defekten.
Die folgenden 7 Eintraege sind nur noch als Nachweis-Dokumentation (Root-Cause + Fix) erhalten, nicht mehr als offene Punkte:
tests/test_phase15.py(7 Tests, urspruenglich) -- Signatur-Drift zwischen Testcode und Implementierung:Request()ohnescope,active_sessions.register()ohneuser_id,fake_connect_to_host()ohneuser_id-Keyword,UploadFile("name", bytes)mit veralteter Zwei-Positional-Argumente-Signatur (echte Starlette-Signatur inzwischenUploadFile(file, *, filename=...)),start_sftp_client()-Fakes ohne passendenasync/await-Vertrag und fehlendemexit(), sowieasyncssh.Error(reason=...)ohne das inzwischen pflicht-positionalecode-Argument. Fix: ausschliesslich intests/test_phase15.py-- alle Fake-Objekte/Aufrufe an die echten, aktuellen Signaturen angepasst (kein Produktivcode veraendert, reiner Test-Drift). Verifiziert: 11/11 gruen.tests/test_phase12.py(4 Tests) +tests/test_phase10.py(1 Test) --build_rdp_params()(app/rdp_proxy/guacd_client.py:413) verlangt inzwischensession_idals Pflicht-Keyword-Argument, die Tests riefen ohne dieses Argument auf. Fix: in beiden Testdateien an allen betroffenen Aufrufstellensession_id=1(Testwert, fachlich irrelevant fuer die geprueften Assertions) ergaenzt. Verifiziert: 26/26 gruen.tests/test_session_reaper.py(2 Tests) -- echter, reproduzierbarer Bug:app/security/session_reaper.py::reap_orphaned_sessions()fuehrte einUPDATE sessions ...aus (oeffnet bei Test-Verbindungen ohneisolation_level=Noneimplizit eine Transaktion), rief danachwrite_audit_event()auf, das seinerseitsBEGIN IMMEDIATEversucht -- doppeltes BEGIN,sqlite3.OperationalError. Fix: inapp/security/session_reaper.pyein explizitesawait conn.commit()direkt nach demUPDATEergaenzt (schliesst die implizite Transaktion, bevorwrite_audit_event()seine eigene oeffnet -- funktioniert sowohl mit Produktions-Autocommit-Verbindungen als auch mit dem Test-:memory:-Verbindungsstil). Verifiziert: 3/3 gruen.tests/test_phase13.py::test_session_recording_entries_ssh_playback_and_audit--SessionRecorder(app/recordings/recorder.py) hat nur nochasync def aclose(), der Test rief synchron.close()(existiert nicht). Fix: Testaufruf aufawait recorder.aclose()umgestellt (reiner Test-Drift, kein Produktivcode veraendert). Verifiziert: gruen.tests/test_phase13.py::test_ssh_password_credentials_set_and_delete-- Test-Bug, kein echtes Leck:assert "password" not in <json>schlug fehl, weil bereits die Response-FELDNAMEN (ssh_password_credentials_set/_username) die Zeichenkette "password" enthalten -- Substring-Treffer auf den Feldnamen, nicht auf einen Passwort-Klartextwert. Fix: Assertion auf den tatsaechlich sicherheitsrelevanten Fall praezisiert: geprueft wird jetzt, dass der konkrete im Test gesetzte Passwort-WERT ("sehr-geheimes-passwort") nicht im JSON auftaucht. Verifiziert: 11/11 gruen (ganze Datei).tests/test_pentest_security.py::test_password_change_invalidates_old_session_everywhere-- sicherheitsrelevant, per empirischer Debug-Sitzung (temporaeres, danach geloeschtes Debug-Testskript) untersucht. Ergebnis: die Sicherheitslogik selbst (session_version-Vergleich inapp/auth/deps.py::get_current_user) war die ganze Zeit KORREKT -- der alte Cookie wurde bei isolierter Pruefung zuverlaessig mit 401 abgelehnt. Der Fehlschlag war ein reines Test-Harness-Artefakt: dieget_current_user()-Dependency schreibt bei jeder Anfrage bereits ein eigenes Refresh-Set-Cookie (gleitender Idle-Timeout), undchange_password()/logout()/logout_everywhere()haengten danach noch ein ZWEITES, eigenes Set-Cookie an dieselbe Antwort -- zweiSet-Cookie: jh_session=...-Header in einer Antwort. Der geteilte httpx-Test-Client verwechselte beim naeherungsweisen Ueberschreiben seines Cookie-Jars (client.cookies.set(...)auf einem Jar, das durch die doppelten Set-Cookie-Header bereits einen widerspruechlichen Zustand hatte) den alten mit dem neuen Cookie-Wert -- ein von httpx selbst dokumentiertes Ambiguitaetsproblem beim Ueberschreiben/per-Request-Setzen von Cookies in einem bereits befuellten Jar. Fix (zwei Teile): (a) Anwendungsseitig inapp/auth/routes.py-- inchange_password(),logout()undlogout_everywhere()wird das von der Dependency bereits gesetzte Set-Cookie explizit entfernt (del response.headers["set-cookie"]), bevor die jeweilige Routenfunktion ihr eigenes, autoritatives Set-Cookie schreibt -- Verteidigung in der Tiefe, sauberere HTTP-Antworten unabhaengig vom Test. (b) Testseitig intests/test_pentest_security.py-- der alte Cookie wird jetzt gegen einen komplett FRISCHENhttpx.AsyncClient(leeres Cookie-Jar) geprueft statt gegen den geteilten, bereits "verschmutzten" Test-Client. Verifiziert: 17/17 gruen, keine httpx-Warnungen mehr.tests/test_admin_crud.py::test_user_update_deactivate_and_delete_semantics-- echter Bug:app/admin/routes.py::delete_user()pruefteSELECT 1 FROM audit_log WHERE user_id = ? LIMIT 1mit der ID des ZIEL-Nutzers, aberuser_updated/user_deactivated-Events werden mituser_id=<handelnder Admin>geschrieben -- der betroffene Nutzer steht stattdessen unterdetails_json.target_user_id(dieselbe Konvention wie beiuser_added_to_group/removed_from_group,api_token_issued,session_terminated_by_admin, per Grep bestaetigt). Der Audit-Historie-Check fand deshalb nie etwas fuer den Zielnutzer und loeschte hart, obwohl ein anonymisierendes Soft-Delete erwartet war -- eine echte, sicherheitsrelevante Datenverlust-Regression (Accounts mit Aenderungshistorie waeren faelschlich hart geloescht statt anonymisiert worden). Fix: dieSELECT-Bedingung inapp/admin/routes.py::delete_user()umOR json_extract(details_json, '$.target_user_id') = ?erweitert (SQLite-JSON1-Verfuegbarkeit vorab eigenstaendig verifiziert). Verifiziert: 9/9 gruen (ganze Datei).
Fussnote (nicht Teil der 7 Bugs, nicht behoben): der volle
Regressionslauf zeigt 6 DeprecationWarnings in app/admin/routes.py:562
(payload.ssh_username, payload.rdp_username, payload.rdp_domain --
vermutlich ein veraltetes Pydantic-Zugriffsmuster). Harmlos (keine
Testfehlschlaege), ausserhalb des vom Nutzer beauftragten Scopes dieser
Session, aber als Hinweis fuer eine kuenftige Aufraeum-Session vermerkt.
4) Wichtiger Hinweis: zwei Dateien liegen weiterhin in _to_delete/
Unveraendert gegenueber dem vorherigen Dokumentstand: device_bash darf
standardmaessig keine Dateien loeschen. In _to_delete/ liegen
tenancy.py (ehemals app/tenancy.py) und test_tenants.py (ehemals
tests/test_tenants.py), technisch noch vorhanden, aber aus dem aktiven
Code-/Test-Pfad entfernt (pytest sammelt den Ordner seit dem
pytest.ini-Update in dieser Session auch nicht mehr ein). Dazu aeltere,
nicht zusammenhaengende Reste (appsrc.tgz, jh.tgz,
tmp_stage_1788191215). Der Nutzer kann diesen Ordner jederzeit selbst
loeschen, oder eine kuenftige Session kann device_request_delete_permission
versuchen.
5) Empfohlene naechste Schritte
- Livetest/Abnahmetest (Browser/SSH/RDP, echtes Deployment) fuer Teil F.6 und Aufgabe #11 -- war vom Nutzer ausdruecklich zurueckgestellt ("livetest spaeter machen zuerst den rest machen das das dann wirkliche abnahmetests sind"), bis die in Abschnitt 3 dokumentierten 7 Bugs behoben sind. Das ist jetzt der Fall (alle 7 behoben, voller Regressionslauf 164/164 gruen) -- der Livetest kann in der naechsten Session als echter, aussagekraeftiger Abnahmetest durchgefuehrt werden.
- Teil D -- Berechtigungen ausschliesslich ueber Benutzergruppen
(naechster Abschnitt in
Umsetzungsauftrag_Sonnet5.md, direkt nach Teil C) -- noch nicht gelesen/begonnen. - Weiterhin offen, unabhaengig von der Code-Reihenfolge: Phase-1-Skript
(
scripts/promote_tenant_admins.py) auf der echten Produktions-DB ausfuehren, sobald ein Deployment-Fenster dafuer ansteht. - Kleinere, nicht dringende Aufraeumarbeit (siehe Fussnote Abschnitt 3):
die 6
DeprecationWarnings inapp/admin/routes.py:562bei Gelegenheit beheben.
6) Technische Hinweise fuer die naechste Session
- Repo ueber
mcp__remote-devices__device_basherreichbar, gemountet unter$HOME/mnt/ssh_jumphost. Diese Bridge laeuft direkt auf dem Nutzer-Rechner (nicht die evtl. veraltete Cloud-Container-Kopie unter/mnt/user-data/uploads/ssh_jumphost/, die nur zum schnellen Lesen taugt). - Siehe Abschnitt 0 fuer venv/Env-Var-Setup und die Eigenheiten der device_bash-Sandbox (frische Namespaces pro Aufruf, 180s-Cap, Loesch-Restriktion).
- Alle Datei-Edits weiterhin ueber
device_bash-Python-Skripte mitcontent.count(old) == 1-Assert-dann-Replace-Muster durchfuehren (nie blindessed), danach IMMER pergrepgegenpruefen, dass alles persistiert wurde. - Fuer SQL-Migrationsaenderungen weiterhin einen unabhaengigen
sqlite3-Test-Harness bauen (wie in einer frueheren Session unter/tmp/migtest/run_test.pyim Cloud-Container) -- jetzt aber alternativ auch direkt mit echtempytestgegen die volle FastAPI-App verifizierbar, siehe Abschnitt 0/3.