umbau 1.3
This commit is contained in:
150
scripts/cleanup_rdp_drives.py
Normal file
150
scripts/cleanup_rdp_drives.py
Normal file
@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Entfernt die pro RDP-Sitzung umgeleiteten Laufwerksverzeichnisse unter
|
||||
{data_dir}/rdp-drives, sobald die zugehoerige Sitzung beendet ist.
|
||||
|
||||
Warum ein eigener, als root laufender Dienst und nicht die App selbst
|
||||
(Befund E6, Umsetzungsauftrag Teil E -- Nachtrag Live-Test 3.9.):
|
||||
|
||||
Die App hat auf diese Verzeichnisse GRUNDSAETZLICH keinen Zugriff, und das
|
||||
ist Absicht:
|
||||
|
||||
/var/lib/jumphost 0710 jumphost:jumphost
|
||||
/var/lib/jumphost/rdp-drives 0750 guacd:guacd (Rolle guacd)
|
||||
.../rdp-drives/{host}_{session} 0700 guacd:guacd (von guacd selbst
|
||||
per "create-drive-path" angelegt; guacd
|
||||
ruft mkdir mit S_IRWXU auf, die Unit setzt
|
||||
zusaetzlich UMask=0077)
|
||||
|
||||
Der App-Prozess laeuft als "jumphost" und ist NICHT in der Gruppe "guacd"
|
||||
(umgekehrt schon: guacd ist in der Gruppe "jumphost", damit es
|
||||
/var/lib/jumphost ueberhaupt durchqueren kann). Fuer "other" bleiben auf
|
||||
rdp-drives 0 Bits -- kein Lesen, kein Schreiben, kein Betreten. Ein
|
||||
shutil.rmtree() aus der App heraus kann daher NIE erfolgreich sein.
|
||||
|
||||
Genau das ist die alte Fassung von app/rdp_proxy/ws_tunnel.py::
|
||||
_cleanup_rdp_drive_path() gewesen -- und weil sie rmtree mit
|
||||
ignore_errors=True aufrief, ist der PermissionError stillschweigend
|
||||
verschluckt worden: kein Logeintrag, keine Fehlermeldung, und die
|
||||
Verzeichnisse (inklusive der darin uebertragenen Dateien) sind seit
|
||||
Inbetriebnahme unbegrenzt liegengeblieben.
|
||||
|
||||
Das laesst sich nicht ueber Gruppenrechte reparieren: die Blattverzeichnisse
|
||||
haben Modus 0700, eine Gruppenmitgliedschaft wuerde daran nichts aendern, und
|
||||
guacd das Recht zu geben, sie selbst wieder abzuraeumen, kann guacd nicht --
|
||||
es kennt das Sitzungsende nicht.
|
||||
|
||||
Deshalb dieser Weg: ein systemd-Timer ruft dieses Skript als root auf. Es
|
||||
loescht ein Verzeichnis NUR dann, wenn dessen Sitzungs-ID in der Datenbank
|
||||
als beendet markiert ist (sessions.ended_at IS NOT NULL) -- also kein
|
||||
Altersheuristik-Zeitfenster, das eine lange, aber stille Sitzung mitten im
|
||||
Betrieb abraeumen koennte.
|
||||
|
||||
Aufruf (siehe ansible/roles/jumphost_app):
|
||||
cleanup_rdp_drives.py [--data-dir /var/lib/jumphost] [--dry-run]
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sqlite3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Verzeichnisname, den app/rdp_proxy/guacd_client.py::rdp_drive_path() erzeugt:
|
||||
# "{host_id}_{session_id}". Alles, was diesem Muster NICHT entspricht, wird
|
||||
# bewusst in Ruhe gelassen -- dieses Skript laeuft als root und soll unter
|
||||
# keinen Umstaenden etwas anfassen, das es nicht selbst zuordnen kann.
|
||||
_DRIVE_DIR_RE = re.compile(r"^(?P<host_id>\d+)_(?P<session_id>\d+)$")
|
||||
|
||||
logger = logging.getLogger("jumphost.cleanup_rdp_drives")
|
||||
|
||||
|
||||
def _open_sessions(db_path: Path) -> set[int]:
|
||||
"""IDs aller Sitzungen, die NOCH LAUFEN (ended_at IS NULL).
|
||||
|
||||
Bewusst die laufenden statt der beendeten abfragen: ein Verzeichnis wird
|
||||
nur geloescht, wenn seine Sitzung nachweislich NICHT mehr in dieser Menge
|
||||
steht. Faellt die Abfrage aus, bricht das Skript ab (siehe main) --
|
||||
lieber nichts loeschen als das Falsche.
|
||||
"""
|
||||
# Nur-Lesend oeffnen: dieses Skript darf die Datenbank unter keinen
|
||||
# Umstaenden veraendern, und ein versehentliches Anlegen einer leeren DB
|
||||
# (Standardverhalten von sqlite3.connect bei falschem Pfad) wuerde jede
|
||||
# Sitzung als "beendet" erscheinen lassen und ALLES loeschen.
|
||||
uri = f"file:{db_path}?mode=ro"
|
||||
with sqlite3.connect(uri, uri=True, timeout=10.0) as conn:
|
||||
rows = conn.execute("SELECT id FROM sessions WHERE ended_at IS NULL").fetchall()
|
||||
return {int(r[0]) for r in rows}
|
||||
|
||||
|
||||
def cleanup(data_dir: Path, dry_run: bool = False) -> tuple[int, int]:
|
||||
"""Gibt (entfernt, uebersprungen) zurueck."""
|
||||
drives_dir = data_dir / "rdp-drives"
|
||||
if not drives_dir.is_dir():
|
||||
logger.info("Kein Verzeichnis %s vorhanden -- nichts zu tun.", drives_dir)
|
||||
return (0, 0)
|
||||
|
||||
open_ids = _open_sessions(data_dir / "jumphost.db")
|
||||
logger.debug("Laufende Sitzungen laut Datenbank: %s", sorted(open_ids) or "keine")
|
||||
|
||||
removed = skipped = 0
|
||||
for entry in sorted(drives_dir.iterdir()):
|
||||
match = _DRIVE_DIR_RE.match(entry.name)
|
||||
if not entry.is_dir() or not match:
|
||||
logger.debug("Uebersprungen (kein Sitzungsverzeichnis): %s", entry)
|
||||
skipped += 1
|
||||
continue
|
||||
session_id = int(match.group("session_id"))
|
||||
if session_id in open_ids:
|
||||
logger.debug("Uebersprungen (Sitzung %s laeuft noch): %s", session_id, entry)
|
||||
skipped += 1
|
||||
continue
|
||||
if dry_run:
|
||||
logger.info("[dry-run] Wuerde entfernen: %s", entry)
|
||||
removed += 1
|
||||
continue
|
||||
try:
|
||||
shutil.rmtree(entry)
|
||||
except OSError:
|
||||
# Nicht ignorieren: wenn selbst root hier scheitert, stimmt etwas
|
||||
# Grundsaetzliches nicht (Einhaengepunkt, Dateisystem read-only)
|
||||
# und das gehoert sichtbar ins Journal -- genau der Fehler, den
|
||||
# die alte App-seitige Fassung verschluckt hat.
|
||||
logger.warning("Konnte %s nicht entfernen", entry, exc_info=True)
|
||||
skipped += 1
|
||||
continue
|
||||
logger.info("Entfernt: %s (Sitzung %s beendet)", entry, session_id)
|
||||
removed += 1
|
||||
return (removed, skipped)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument(
|
||||
"--data-dir",
|
||||
default=os.environ.get("JUMPHOST_DATA_DIR", "/var/lib/jumphost"),
|
||||
help="Datenverzeichnis der Anwendung (Standard: $JUMPHOST_DATA_DIR oder /var/lib/jumphost)",
|
||||
)
|
||||
parser.add_argument("--dry-run", action="store_true", help="Nur anzeigen, was entfernt wuerde")
|
||||
parser.add_argument("--verbose", "-v", action="store_true", help="Debug-Ausgabe")
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG if args.verbose else logging.INFO,
|
||||
format="%(levelname)s %(message)s",
|
||||
)
|
||||
try:
|
||||
removed, skipped = cleanup(Path(args.data_dir), dry_run=args.dry_run)
|
||||
except sqlite3.Error:
|
||||
# Ohne verlaessliche Sitzungsliste wird NICHTS geloescht.
|
||||
logger.error("Datenbank nicht lesbar -- es wurde nichts entfernt", exc_info=True)
|
||||
return 1
|
||||
logger.info("Fertig: %d entfernt, %d uebersprungen.", removed, skipped)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user