From 6ec9e5fd56967d6b3219bf24ebe21c94516ee7a5 Mon Sep 17 00:00:00 2001 From: alientim Date: Sat, 22 Aug 2026 09:31:24 +0200 Subject: [PATCH] Gitea #2: Papierkorb bekommt eigenes Recht (papierkorb.view) can_view_papierkorb leitete Sichtbarkeit bisher aus IRGENDEINEM der fuenf Ressourcen-Leserechte (devices/switches/credentials/users/groups .view) ab -- da die Standardgruppe 'Benutzer' bereits devices.view und switches.view hat, konnte praktisch jeder Benutzer den Papierkorb sehen (gemeldet: 'Papierkorb ist fuer alle sichtbar -> Rechtesystem'). Papierkorb hat jetzt ein eigenes, dediziertes Recht (papierkorb.view, neuer Eintrag unter PERMISSIONS/devices_group/children, automatisch im Gruppen-Editor sichtbar). Innerhalb der Seite bleibt jede Kachel zusaetzlich einzeln je Ressource gated wie bisher -- papierkorb.view schaltet nur die Seite selbst frei, keine automatischen Zusatzrechte. Die Standardgruppe bekommt das neue Recht NICHT automatisch (das ist der Fix) -- Admins behalten per is_admin-Bypass weiterhin vollen Zugriff. Live auf POETEST verifiziert: ein Testnutzer mit exakt den Standardgruppen-Rechten bekam vorher 200 auf /papierkorb, jetzt 302 (Zugriff verweigert), 'Papierkorb' verschwindet aus der Navigation, /devices bleibt unveraendert erreichbar. Gitea #1: LDAP-Login-Retry bei Domain-Controller in anderem Netzsegment Gemeldet: bei einem DC in einem anderen (langsameren) Netzsegment war ein doppelter Login noetig -- der erste Versuch scheiterte mit 'Benutzer/Passwort ungueltig', obwohl die Zugangsdaten korrekt waren. Ursache: sowohl der Service- als auch der User-Bind nutzten ein hartes 5s-Timeout (connect_timeout/receive_timeout) ohne jeden Retry -- eine erste, 'kalte' Verbindung zu einem entfernten/langsameren Netzsegment kann das leicht ueberschreiten (ARP-/Routing-Aufwaermen, TCP-Handshake, initiale Info-Abfrage), waehrend ein sofortiger zweiter Versuch dann klappt, weil der Pfad bereits 'warm' ist. Timeouts auf 10s angehoben und ein gezielter, genau EINMALIGER automatischer Retry ergaenzt (_ldap_connect_with_retry) -- greift NUR bei einem rein netzwerk-/timeout-bedingten Fehler (LDAPSocketOpenError/ LDAPSocketReceiveError/LDAPSocketSendError/LDAPResponseTimeoutError), NICHT bei einer tatsaechlich abgelehnten Anmeldung (z.B. falsches Passwort) -- ein Tippfehler soll keinen AD-Kontosperren-Zaehler unnoetig doppelt hochzaehlen. Isoliert unit-getestet (transienter Fehler -> 1 Retry -> Erfolg; echte Ablehnung -> kein Retry, sofortige Exception) und auf POETEST regressionsgetestet (LDAP dort weiterhin aktiviert und Dienst startet fehlerfrei). Zusaetzlich: /login aktiv gegen SQL-Injection/Login-Bypass/SSTI-Payloads getestet (13 Payload-Varianten inkl. UNION SELECT, DROP TABLE, Jinja- SSTI) -- keine Auffaelligkeiten, durchgehend parametrisierte Queries bestaetigt, users-Tabelle unveraendert. --- srv/tesm/app.py | 70 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 15 deletions(-) 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