743 lines
44 KiB
Markdown
743 lines
44 KiB
Markdown
# 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).
|