umbau 1.0
This commit is contained in:
742
FORTSETZUNG_Teil_F.md
Normal file
742
FORTSETZUNG_Teil_F.md
Normal file
@ -0,0 +1,742 @@
|
||||
# Fortsetzungsdokument: Umsetzungsauftrag_Sonnet5.md — Teil F (Mehrere Sitzungen je Benutzer mit Seitenleiste)
|
||||
|
||||
Stand: 2026-09-01, Stufen F1+F2+F3+F4+F5 abgeschlossen und verifiziert (F.6-Abnahme
|
||||
noch ausstehend, siehe Abschnitt 6). 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 (fertig) ->
|
||||
Teil D (fertig, siehe `FORTSETZUNG_Teil_D.md`) -> **Teil F (dieses Dokument,
|
||||
laufend)** -> Aufgabe #11 (SSH Copy-Paste + Terminal-Farben, vom Nutzer
|
||||
nachtraeglich hinzugefuegt).
|
||||
|
||||
**Erledigt: Teil A, Teil B, Teil E, Teil C, Teil D** -- alle vollstaendig
|
||||
abgeschlossen in frueheren Sessions, hier nicht mehr relevant. Teil F siehe
|
||||
unten.
|
||||
|
||||
---
|
||||
|
||||
## 0) Betreiberentscheidungen fuer Teil F (Nutzerantworten dieser Session)
|
||||
|
||||
Der Umsetzungsauftrag verlangt vor Beginn von Teil F Antworten auf drei der
|
||||
acht offenen Betreiberentscheidungen (Abschnitt "Offene Entscheidungen fuer
|
||||
den Betreiber", Punkte 6-8):
|
||||
|
||||
1. **#6 Obergrenze gleichzeitiger Sitzungen (Teil E.4)** -- bereits durch
|
||||
eine fruehere Session beantwortet und implementiert: `app/config.py`
|
||||
`max_sessions_per_user=6`, `max_sessions_global=100` (Env-Overrides
|
||||
`JUMPHOST_MAX_SESSIONS_PER_USER`/`_GLOBAL`), durchgesetzt in
|
||||
`app/ssh_proxy/terminal_ws.py` und `app/rdp_proxy/ws_tunnel.py` VOR jedem
|
||||
Ressourcenverbrauch. Keine neue Entscheidung noetig.
|
||||
2. **#7 Verhalten von Hintergrundsitzungen (F.3.9)** -- Nutzerentscheidung
|
||||
dieser Session: **voll verbunden bleiben** (Spec-Empfehlung fuer Stufe 1).
|
||||
Kein Halte-/Pausenzustand, keine neue Zustandsmaschine. Die bereits
|
||||
vorhandene niedrige Obergrenze (Punkt 1) begrenzt den Ressourcenverbrauch.
|
||||
3. **#8 Wiederanschluss nach Seiten-Reload (F.3.7)** -- Nutzerentscheidung
|
||||
dieser Session: **kein Wiederanschluss, klar beschriftet**
|
||||
(Spec-Empfehlung fuer Stufe 1). Ein Reload beendet die Sitzung wie heute;
|
||||
die Oberflaeche muss das ehrlich kommunizieren ("Sitzung wurde beendet,
|
||||
erneut verbinden"), statt es zu verschweigen. Serverseitiges Halten von
|
||||
SSH-Verbindung + Ausgabepuffer ueber eine Trennung hinweg ist explizit
|
||||
NICHT Teil dieser Umsetzung (F.3.7, Umsetzungsauftrag: "kein
|
||||
Nebenbei-Feature").
|
||||
|
||||
## 0a) Ist-Zustand-Bestandsaufnahme (vor F1)
|
||||
|
||||
Bereits vorhanden und fuer Teil F wiederverwendbar (nichts davon musste neu
|
||||
gebaut werden):
|
||||
|
||||
* **E7 Session-Reaper** (`app/security/session_reaper.py`, in
|
||||
`app/main.py::lifespan` vor dem ersten Request aufgerufen) -- keine
|
||||
Geistersitzungen nach Neustart.
|
||||
* **E9 Keepalive** (`static/js/session-keepalive.js`, in terminal.html/
|
||||
rdp.html eingebunden) -- haelt das Anmelde-Cookie waehrend einer offenen
|
||||
Sitzung per periodischem `GET /auth/me` am Leben.
|
||||
* **E10 Aktivitaets-basierter Idle-Timeout** (`app/ssh_proxy/terminal_ws.py`
|
||||
`IDLE_TIMEOUT_SECONDS`, `activity["t"]` wird sowohl bei Ein- als auch bei
|
||||
Ausgabe aktualisiert) -- ein `tail -f` im Hintergrund haelt die Sitzung am
|
||||
Leben.
|
||||
* **E.4 Obergrenzen + Registry** (`app/security/active_sessions.py`:
|
||||
`count_for_user()`, `count_total()`, prozesslokale In-Memory-Registry mit
|
||||
`task`/`user_id`/`watchers`) -- Grundlage fuer F.3.6 (eigene
|
||||
Sitzungs-API, Stufe F2).
|
||||
* **D4 RDP dynamische Aufloesung** (`resize-method: display-update` in
|
||||
`app/rdp_proxy/guacd_client.py::build_rdp_params()`, `client.sendSize()`
|
||||
bereits in `rdp.js` aufgerufen mit Debounce) -- F.3.3 kann direkt darauf
|
||||
aufbauen, keine Server-Aenderung noetig.
|
||||
* **GET /admin/sessions + Beenden-Logik** (`app/admin/routes.py`) als
|
||||
Referenzmuster fuer die in F2 zu bauende, auf `user_id=eigene`
|
||||
beschraenkte Variante im Katalog-Router.
|
||||
|
||||
## 1) Stufe F1 -- FERTIG (terminal.js/rdp.js zu instanzierbaren Bausteinen)
|
||||
|
||||
**Ziel (F.3.1):** aus zwei Dokument-Singletons (ein Skript pro Seite, feste
|
||||
Element-IDs) werden zwei Klassen mit genau einer Instanz pro Sitzung:
|
||||
`connect()`, `dispose()`, `setActive(bool)`, ein Statusrueckruf fuer eine
|
||||
kuenftige Seitenleiste. Verhalten danach unveraendert (reiner Umbau).
|
||||
|
||||
### Umgesetzt
|
||||
|
||||
* **`static/js/terminal.js`**: `class SshTerminalSession` -- baut ihr
|
||||
komplettes DOM (Toolbar, Terminal-Container, Dateitransfer-Panel) selbst
|
||||
per `innerHTML`-Template und `document.createElement`, referenziert intern
|
||||
ausschliesslich ueber Klassen (`.session-shell`, `.status`, `.ft-overlay`
|
||||
usw. -- keine IDs mehr fuer die interne Verdrahtung, mit Ausnahme von zwei
|
||||
dynamisch generierten, instanzeindeutigen IDs fuer die
|
||||
`<label for>`-Kopplung im Dateitransfer-Panel). Konstruktor baut nur auf;
|
||||
`connect()` oeffnet den WebSocket getrennt davon (damit eine kuenftige
|
||||
Seitenleiste eine Kachel erst anzeigen und danach verbinden kann).
|
||||
`dispose()` ist idempotent: schliesst WebSocket + xterm.js-Instanz,
|
||||
entfernt dokumentweite Resize-/Fullscreen-Listener und das eigene
|
||||
DOM-Element. `setActive(bool)` macht bei genau einer Instanz (heutiger
|
||||
Stand) nur ein Refit+Fokus -- die eigentliche Mehrfach-Instanz-Wirkung
|
||||
kommt erst in Stufe F4.
|
||||
* Tastatur-Sicherheit (F.3.2) ist fuer SSH bereits strukturell gegeben:
|
||||
xterm.js haengt seine Tastatur-Erfassung an sein EIGENES
|
||||
Containerelement, nicht an `document` -- das in F.3.2 beschriebene
|
||||
Risiko ("Tastendruck landet auf drei Servern") betrifft nur RDP.
|
||||
* **`static/js/rdp.js`**: `class RdpSession`, analog aufgebaut (Toolbar,
|
||||
Anzeigeflaeche, Fehler-Overlay als eigenes DOM-Fragment). Da
|
||||
guacamole-common-js' `Guacamole.Keyboard` nur an `document` gebunden
|
||||
werden kann (keine saubere Abmeldung/Umschaltung in der Bibliothek),
|
||||
filtert jede Instanz Tastatur-/Paste-Weiterleitung ueber `this._active`
|
||||
(gesetzt durch `setActive()`) -- das ist die Grundlage fuer F.3.2, aber
|
||||
noch NICHT die vollstaendige Loesung (fehlende Modifiertasten-Ruecksetzung
|
||||
beim Umschalten folgt in Stufe F4, wenn es ueberhaupt zwei gleichzeitige
|
||||
Instanzen geben kann). Bei genau einer Instanz (heutiger Stand:
|
||||
Stufe F1/F2/F3) ist das Verhalten exakt wie vorher. `dispose()` hat eine
|
||||
dokumentierte Einschraenkung: der zugrundeliegende `document`-Listener von
|
||||
`Guacamole.Keyboard` bleibt technisch bestehen (keine Bibliotheks-API zum
|
||||
Abmelden), sendet aber nach `dispose()` nichts mehr, da `_active=false`
|
||||
bleibt -- folgenlos bei einer Instanz, muss bei Stufe F4
|
||||
(Kachel-Schliessen mit mehreren offenen Sitzungen) erneut betrachtet
|
||||
werden.
|
||||
* **`templates/terminal.html`, `templates/rdp.html`**: auf duennen Rahmen
|
||||
reduziert -- nur noch `<div id="session-container" data-host-id="{{ host_id }}">`
|
||||
plus die Script-Includes. Die Seite baut kein Sitzungs-DOM mehr selbst;
|
||||
`terminal.js`/`rdp.js` erkennen `#session-container` beim Laden (Bootstrap-
|
||||
Block am Dateiende), erzeugen GENAU EINE Instanz, haengen deren
|
||||
`.element` ein und rufen `connect()` + `setActive(true)` auf. Eine
|
||||
kuenftige Arbeitsflaeche `/workspace` (Stufe F3) hat kein Element mit
|
||||
dieser ID -- der Bootstrap-Block bleibt dort wirkungslos und baut ihre
|
||||
Instanzen stattdessen selbst; **keine zweite Implementierung** (F.2,
|
||||
letzter Absatz, ausdruecklich eingehalten).
|
||||
* **`static/css/app.css`**: `#terminal, #rdp-display` (dokumentweit
|
||||
eindeutige IDs, passten zum alten Ein-Instanz-Modell) zu
|
||||
`.session-terminal, .session-rdp-display` (Klassen, da das DOM jetzt pro
|
||||
Instanz dynamisch gebaut wird und mit mehreren gleichzeitigen Instanzen
|
||||
in Stufe F4 IDs nicht mehr eindeutig waeren). Alle anderen betroffenen
|
||||
Selektoren (`.session-shell`, `.session-toolbar`, `.ft-overlay`,
|
||||
`.rdp-error-overlay` usw.) waren bereits klassenbasiert, keine Aenderung
|
||||
noetig.
|
||||
* **`tests/test_csp_compliance.py`**: Docstring-Korrektur bei
|
||||
`test_session_pages_expose_host_id_via_data_attribute` (data-host-id sitzt
|
||||
jetzt auf `#session-container`, nicht mehr auf `#session-shell` -- die
|
||||
eigentliche Assertion pruefte nur den rohen `data-host-id="..."`-String
|
||||
und war davon nicht betroffen, blieb aber inhaltlich unpraezise).
|
||||
|
||||
### Bewusst NICHT in Stufe F1 (folgt in spaeteren Stufen)
|
||||
|
||||
* F.3.2 (vollstaendige Fokus-/Tastaturtrennung bei RDP inkl.
|
||||
Modifiertasten-Reset) -- Grundlage gelegt (`_active`-Gate), vollstaendig
|
||||
erst relevant/loesbar mit echten Mehrfach-Instanzen in F4.
|
||||
* F.3.3 (Refit erst beim Sichtbarwerden einer ausgeblendeten Kachel) --
|
||||
betrifft nur die kuenftige Seitenleiste, in F1 gibt es noch keine
|
||||
ausgeblendeten Instanzen.
|
||||
* F.3.6 (eigene Sitzungs-API im Katalog-Router) -- Stufe F2.
|
||||
* `/workspace` selbst -- Stufe F3.
|
||||
|
||||
### Verifikation
|
||||
|
||||
* `node --check static/js/terminal.js` und `static/js/rdp.js` -- beide
|
||||
syntaktisch fehlerfrei.
|
||||
* Vollstaendiger Grep nach allen vormaligen IDs (`session-shell`,
|
||||
`fullscreen-btn`, `exit-btn`, `filetransfer-btn`, `ctrlaltdel-btn`,
|
||||
`rdp-error*`, `ft-overlay`, `ft-status`, `ft-list`) VOR der Aenderung
|
||||
ergab: nur `terminal.js`/`rdp.js`/`terminal.html`/`rdp.html`/`app.css`
|
||||
referenzieren sie direkt; `templates/admin.html` nutzt dieselbe
|
||||
`.ft-overlay`-CSS-Klasse fuer zwei eigene, unabhaengige Overlays
|
||||
(`#rec-overlay`, `#watch-overlay` -- Sitzungswiedergabe/Live-Mitschau im
|
||||
Adminbereich) mit eigenen IDs, keine Kopplung an terminal.js/rdp.js-Intern
|
||||
-- keine weiteren Anpassungen noetig.
|
||||
* Echter `pytest`-Lauf (4-Gruppen-Split wie in Teil C/D etabliert, ueber die
|
||||
device_bash-Bridge mit funktionierendem venv/pip):
|
||||
`test_csp_compliance.py` einzeln: **10/10 bestanden**. Voller
|
||||
Regressionslauf ueber alle 19 Testdateien:
|
||||
|
||||
| Gruppe | Dateien | Ergebnis |
|
||||
|---|---|---|
|
||||
| 1 | admin_crud, admin_groups_tokens, audit_chain, auth_flow, csp_compliance | 39 passed, 1 failed |
|
||||
| 2 | passwords_totp, pentest_security, phase9, phase10, phase11 | 44 passed, 2 failed |
|
||||
| 3 | phase12, phase13, phase14, phase15 | 34 passed, 13 failed |
|
||||
| 4a | rbac, teil_d_schritt5, teil_d_schritt6 | 11 passed, 0 failed |
|
||||
| 4b | session_reaper | 1 passed, 2 failed |
|
||||
| 4c | session_recorder | 5 passed, 0 failed |
|
||||
| **Summe** | | **134 passed, 18 failed, 152 total** |
|
||||
|
||||
**Exakt dieselben Zahlen wie am Ende von Teil D** (siehe
|
||||
`FORTSETZUNG_Teil_D.md` Abschnitt 1d) -- die 18 Fehlschlaege sind
|
||||
einzeln stichprobenartig gegengeprueft (u.a.
|
||||
`test_user_update_deactivate_and_delete_semantics` [Bug #7],
|
||||
`test_password_change_invalidates_old_session_everywhere` [Bug #6,
|
||||
sicherheitsrelevant, bewusst unangetastet], `test_build_rdp_params_...`
|
||||
[Bug #2, `session_id`-Keyword], `test_reap_closes_only_open_sessions`
|
||||
[„cannot start a transaction within a transaction"]) und stammen
|
||||
unveraendert aus dem bereits dokumentierten 18-Bug-Backlog aus Teil C.
|
||||
**Stufe F1 hat null neue Testfehlschlaege verursacht** -- reiner,
|
||||
verhaltensneutraler Umbau bestaetigt.
|
||||
* Kein manueller Browser-Test in dieser Session moeglich (keine laufende
|
||||
Instanz mit echtem SSH-/RDP-Ziel in der Sandbox) -- die Aequivalenz
|
||||
Alt-/Neu-Verhalten stuetzt sich auf: (a) den unveraenderten Testlauf oben
|
||||
(die Test-Suite hat keine dedizierten JS-Tests fuer
|
||||
terminal.js/rdp.js -- diese Luecke bestand bereits vorher), (b) die
|
||||
Codegegenueberstellung Zeile fuer Zeile (jede Funktion des alten IIFE hat
|
||||
eine 1:1-Entsprechung als Methode in der neuen Klasse, keine Logik
|
||||
geaendert, nur Referenzierung von globalen IDs auf `this.xxx`
|
||||
umgestellt). Empfehlung fuer eine kuenftige Session mit Zugriff auf einen
|
||||
echten SSH-/RDP-Testhost: manueller Rauchtest von `/terminal/{id}` und
|
||||
`/rdp/{id}` vor dem Weiterbau zu Stufe F3/F4.
|
||||
|
||||
---
|
||||
|
||||
## 2) Stufe F2 -- FERTIG (eigene Sitzungs-API im Katalog-Router)
|
||||
|
||||
**Ziel (F.3.6):** eine Liste der eigenen offenen Sitzungen und ein Endpunkt
|
||||
zum Beenden einer eigenen Sitzung -- zwingend auf `user_id = <eigene>`
|
||||
beschraenkt, NICHT ueber `GET /admin/sessions` (zeigt fremde Sitzungen,
|
||||
Benutzernamen, Aufzeichnungspfade).
|
||||
|
||||
### Umgesetzt
|
||||
|
||||
* **Migration `0020_session_user_ended_index.sql`**: zusammengesetzter Index
|
||||
`idx_sessions_user_ended ON sessions(user_id, ended_at)` -- deckt per
|
||||
SQLite-Index-Praefix-Regel sowohl "alle eigenen Sitzungen" als auch
|
||||
"eigene noch offene Sitzungen" ab. `idx_sessions_user` (nur `user_id`,
|
||||
aus 0001) bewusst nicht geloescht (Projektkonvention, siehe 0019) -- wird
|
||||
weiterhin von anderen Abfragen (u.a. `GET /admin/sessions`) genutzt.
|
||||
* **`app/catalog/routes.py`**: `GET /catalog/sessions` (Parameter
|
||||
`active_only`, Standard `true`) und `POST /catalog/sessions/{id}/terminate`.
|
||||
Beide Endpunkte filtern hart auf `user.id` -- sowohl beim DB-Zeilenzugriff
|
||||
als auch (beim Beenden) zusaetzlich bei der `active_sessions`-Registry
|
||||
(`entry.user_id != user.id` als zweite, unabhaengige Pruefung). Eine
|
||||
fremde ODER nicht existierende `session_id` liefert in BEIDEN Faellen 404
|
||||
(nicht 403 -- ein abweichender Code wuerde die Existenz einer fremden
|
||||
Sitzung verraten). Bereits beendete eigene Sitzung erneut beenden -> 409.
|
||||
Erfolgreiches Beenden schreibt ein Audit-Ereignis
|
||||
`session_terminated_by_owner` (Pendant zu `session_terminated_by_admin`
|
||||
im Admin-Router) und committet.
|
||||
* **`tests/test_teil_f_schritt2.py`** (4 Tests, alle bestanden): eigene vs.
|
||||
fremde Sitzungen in der Liste; `active_only`-Filterung; **Rechteumgehungs-
|
||||
Testfall aus der F.5-Risikotabelle** (`POST .../terminate` auf fremde
|
||||
`session_id` -> 404, nicht 403/200); echtes Beenden mit `asyncio.Task`
|
||||
(Cancellation wird abgewartet, nicht nur "einmal nachgeben") inkl.
|
||||
Audit-Eintrag- und Hash-Ketten-Pruefung; 409 bei bereits beendeter eigener
|
||||
Sitzung.
|
||||
|
||||
### Verifikation
|
||||
|
||||
* `py_compile` fuer `app/catalog/routes.py` und den neuen Testfile.
|
||||
* `tests/test_teil_f_schritt2.py` einzeln: **4/4 bestanden**.
|
||||
* Voller Regressionslauf ueber alle 20 Testdateien (19 aus F1 + der neue
|
||||
F2-Testfile), erneut im 6-Gruppen-Split wegen der device_bash-Zeitgrenze:
|
||||
|
||||
| Gruppe | Ergebnis |
|
||||
|---|---|
|
||||
| admin_crud/admin_groups_tokens/audit_chain/auth_flow/csp_compliance/phase9 | 46 passed, 1 failed |
|
||||
| passwords_totp/pentest_security/phase10/phase11 | 37 passed, 2 failed |
|
||||
| phase12/phase13 | 22 passed, 6 failed |
|
||||
| phase14/phase15 | 12 passed, 7 failed |
|
||||
| rbac/teil_d_schritt5/teil_d_schritt6/session_recorder | 16 passed, 0 failed |
|
||||
| session_reaper | 1 passed, 2 failed |
|
||||
| teil_f_schritt2 (neu) | 4 passed, 0 failed |
|
||||
| **Summe** | **138 passed, 18 failed, 156 total** |
|
||||
|
||||
156 = 152 (Stand nach Teil D/F1) + 4 neue F2-Tests. Die 18 Fehlschlaege
|
||||
sind unveraendert exakt dieselben wie nach F1 (u.a. wieder
|
||||
`test_user_update_deactivate_and_delete_semantics`,
|
||||
`test_password_change_invalidates_old_session_everywhere`,
|
||||
`test_build_rdp_params_...`, die vier `test_rdp_params_...` aus
|
||||
phase12, `test_session_reaper.py` mit der bekannten
|
||||
Transaktions-Verschachtelungs-Meldung). Migration 0020 wurde in JEDEM
|
||||
Testlauf sichtbar sauber angewendet (Log-Zeile "Wende Migration an:
|
||||
0020_session_user_ended_index.sql"), `PRAGMA foreign_key_check` (Teil der
|
||||
Migrations-Pipeline, haelt bei Verletzung hart an) lief in keinem Lauf an.
|
||||
**Stufe F2 hat null neue Testfehlschlaege verursacht.**
|
||||
|
||||
## 3) Stufe F3 -- FERTIG (Arbeitsflaeche /workspace, eine Sitzung gleichzeitig)
|
||||
|
||||
**Ziel (F.3, F.4-Tabelle):** dauerhafte Arbeitsflaeche mit Seitenleiste;
|
||||
diese Stufe haelt bewusst nur EINE verbundene Sitzung gleichzeitig (F4 baut
|
||||
"mehrere gleichzeitig, Umschalten" darauf auf, siehe F.4-Stufentabelle).
|
||||
|
||||
### Umgesetzt
|
||||
|
||||
* **`app/main.py`**: `GET /workspace` -- wie `/dashboard`/`/admin` reines
|
||||
statisches Markup, Auth clientseitig ueber `GET /auth/me`.
|
||||
* **`templates/workspace.html`**: Topbar (Benutzername, Link zurueck zum
|
||||
Dashboard, Abmelden) + zweispaltiges Layout (Seitenleiste/Hauptbereich).
|
||||
Bindet `terminal.js` UND `rdp.js` gleichzeitig ein -- deren
|
||||
Bootstrap-Bloecke (siehe F1) bleiben hier wirkungslos, da kein Element mit
|
||||
ID `session-container` existiert; `workspace.js` erzeugt Instanzen
|
||||
ausschliesslich selbst. Enthaelt einen staendig sichtbaren Hinweis in der
|
||||
Seitenleiste, dass ein Reload jede offene Sitzung beendet (F.3.7 /
|
||||
Betreiberentscheidung #8 -- **klar beschriftet statt verschwiegen**, siehe
|
||||
Abschnitt 0 Punkt 3). Bewusst KEIN `beforeunload`-Bestaetigungsdialog
|
||||
(native Browser-Warnung) zusaetzlich eingebaut -- der Dauerhinweis in der
|
||||
Seitenleiste wurde als ausreichend fuer "klar beschriftet" bewertet; ein
|
||||
Reload-Warndialog waere eine zusaetzliche UX-Entscheidung, die die Spec
|
||||
nicht verlangt und die eine kuenftige Session bei Bedarf ergaenzen kann.
|
||||
* **`static/js/workspace.js`**: orchestriert drei Zustaende im Hauptbereich
|
||||
(Hostkatalog / eine aktive Sitzung / Wechsel-Bestaetigung) und die
|
||||
Seitenleiste (Kacheln). Kernentscheidungen:
|
||||
* **Kein Server-seitiges "welche Sitzung bin ich"-Signal noetig:** die
|
||||
aktive Kachel im eigenen Tab wird direkt aus dem lokalen
|
||||
`SshTerminalSession`/`RdpSession`-Objekt gespeist (Status-Callback aus
|
||||
F1, Klick-Handler ruft direkt `dispose()`) -- kein Abgleich mit der
|
||||
server-seitigen `session_id` noetig, da terminal_ws.py/ws_tunnel.py
|
||||
diese ID bislang nirgends an den Client zurueckmelden (kein Frame dafuer
|
||||
im Protokoll). **Bewusste Vereinfachung, dokumentiert als Limitierung**
|
||||
unten.
|
||||
* **`GET /catalog/sessions` (F2) wird NUR beim Laden der Seite und direkt
|
||||
nach dem Beenden der eigenen Sitzung abgefragt**, nicht fortlaufend
|
||||
waehrend eine eigene Sitzung aktiv ist. Vermeidet dadurch das Problem,
|
||||
die eigene (im DOM bereits als "aktive Kachel" dargestellte) Sitzung von
|
||||
denselben Zeilen in der API-Antwort unterscheiden zu muessen (kein
|
||||
Duplikat-Risiko an den beiden Abfragezeitpunkten, da die eigene Sitzung
|
||||
dort entweder noch nicht existiert oder bereits beendet ist). **Kehrseite
|
||||
(dokumentierte Limitierung):** Sitzungen, die WAEHREND eine eigene
|
||||
Sitzung offen ist in einem ANDEREN Tab/Fenster geoeffnet oder beendet
|
||||
werden, erscheinen/verschwinden in dieser Seitenleiste nicht live --
|
||||
erst nach dem naechsten Laden/Beenden. Kein Polling in dieser Stufe
|
||||
eingebaut (bewusst, um die Sitzungs-Identitaets-Frage nicht ungeloest
|
||||
mitzuschleppen); ein spaeterer periodischer Abgleich (mit echtem
|
||||
Sitzungs-ID-Abgleich, s.u.) ist Backlog fuer F4/F5.
|
||||
* **Sitzungswechsel ist ehrlich, nicht still:** ein Klick auf "Verbinden"
|
||||
bei bereits offener eigener Sitzung fuehrt NICHT automatisch zum
|
||||
Trennen -- ein eigenes Bestaetigungspanel (`.banner.error` +
|
||||
Beenden-und-wechseln/Abbrechen, kein natives `confirm()` -- passt zum
|
||||
bestehenden Stil des Projekts, das durchgehend eigene Overlays statt
|
||||
Browser-Dialoge nutzt) erklaert, dass diese Stufe nur eine gleichzeitige
|
||||
Sitzung haelt.
|
||||
* `?host=<id>` (von `dashboard.js` gesetzt) verbindet beim Laden direkt,
|
||||
ohne den Katalog zwischenzuschalten.
|
||||
* **`static/js/dashboard.js`**: "Verbinden" fuehrt jetzt zu
|
||||
`/workspace?host={id}` statt direkt zu `/terminal/{id}`/`/rdp/{id}`.
|
||||
Weiterhin `target="_blank"` (eigener Tab) -- das Dashboard bleibt dadurch
|
||||
erreichbar, waehrend die Arbeitsflaeche laeuft; von dort fuehrt der
|
||||
Topbar-Link "Dashboard" zurueck. Die duennen Rahmen `/terminal/{id}`,
|
||||
`/rdp/{id}` selbst bleiben unveraendert erreichbar (F.2: Lesezeichen/
|
||||
Fehlersuche).
|
||||
* **`static/css/app.css`**: neue `.workspace-*`-Regeln (Seitenleiste,
|
||||
Kacheln, Statuspunkt-Farben per Klasse, Hauptbereich). Wichtig:
|
||||
`.workspace-main .session-instance .session-shell { height: 100%; }`
|
||||
ueberschreibt die `height: 100vh`-Regel aus F1 (dort fuer den
|
||||
Ein-Instanz-Vollseiten-Fall gedacht) -- sonst wuerde die Sitzung in der
|
||||
Arbeitsflaeche die Seitenleiste ueberdecken.
|
||||
* **Bewusst NICHT in F3 uebernommen** (gehoert nicht zum Katalog-Funktions-
|
||||
umfang von F.3): die "Zugangsdaten anzeigen"-Funktion des Dashboards
|
||||
(`showCredentials()` in `dashboard.js`) wurde NICHT in den
|
||||
Arbeitsflaechen-Hostkatalog uebernommen -- bleibt eine reine
|
||||
Dashboard-Funktion. Spart Komplexitaet, ohne eine F.3-Anforderung zu
|
||||
verfehlen (die verlangt nur, dass "Neue Sitzung" den Hostkatalog zum
|
||||
Verbinden zeigt).
|
||||
|
||||
### Bekannte Limitierung (fuer F4/F5 vorzumerken)
|
||||
|
||||
Der Server meldet die von ihm vergebene `session_id` (SSH: `terminal_ws.py`,
|
||||
RDP: `ws_tunnel.py`) aktuell an keiner Stelle an den Client zurueck -- weder
|
||||
per JSON-Frame (SSH-Protokoll koennte das leicht ergaenzen) noch per
|
||||
Guacamole-Instruktion (RDP-Protokoll ist reines Guacamole-Binaerprotokoll,
|
||||
eine eigene Instruktion dort einzuschleusen waere riskanter, da
|
||||
guacamole-common-js nur gueltige Guacamole-Instruktionen erwartet). F3 kommt
|
||||
bewusst ohne diese ID aus (s.o.). Sollte F4/F5 einen echten Abgleich
|
||||
"welche Kachel entspricht welcher Server-Sitzung" brauchen (z.B. fuer
|
||||
Live-Polling waehrend mehrerer offener Sitzungen), muss zuerst entschieden
|
||||
werden, ob/wie die SSH- und RDP-Wege dafuer je einen zusaetzlichen
|
||||
Mechanismus bekommen -- **Betreiberentscheidung/Design-Frage fuer eine
|
||||
kuenftige Session, nicht in dieser Stufe geloest.**
|
||||
|
||||
### Verifikation
|
||||
|
||||
* `node --check` fuer `workspace.js` -- fehlerfrei.
|
||||
* **Kein manueller Browser-/SSH-/RDP-Rauchtest moeglich** (siehe bereits bei
|
||||
F1 dokumentierte Einschraenkung -- keine echte SSH-/RDP-Zielumgebung in
|
||||
dieser Sandbox). Die Arbeitsflaeche selbst (Seitenleiste, Katalogwechsel,
|
||||
Wechsel-Bestaetigung) wurde NICHT im echten Browser gesehen -- nur durch
|
||||
Codelesen/CSP-Tests/Backend-Tests abgesichert. **Empfehlung unveraendert:
|
||||
vor dem Weiterbau zu F4 einen manuellen Rauchtest mit echtem SSH-/RDP-Ziel
|
||||
durchfuehren.**
|
||||
* **`tests/test_teil_f_schritt3.py`** (2 Tests, neu): `GET /workspace`
|
||||
liefert die erwarteten Bausteine (Seitenleiste, Hauptbereich, Neue-
|
||||
Sitzung-Knopf, terminal.js/rdp.js/workspace.js eingebunden, KEIN
|
||||
`#session-container` -- sonst wuerde der F1-Bootstrap-Block automatisch
|
||||
eine Sitzung starten); `/catalog/hosts` und `/catalog/sessions` bleiben
|
||||
fuer einen frisch angemeldeten Benutzer erreichbar (leere Sitzungsliste).
|
||||
* **`tests/test_csp_compliance.py`**: `/workspace` in die
|
||||
`test_rendered_pages_contain_no_inline_style_or_script`-Parametrisierung
|
||||
aufgenommen (jetzt 11 statt 10 Tests in der Datei) -- kein Inline-
|
||||
style/-script auf der neuen Seite.
|
||||
* Voller Regressionslauf ueber alle 22 Testdateien (20 aus F1+F2 + der neue
|
||||
F3-Testfile, `test_csp_compliance.py` mit einem zusaetzlichen
|
||||
Parametrisierungsfall):
|
||||
|
||||
| Gruppe | Ergebnis |
|
||||
|---|---|
|
||||
| admin_crud/admin_groups_tokens/audit_chain/auth_flow/phase9 | 36 passed, 1 failed |
|
||||
| passwords_totp/pentest_security/phase10/phase11 | 37 passed, 2 failed |
|
||||
| phase12/phase13 | 22 passed, 6 failed |
|
||||
| phase14/phase15 | 12 passed, 7 failed |
|
||||
| rbac/teil_d_schritt5/teil_d_schritt6/teil_f_schritt2/session_recorder | 20 passed, 0 failed |
|
||||
| session_reaper | 1 passed, 2 failed |
|
||||
| csp_compliance (11) + teil_f_schritt3 (2, neu) | 13 passed, 0 failed |
|
||||
| **Summe** | **141 passed, 18 failed, 159 total** |
|
||||
|
||||
159 = 156 (Stand nach F2) + 2 neue F3-Tests + 1 neuer Parametrisierungsfall
|
||||
in `test_csp_compliance.py`. Die 18 Fehlschlaege sind wieder unveraendert
|
||||
dieselben wie nach F1/F2. **Stufe F3 hat null neue Testfehlschlaege
|
||||
verursacht.**
|
||||
|
||||
## 4) Stufe F4 -- FERTIG (mehrere gleichzeitige Sitzungen, Umschalten, Fokus-/Tastaturtrennung)
|
||||
|
||||
**Ziel (F.3.2, F.3.3):** mehrere Sitzungen gleichzeitig offen halten,
|
||||
zwischen ihnen umschalten ohne Verbindungsabbruch, Eingaben landen nur in
|
||||
der sichtbaren Sitzung, Groesse stimmt nach jedem Umschalten.
|
||||
|
||||
### Geloeste Design-Frage aus F3 (Server-Sitzungs-ID-Zuordnung)
|
||||
|
||||
F3 hatte offen gelassen, wie eine Kachel im Browser einer server-seitigen
|
||||
`sessions`-Zeile zugeordnet wird, da `terminal_ws.py`/`ws_tunnel.py` die
|
||||
`session_id` an keiner Stelle an den Client zurueckmelden. Entscheidung
|
||||
dieser Stufe: **kein Backend-Protokoll-Eingriff** (ein SSH-JSON-Frame waere
|
||||
zwar risikoarm gewesen, aber eine passende Aenderung am RDP-Weg haette eine
|
||||
eigene Instruktion in den Guacamole-Binaerprotokoll-Tunnel einschleusen
|
||||
muessen -- ohne echten Browser/RDP-Testaufbau in dieser Sandbox als zu
|
||||
riskant bewertet, um es ungetestet zu aendern). Stattdessen: **`static/js/
|
||||
workspace.js::claimServerSessionId()`** -- nach `instance.connect()` ruft
|
||||
jede lokal geoeffnete Sitzung bis zu 5x (250ms Abstand) `GET
|
||||
/catalog/sessions?active_only=true` ab und beansprucht die juengste,
|
||||
Host-ID-passende, noch nicht beanspruchte Zeile fuer sich
|
||||
(`claimedSessionIds`-Set). Damit werden eigene Kacheln korrekt aus der
|
||||
"laeuft anderswo"-Liste herausgefiltert -- **das Duplikat-/Verwechslungs-
|
||||
Risiko, das F3 noch offen liess, ist damit fuer den Normalfall geloest.**
|
||||
Dokumentiertes Restrisiko: ein echter Wettlauf (zwei Tabs oeffnen fast
|
||||
zeitgleich eine Sitzung zu genau demselben Host) koennte die Zuordnung in
|
||||
seltenen Faellen vertauschen -- rein kosmetisch (betrifft nur, welche
|
||||
Kachel als "eigene" vs. "anderswo" angezeigt wird, nicht Zugriffsrechte:
|
||||
Beenden bleibt ueber `POST /catalog/sessions/{id}/terminate`, Stufe F2,
|
||||
weiterhin hart auf den eigenen Benutzer beschraenkt, unabhaengig von dieser
|
||||
Heuristik).
|
||||
|
||||
### Umgesetzt
|
||||
|
||||
* **`static/js/workspace.js`** komplett auf Mehrfach-Sitzungen umgebaut:
|
||||
* `openSessions`-Array statt einer einzelnen `activeSession`-Variable;
|
||||
jede Instanz bleibt nach dem Erzeugen dauerhaft in
|
||||
`#workspace-sessions-area` im DOM (wird nie durch Umschalten entfernt,
|
||||
nur ein-/ausgeblendet) -- erst `dispose()` (Sitzung schliessen) entfernt
|
||||
sie wirklich.
|
||||
* `showSession(uid)`: blendet alle anderen Instanzen aus (`.hidden`-Klasse,
|
||||
app.css, derselbe CSP-konforme Sichtbarkeits-Toggle wie ueberall im
|
||||
Projekt) und die Zielinstanz ein, ruft `setActive(true)` auf ihr und
|
||||
`setActive(false)` auf allen anderen -- genau EIN `_active=true`
|
||||
gleichzeitig.
|
||||
* `showCatalog()`: blendet ALLE Sitzungen aus (`setActive(false)` auf
|
||||
jede), zeigt den Hostkatalog -- Sitzungen bleiben dabei voll verbunden
|
||||
im Hintergrund (Betreiberentscheidung #7).
|
||||
* Der bisherige F3-"Wechsel-Bestaetigung"-Dialog (eine Sitzung ersetzt die
|
||||
andere) entfaellt ersatzlos -- F4 erlaubt echte Koexistenz, "Verbinden"
|
||||
im Katalog oeffnet jetzt einfach eine zusaetzliche Kachel.
|
||||
* Sidebar-Kacheln unterscheiden jetzt drei Zustaende optisch:
|
||||
`workspace-tile-active` (gerade sichtbar), `workspace-tile-background`
|
||||
(offen, verbunden, aber ausgeblendet -- anklickbar zum Umschalten,
|
||||
inkl. Tastatur-Bedienung ueber `tabindex`/Enter/Leertaste),
|
||||
`workspace-tile-remote` (laeuft in einem anderen Tab, unveraendert aus
|
||||
F3).
|
||||
* **`static/js/rdp.js`** (F.3.2, Fokus-/Tastatursicherheit):
|
||||
`setActive(active)` ruft jetzt `this.keyboard.reset()`
|
||||
(guacamole-common-js-API, dieselbe Methode, die der offizielle Client bei
|
||||
Fensterfokusverlust nutzt -- per Feature-Check `typeof ... ===
|
||||
"function"` abgesichert) sowohl beim Deaktivieren als auch defensiv beim
|
||||
Aktivieren auf, damit keine haengengebliebenen Modifiertasten (Strg/Alt)
|
||||
von einer verlassenen Sitzung in die naechste durchschlagen. Die
|
||||
eigentliche Weiterleitungs-Sperre (`this._active`-Gate in
|
||||
`keyboard.onkeydown`/`onkeyup`/dem `paste`-Listener) stammt bereits aus
|
||||
F1 und musste hier nicht mehr geaendert werden -- sie greift jetzt einfach
|
||||
echt, weil erstmals mehrere `RdpSession`-Instanzen gleichzeitig existieren
|
||||
koennen.
|
||||
* **`static/js/rdp.js` und `static/js/terminal.js`** (F.3.3, Refit beim
|
||||
Sichtbarwerden): der dokumentweite `resize`/`fullscreenchange`-Listener
|
||||
jeder Instanz reagiert jetzt NUR noch, wenn `this._active` wahr ist --
|
||||
vorher haette ein Browser-Resize bei mehreren gleichzeitig existierenden,
|
||||
aber ausgeblendeten (`display:none`, Groesse 0) Instanzen `fitAddon.fit()`
|
||||
bzw. `sendSize()` mit bedeutungslosen Nullgroessen ausgeloest. Der
|
||||
eigentliche Refit beim Umschalten passiert weiterhin explizit in
|
||||
`setActive(true)` (unveraendert aus F1: `sendResize()` bzw.
|
||||
`_sendCurrentSize()`).
|
||||
* **`templates/workspace.html`**: `#workspace-main` in zwei feste
|
||||
Unterbereiche aufgeteilt -- `#workspace-catalog-view` und
|
||||
`#workspace-sessions-area` -- statt vorher `innerHTML` bei jedem
|
||||
Zustandswechsel neu zu schreiben (das haette in F4 alle offenen
|
||||
Sitzungs-Elemente mitsamt ihrer aktiven WebSockets zerstoert).
|
||||
* **`static/css/app.css`**: `.workspace-tile-background`/`-remote`-Stile;
|
||||
`.workspace-sessions-area .session-instance` (statt vorher direkt auf
|
||||
`.session-instance` im Hauptbereich) fuer die Hoehe; der F3-Uebergangsstil
|
||||
fuer die Wechsel-Bestaetigung (`.workspace-replace-confirm` usw.) entfernt,
|
||||
da der Dialog entfaellt.
|
||||
|
||||
### Bewusst NICHT in Stufe F4 (folgt in F5)
|
||||
|
||||
* F.3.8 (Obergrenze in der Oberflaeche sichtbar -- "Neue Sitzung"
|
||||
deaktivieren) -- ein Ueberschreiten zeigt sich in F4 nur reaktiv als
|
||||
Fehlerstatus auf der neuen Kachel (Server lehnt mit WS-Code 4429 ab, siehe
|
||||
`app/ssh_proxy/terminal_ws.py`/`app/rdp_proxy/ws_tunnel.py`, E.4 aus Teil
|
||||
E) -- funktional korrekt, aber nicht die von F.3.8 verlangte
|
||||
*vorherige* Kennzeichnung.
|
||||
* Live-Aktualisierung der Seitenleiste fuer Sitzungen, die in ANDEREN Tabs
|
||||
waehrend einer laufenden eigenen Sitzung geoeffnet/beendet werden (siehe
|
||||
oben, unveraendert aus F3 -- die Sitzungs-ID-Zuordnung loest das
|
||||
Duplikat-Problem, aber es wird weiterhin nicht periodisch neu abgefragt).
|
||||
|
||||
### Verifikation
|
||||
|
||||
* `node --check` fuer `terminal.js`, `rdp.js`, `workspace.js`, `dashboard.js`
|
||||
-- alle vier fehlerfrei.
|
||||
* **Weiterhin kein manueller Browser-/SSH-/RDP-Rauchtest moeglich** (keine
|
||||
echte Zielumgebung in dieser Sandbox). Das betrifft F4 staerker als F1-F3,
|
||||
weil hier die eigentliche, in der F.5-Risikotabelle als groesstes Risiko
|
||||
benannte Eigenschaft entsteht ("Eingaben landen in der falschen
|
||||
Sitzung"). Die Absicherung stuetzt sich in dieser Stufe ausschliesslich
|
||||
auf Codelesen/-gegenueberstellung (SSH: `xterm.js` bindet ohnehin an sein
|
||||
eigenes Containerelement, kein Aenderungsbedarf; RDP: das bereits in F1
|
||||
angelegte `_active`-Gate plus das neue `keyboard.reset()`) -- **dringende
|
||||
Empfehlung fuer die naechste Session mit Zugriff auf echte SSH-/RDP-Ziele:
|
||||
das F.6-Abnahmeszenario (vier Sitzungen, mehrfach umschalten, 30 Minuten
|
||||
offen lassen) tatsaechlich im Browser durchspielen, bevor F5 als
|
||||
"fertig" gilt.**
|
||||
* Keine neuen Backend-Testdateien in dieser Stufe (F4 ist eine reine
|
||||
Frontend-/JS-Aenderung ohne neue Endpunkte, Migrationen oder
|
||||
Schema-Aenderungen -- die bestehenden F1-F3-Tests decken weiterhin
|
||||
denselben Backend-Vertrag ab, auf dem `workspace.js` aufbaut). Voller
|
||||
Regressionslauf ueber alle 22 Testdateien (unveraendert gegenueber F3):
|
||||
|
||||
| Gruppe | Ergebnis |
|
||||
|---|---|
|
||||
| csp_compliance/teil_f_schritt2/teil_f_schritt3 | 17 passed, 0 failed |
|
||||
| admin_crud/admin_groups_tokens/audit_chain/auth_flow/phase9 | 36 passed, 1 failed |
|
||||
| passwords_totp/pentest_security/phase10/phase11 | 37 passed, 2 failed |
|
||||
| phase12/phase13 | 22 passed, 6 failed |
|
||||
| phase14/phase15 | 12 passed, 7 failed |
|
||||
| rbac/teil_d_schritt5/teil_d_schritt6/session_recorder | 16 passed, 0 failed |
|
||||
| session_reaper | 1 passed, 2 failed |
|
||||
| **Summe** | **141 passed, 18 failed, 159 total** |
|
||||
|
||||
Identische Zahlen wie am Ende von F3 (159 Tests insgesamt, dieselben 18
|
||||
bereits bekannten Fehlschlaege) -- erwartungsgemaess, da F4 keine
|
||||
Backend-/Testdatei-Aenderungen enthielt. **Stufe F4 hat null neue
|
||||
Testfehlschlaege verursacht**, wohlgemerkt bei einer Stufe, deren
|
||||
eigentliche Kernaussage (Eingabe-Isolation) mit den vorhandenen
|
||||
Backend-Tests strukturell gar nicht pruefbar ist -- siehe
|
||||
Rauchtest-Empfehlung oben.
|
||||
|
||||
## 5) Stufe F5 -- FERTIG
|
||||
|
||||
Ziel (F.4 der Spec-Tabelle, Cowork-Task #21): Obergrenzen in der
|
||||
Oberflaeche sichtbar machen (F.3.8), Sammelzustand-Kennzeichnung bei
|
||||
Hintergrundfehlern/-trennungen, Feinschliff.
|
||||
|
||||
### F.3.8 -- Obergrenzen sichtbar
|
||||
|
||||
Bisher (F1-F4) war die Durchsetzung der Obergrenzen (Teil E.4,
|
||||
`app/ssh_proxy/terminal_ws.py`/`app/rdp_proxy/ws_tunnel.py`, VOR jedem
|
||||
Ressourcenverbrauch geprueft, WS-Code 4429 bei Ueberschreitung) bereits
|
||||
vollstaendig vorhanden und wirksam -- F.3.8 verlangt zusaetzlich, dass die
|
||||
Grenze VOR dem Verbindungsversuch sichtbar ist, nicht erst als
|
||||
Fehlerstatus danach.
|
||||
|
||||
* **`app/catalog/routes.py`**: neuer Endpunkt `GET /catalog/session-limits`
|
||||
(Abschnitt "Sitzungs-Obergrenzen (F5, Teil F.3.8)"). Liest ausschliesslich
|
||||
bereits vorhandene Quellen -- `app/config.py` (`max_sessions_per_user`,
|
||||
`max_sessions_global`) und `app/security/active_sessions.py`
|
||||
(`count_for_user()`, `count_total()`) -- und bildet **exakt dieselbe
|
||||
Schwellenlogik** wie die Durchsetzung nach: Admins sind von der
|
||||
Je-Nutzer-Grenze ausgenommen (`user_limit_applies`/`at_user_limit` nur bei
|
||||
`not user.is_admin`), nicht aber von der globalen Grenze
|
||||
(`at_global_limit` gilt fuer jeden). Absichtlich keine neue
|
||||
Zustandsmaschine, keine Verdopplung der Durchsetzung -- reine
|
||||
Lesefunktion. Liefert ausschliesslich aggregierte Zahlen, keine
|
||||
Details zu fremden Sitzungen (user_id/hostname anderer Nutzer bleiben
|
||||
wie beim gesamten Katalog-Router unsichtbar, siehe Moduldoc).
|
||||
* **`static/js/workspace.js`**: `refreshSessionLimits()`/`atAnyLimit()`/
|
||||
`renderLimitHint()` -- ruft den neuen Endpunkt beim Laden (`main()`), nach
|
||||
dem Start einer Sitzung und nach dem Schliessen (jeweils mit derselben
|
||||
300ms-Verzoegerung wie der bestehende `refreshRemoteSessions()`-Aufruf,
|
||||
da `active_sessions.register()`/`unregister()` serverseitig erst nach dem
|
||||
WS-Handshake bzw. nach dem `finally`-Block laufen). `renderLimitHint()`
|
||||
zeigt "Eigene Sitzungen: X/Y" (nur wenn die Grenze fuer diesen Nutzer
|
||||
gilt) und "Server gesamt: X/Y" im Sidebar-Header und deaktiviert
|
||||
`+ Neue Sitzung`, sobald eine der beiden Grenzen erreicht ist.
|
||||
`hostCard()` deaktiviert zusaetzlich jeden einzelnen
|
||||
"Verbinden"-Knopf im Hostkatalog unter derselben Bedingung.
|
||||
Serverseitige Durchsetzung (WS-Code 4429) bleibt unveraendert die
|
||||
verbindliche Kontrolle -- diese Anzeige ist bewusst nur informativ und
|
||||
kann durch die Race zwischen Anzeige-Refresh und tatsaechlichem
|
||||
Verbindungsaufbau kurzzeitig veralten (dieselbe Klasse von
|
||||
Eventual-Consistency wie bei `claimServerSessionId` aus F4).
|
||||
* **`templates/workspace.html`**: neues Element
|
||||
`<p id="workspace-limit-hint">` im Sidebar-Header.
|
||||
* **`static/css/app.css`**: `.workspace-limit-hint--at-limit` (Warnfarbe bei
|
||||
erreichter Grenze) sowie ein neues globales
|
||||
`button:disabled { opacity: 0.5; cursor: not-allowed; }` (gab es vorher
|
||||
nicht -- deaktivierte Knoepfe sahen bislang optisch aktiv aus).
|
||||
|
||||
### Sammelzustand-Kennzeichnung bei Hintergrundfehlern/-trennungen
|
||||
|
||||
* **`static/js/workspace.js`**: `hasAttention(record)` (Hilfsfunktion neben
|
||||
`statusClass()`) erkennt eine Hintergrundsitzung mit Fehler- oder
|
||||
Trennungsstatus. `buildLocalTile()` markiert eine solche Kachel zusaetzlich
|
||||
mit der Klasse `workspace-tile-attention` und ersetzt den generischen Text
|
||||
"im Hintergrund verbunden" durch "im Hintergrund: Fehler" bzw.
|
||||
"im Hintergrund: getrennt". `renderSidebar()` fasst zusaetzlich alle
|
||||
betroffenen Hintergrundsitzungen (die gerade aktive zaehlt nicht mit, ihr
|
||||
Zustand ist ohnehin direkt sichtbar) in einer Sammel-Zeile ueber der
|
||||
Kachelliste zusammen ("⚠ N Sitzungen im Hintergrund mit Problem"), damit
|
||||
ein Problem nicht erst durch Umschalten auf jede einzelne Kachel entdeckt
|
||||
wird.
|
||||
* **`templates/workspace.html`**: neues Element
|
||||
`<div id="workspace-attention-summary" class="hidden">`.
|
||||
* **`static/css/app.css`**: `.workspace-tile-attention` (roter Rahmen) und
|
||||
`.workspace-attention-summary` (Warnbanner-Stil).
|
||||
|
||||
### Feinschliff
|
||||
|
||||
* Globales `button:disabled`-Stil (siehe oben) -- kam durch F.3.8 zum
|
||||
ersten Mal ueberhaupt im Projekt zum Einsatz (vorher gab es keine
|
||||
deaktivierten Knoepfe in dieser Form), war aber vorher schlicht nicht
|
||||
vorhanden und wird jetzt konsequent von allen drei neuen
|
||||
deaktivierbaren Knoepfen (`+ Neue Sitzung`, jeder Katalog-"Verbinden") genutzt.
|
||||
|
||||
### Bewusst NICHT in Stufe F5
|
||||
|
||||
* Keine Aenderung an der serverseitigen Durchsetzung selbst (E.4) -- F5 ist
|
||||
ausschliesslich Anzeige/UX auf Basis der bestehenden, bereits in
|
||||
frueheren Sessions verifizierten Logik.
|
||||
* Keine Push-/Live-Aktualisierung der Obergrenzen-Anzeige zwischen Tabs
|
||||
(dieselbe bekannte Einschraenkung wie die Sitzungsliste selbst seit F3 --
|
||||
kein Polling, nur bei den bereits vorhandenen Refresh-Zeitpunkten dieser
|
||||
Seite).
|
||||
|
||||
### Verifikation
|
||||
|
||||
* `node --check` fuer `workspace.js` (auf dem Geraet) -- fehlerfrei.
|
||||
`python3 -m py_compile` fuer `app/catalog/routes.py` -- fehlerfrei.
|
||||
* Neue Testdatei `tests/test_teil_f_schritt5.py` (5 Tests, alle bestehen):
|
||||
Obergrenzen entsprechen `app/config.py` bei leerer Registry;
|
||||
`current_user_count` zaehlt ausschliesslich eigene Registry-Eintraege
|
||||
(fremde Sitzung beeinflusst NUR den globalen, nicht den eigenen Zaehler
|
||||
-- F.5-Risikotabelle-Prinzip wie in `test_teil_f_schritt2.py`);
|
||||
`at_user_limit` wird bei Erreichen von `max_sessions_per_user` wahr;
|
||||
die Admin-Asymmetrie aus `terminal_ws.py` (ausgenommen von der
|
||||
Je-Nutzer-Grenze, NICHT von der globalen) wird exakt nachgebildet;
|
||||
der Endpunkt verlangt eine Anmeldung (401 sonst).
|
||||
* Voller Regressionslauf ueber alle 23 Testdateien:
|
||||
|
||||
| Gruppe | Ergebnis |
|
||||
|---|---|
|
||||
| csp_compliance/teil_f_schritt2/teil_f_schritt3/teil_f_schritt5 | 22 passed, 0 failed |
|
||||
| admin_crud/admin_groups_tokens/audit_chain/auth_flow/phase9 | 36 passed, 1 failed |
|
||||
| passwords_totp/pentest_security/phase10/phase11 | 37 passed, 2 failed |
|
||||
| phase12/phase13 | 22 passed, 6 failed |
|
||||
| phase14/phase15 | 12 passed, 7 failed |
|
||||
| rbac/teil_d_schritt5/teil_d_schritt6/session_recorder | 16 passed, 0 failed |
|
||||
| session_reaper | 1 passed, 2 failed |
|
||||
| **Summe** | **146 passed, 18 failed, 164 total** |
|
||||
|
||||
Gegenueber F4 (141/18/159): +5 neue, bestehende Tests, dieselben 18
|
||||
bereits bekannten, dokumentierten Fehlschlaege (siehe
|
||||
`FORTSETZUNG_Teil_C.md`), **null neue Regressionen**.
|
||||
|
||||
## 6) Abnahme (F.6) -- Code-Stand fertig, Live-Szenario ausstehend
|
||||
|
||||
Wortlaut F.6 (Umsetzungsauftrag_Sonnet5.md): "Ein Benutzer oeffnet vier
|
||||
Sitzungen (zwei SSH, zwei RDP) in der Arbeitsflaeche, schaltet mehrfach
|
||||
zwischen ihnen um und laesst sie 30 Minuten offen. Danach gilt: alle vier
|
||||
sind noch verbunden, in jeder ist die zuletzt gesehene Ausgabe vollstaendig
|
||||
vorhanden, Eingaben landen ausschliesslich in der sichtbaren Sitzung, die
|
||||
Groesse stimmt nach jedem Umschalten, es existieren genau vier
|
||||
`sessions`-Zeilen mit vier Aufzeichnungen, und ein zweiter Benutzer arbeitet
|
||||
waehrenddessen unbeeintraechtigt."
|
||||
|
||||
### Beim Vorbereiten dieses Abschnitts gefundene und behobene Luecke
|
||||
|
||||
Bei der Pruefung von "30 Minuten offen, alle vier noch verbunden" gegen den
|
||||
tatsaechlichen Code fiel auf: **F.3.4 ("Hintergrundsitzungen duerfen nicht
|
||||
sterben") war fuer eine echt untaetige SSH-Sitzung NICHT vollstaendig
|
||||
erfuellt.** `app/config.py::ssh_idle_timeout_s` steht auf 900s (15 Minuten);
|
||||
E10 (`app/ssh_proxy/terminal_ws.py`) zaehlt zwar sowohl Eingabe als auch
|
||||
Zielausgabe als Aktivitaet (deckt z.B. `tail -f`/einen laufenden Build ab),
|
||||
aber eine Sitzung mit einer wirklich stillen Shell (kein Tastendruck, keine
|
||||
spontane Ausgabe) haette nach 15 Minuten den Idle-Timeout ausgeloest --
|
||||
still im Hintergrund, waehrend die Kachel in der Seitenleiste weiter
|
||||
"verbunden" zeigte. Das widerspricht direkt Betreiberentscheidung #7
|
||||
("voll verbunden bleiben") und haette das F.6-Kriterium selbst beim
|
||||
einfachsten denkbaren Testlauf (vier Sitzungen oeffnen, nichts weiter tun,
|
||||
30 Minuten warten) zum Scheitern gebracht. RDP (`app/rdp_proxy/ws_tunnel.py`)
|
||||
hat ueberhaupt keinen Idle-Timeout, ist also nicht betroffen.
|
||||
|
||||
Behoben rein clientseitig, ohne die serverseitige Logik zu aendern:
|
||||
`static/js/terminal.js::SshTerminalSession.connect()` sendet ab jetzt alle
|
||||
4 Minuten (deutlich unter den 15 Minuten) einen inhaltsleeren
|
||||
`{"type": "ping"}` ueber die offene WebSocket-Verbindung, unabhaengig davon,
|
||||
ob die Sitzung gerade sichtbar ist oder im Hintergrund liegt. Das ist keine
|
||||
neue Sonderbehandlung auf dem Server: `terminal_ws.py` aktualisiert
|
||||
`activity["t"]` bereits fuer JEDE empfangene Nachricht, noch VOR der
|
||||
`type`-Weiche (`if msg.get("type") == "input": ... elif ... == "resize":
|
||||
...`) -- ein unbekannter `type` wie `"ping"` faellt danach einfach durch,
|
||||
ohne Fehler und ohne dass eine der beiden Zweige ausgefuehrt wird. Timer
|
||||
wird in `dispose()` und im `close`-Handler aufgeraeumt (kein Leck bei
|
||||
Sitzungsende). `node --check` fehlerfrei; der volle Regressionslauf
|
||||
(csp_compliance/teil_f_schritt2/3/5, siehe unten) zeigt weiterhin 22/22 --
|
||||
erwartungsgemaess, da die Aenderung rein clientseitig ist und die
|
||||
Server-Verzweigung fuer unbekannte `type`-Werte bereits vor dieser Aenderung
|
||||
bestand (kein neuer, testbar isolierter Serverpfad entstanden).
|
||||
|
||||
**Kein Backend-Test fuer diese Aenderung**: die Idle-Timeout-Schleife liegt
|
||||
inline in der grossen `ssh_terminal()`-Route (nicht in einer eigenstaendigen
|
||||
Funktion) und haengt an einer echten `asyncssh`-Verbindung -- ein
|
||||
Zeit-basierter WS-Integrationstest dafuer waere ein eigenes, nicht triviales
|
||||
Vorhaben (aehnliche Komplexitaet wie die bereits bestehenden, teils
|
||||
fehlschlagenden Mock-Tests in `test_phase15.py`) und wurde hier bewusst
|
||||
nicht nachgezogen, um die Codepruefung nicht mit einem unsicheren neuen Test
|
||||
zu vermischen. Die Absicherung stuetzt sich auf Codelesen (oben) --
|
||||
**die eigentliche Bestaetigung braucht den echten 30-Minuten-Lauf.**
|
||||
|
||||
### Kriterium-fuer-Kriterium-Stand
|
||||
|
||||
| Kriterium (F.6) | Stand |
|
||||
|---|---|
|
||||
| Vier Sitzungen (2 SSH + 2 RDP) gleichzeitig oeffnen | Code vorhanden (F4: mehrere gleichzeitige `SshTerminalSession`/`RdpSession`-Instanzen, keine Zerstoerung beim Umschalten); Standard-Obergrenze 6/Nutzer laesst vier zu. **Live-Test aussteht.** |
|
||||
| Mehrfaches Umschalten | Code vorhanden (F1/F4: `setActive()`, `.hidden`-Toggle, Refit/`sendSize()` beim Sichtbarwerden). **Live-Test aussteht.** |
|
||||
| 30 Minuten offen, danach alle vier noch verbunden | Betreiberentscheidung #7 umgesetzt; SSH-Idle-Timeout-Luecke oben gefunden UND behoben; RDP ohne Idle-Timeout unproblematisch. **Live-Test aussteht** (einzige Moeglichkeit, echte 15+ Minuten Stille zuverlaessig zu pruefen). |
|
||||
| Ausgabe in jeder Sitzung vollstaendig vorhanden | Kein Datenverlust durch die Architektur zu erwarten (jede Instanz haelt ihren eigenen `xterm.js`/Guacamole-Zustand, wird nie zerstoert) -- aber nur am echten Terminal pruefbar. **Live-Test aussteht.** |
|
||||
| Eingaben nur in der sichtbaren Sitzung | Code vorhanden: SSH bindet ohnehin an sein eigenes Containerelement (kein Aenderungsbedarf); RDP nutzt das `_active`-Gate (F1) plus `keyboard.reset()` beim Umschalten (F4). Laut F.5-Risikotabelle das groesste Einzelrisiko der ganzen Stufe. **Live-Test aussteht -- dringendste Einzelpruefung vor einer echten Abnahme.** |
|
||||
| Groesse stimmt nach jedem Umschalten | Code vorhanden: `setActive(true)` loest `sendResize()`/`_sendCurrentSize()` aus (F1), Hintergrund-Resize-Events sind seit F4 per `_active`-Gate unterdrueckt. **Live-Test aussteht.** |
|
||||
| Genau vier `sessions`-Zeilen mit vier Aufzeichnungen | Jede Sitzung erzeugt beim Verbindungsaufbau ihre eigene DB-Zeile + `SessionRecorder` (unveraendert seit vor Teil F, F.3.10 als harte Regel nie angetastet) -- vier gleichzeitige Instanzen ergeben strukturell vier Zeilen. **Live-Test aussteht** (Stichprobe in der DB nach dem Lauf). |
|
||||
| Zweiter Benutzer unbeeintraechtigt | Jede neue Katalog-/Sitzungs-API in F2/F5 ist zwingend auf `user_id = eigene` beschraenkt (siehe Moduldoc `app/catalog/routes.py`, dedizierte Tests in `test_teil_f_schritt2.py`/`test_teil_f_schritt5.py` fuer Fremdsitzungs-Isolation). `active_sessions`-Registry ist bereits seit Teil E pro Sitzung, nicht pro Nutzer, geteilt. **Live-Test aussteht** (echter zweiter Login parallel). |
|
||||
|
||||
### Ehrliches Fazit
|
||||
|
||||
Der komplette Code-Stand fuer Teil F (F1-F5) ist fertig, regressionsfrei
|
||||
(146 bestehende + 5 neue Tests bestehen, dieselben 18 laengst bekannten,
|
||||
dokumentierten Fehlschlaege aus `FORTSETZUNG_Teil_C.md` unveraendert) und
|
||||
gegen jedes einzelne F.6-Kriterium durchgesehen -- dabei wurde eine echte,
|
||||
bis dahin unentdeckte Luecke gefunden und behoben (SSH-Idle-Timeout fuer
|
||||
Hintergrundsitzungen, oben). Was in dieser Sandbox **nicht** moeglich war
|
||||
und **fuer keine Stufe von F1 bis F5** nachgeholt werden konnte: ein
|
||||
echter Browser mit echten SSH-/RDP-Zielsystemen. Alle sieben Kriterien in
|
||||
der Tabelle oben sind auf Code-Ebene plausibel, aber keines ist tatsaechlich
|
||||
durchgespielt worden. **Teil F sollte erst dann als im Sinne von F.6
|
||||
"abgenommen" gelten, wenn eine Session mit Zugriff auf echte
|
||||
SSH-/RDP-Testziele und einen echten Browser das Szenario tatsaechlich einmal
|
||||
durchspielt** -- mit besonderem Augenmerk auf Eingabe-Isolation (groesstes
|
||||
Risiko laut F.5) und den 30-Minuten-Idle-Fall (siehe oben).
|
||||
Reference in New Issue
Block a user