diff --git a/srv/tesm/app.py b/srv/tesm/app.py index d01448f..c65372b 100644 --- a/srv/tesm/app.py +++ b/srv/tesm/app.py @@ -23,7 +23,13 @@ import paramiko import yaml import ssl from ldap3 import Server, Connection, Tls, ALL, SIMPLE, BASE -from ldap3.core.exceptions import LDAPException +from ldap3.core.exceptions import ( + LDAPException, + LDAPSocketOpenError, + LDAPSocketReceiveError, + LDAPSocketSendError, + LDAPResponseTimeoutError, +) from ldap3.utils.conv import escape_filter_chars LDAP_MATCHING_RULE_IN_CHAIN = "1.2.840.113556.1.4.1941" @@ -196,6 +202,12 @@ PERMISSIONS = { "edit": "maintenance.edit", }, }, + "papierkorb": { + "label": "Papierkorb", + "rows": { + "view": "papierkorb.view", + }, + }, }, }, "logs_group": { @@ -264,6 +276,7 @@ PERMISSION_LABELS = { "credentials.edit": "Zugangsdaten ändern (inkl. Löschen)", "maintenance.view": "Wartungsseite ansehen (Geräte-Status, Job-Verlauf)", "maintenance.edit": "Wartungsaktionen ausführen (SSH-Update, SSH-Neustart)", + "papierkorb.view": "Papierkorb ansehen (gelöschte Geräte/Switche/Zugangsdaten/Benutzer/Gruppen — jede Kategorie zusätzlich nur mit deren eigenem Leserecht sichtbar)", "logs_live.view": "Live-Log lesen", "logs_activity.view": "Änderungslog lesen", "logs_kea.view": "Kea-DHCP-Log lesen", "users.view": "Benutzer lesen", "users.create": "Benutzer anlegen", @@ -516,13 +529,14 @@ class User(UserMixin): @property def can_view_papierkorb(self): - """Papierkorb-Seite: sichtbar sobald mind. eine der fünf dort - gelisteten Ressourcen (Geräte/Switche/Zugangsdaten/Benutzer/Gruppen) - View-Recht hat — jede Kachel darin ist ohnehin einzeln je Ressource - gated (siehe papierkorb.html).""" - return self.has_any_permission( - ["devices.view", "switches.view", "credentials.view", "users.view", "groups.view"] - ) + """Papierkorb-Seite: eigenes, dediziertes Recht (papierkorb.view) -- + vorher implizit sichtbar für jeden mit irgendeiner der fünf + Ressourcen-Leserechte, wodurch praktisch jeder Benutzer den + Papierkorb sehen konnte (Standardgruppe hat bereits devices.view). + Innerhalb der Seite ist jede Kachel zusätzlich einzeln je Ressource + gated (siehe papierkorb.html) -- papierkorb.view allein zeigt also + noch keine Inhalte, sondern schaltet nur die Seite selbst frei.""" + return self.has_permission("papierkorb.view") @property def can_view_live_log(self): @@ -1232,6 +1246,32 @@ def _ldap_settings(): } +_LDAP_TRANSIENT_EXCEPTIONS = ( + LDAPSocketOpenError, + LDAPSocketReceiveError, + LDAPSocketSendError, + LDAPResponseTimeoutError, +) + + +def _ldap_connect_with_retry(factory): + """Baut eine LDAP-Verbindung auf, mit genau einem automatischen + Wiederholversuch bei einem rein netzwerk-/timeout-bedingten Fehler + (z.B. ein Domain-Controller in einem anderen, langsameren Netzsegment + -- die allererste Verbindung dorthin ist oft spürbar langsamer als + jede folgende, live reproduziert: erster Login schlug mit "Benutzer/ + Passwort ungültig" fehl, ein sofortiger zweiter Versuch klappte). Der + Retry greift NICHT bei einer tatsächlich abgelehnten Anmeldung (z.B. + falsches Passwort, eigene Exception-Klasse in ldap3) -- sonst würde + ein einzelner Tippfehler beim Passwort einen AD-Kontosperren-Zähler + unnötig doppelt statt einmal hochzählen. factory ist ein parameterloser + Callable, das die Verbindung aufbaut (auto_bind=True) und zurückgibt.""" + try: + return factory() + except _LDAP_TRANSIENT_EXCEPTIONS: + return factory() + + def _ldap_connect_service(cfg): """Baut Server-Objekt + Service-Konto-Verbindung auf. Wirft nie — liefert (server, connection) oder (None, None) samt Fehlermeldung im @@ -1248,11 +1288,11 @@ def _ldap_connect_service(cfg): tls = None if cfg["use_ssl"] and cfg.get("tls_skip_verify"): tls = Tls(validate=ssl.CERT_NONE) - server = Server(cfg["server"], port=port, use_ssl=cfg["use_ssl"], tls=tls, get_info=ALL, connect_timeout=5) - conn = Connection( + server = Server(cfg["server"], port=port, use_ssl=cfg["use_ssl"], tls=tls, get_info=ALL, connect_timeout=10) + conn = _ldap_connect_with_retry(lambda: Connection( server, user=cfg["bind_dn"], password=bind_password, - authentication=SIMPLE, auto_bind=True, receive_timeout=5, - ) + authentication=SIMPLE, auto_bind=True, receive_timeout=10, + )) return (server, conn), None except LDAPException as e: return None, str(e) @@ -1563,10 +1603,10 @@ def _ldap_authenticate(username, password): pass try: - user_conn = Connection( + user_conn = _ldap_connect_with_retry(lambda: Connection( server, user=user_dn, password=password, - authentication=SIMPLE, auto_bind=True, receive_timeout=5, - ) + authentication=SIMPLE, auto_bind=True, receive_timeout=10, + )) user_conn.unbind() except LDAPException: return False, None