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.
This commit is contained in:
2026-08-22 09:31:24 +02:00
parent 33753e4f41
commit 6ec9e5fd56
+55 -15
View File
@@ -23,7 +23,13 @@ import paramiko
import yaml import yaml
import ssl import ssl
from ldap3 import Server, Connection, Tls, ALL, SIMPLE, BASE 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 from ldap3.utils.conv import escape_filter_chars
LDAP_MATCHING_RULE_IN_CHAIN = "1.2.840.113556.1.4.1941" LDAP_MATCHING_RULE_IN_CHAIN = "1.2.840.113556.1.4.1941"
@@ -196,6 +202,12 @@ PERMISSIONS = {
"edit": "maintenance.edit", "edit": "maintenance.edit",
}, },
}, },
"papierkorb": {
"label": "Papierkorb",
"rows": {
"view": "papierkorb.view",
},
},
}, },
}, },
"logs_group": { "logs_group": {
@@ -264,6 +276,7 @@ PERMISSION_LABELS = {
"credentials.edit": "Zugangsdaten ändern (inkl. Löschen)", "credentials.edit": "Zugangsdaten ändern (inkl. Löschen)",
"maintenance.view": "Wartungsseite ansehen (Geräte-Status, Job-Verlauf)", "maintenance.view": "Wartungsseite ansehen (Geräte-Status, Job-Verlauf)",
"maintenance.edit": "Wartungsaktionen ausführen (SSH-Update, SSH-Neustart)", "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_live.view": "Live-Log lesen", "logs_activity.view": "Änderungslog lesen",
"logs_kea.view": "Kea-DHCP-Log lesen", "logs_kea.view": "Kea-DHCP-Log lesen",
"users.view": "Benutzer lesen", "users.create": "Benutzer anlegen", "users.view": "Benutzer lesen", "users.create": "Benutzer anlegen",
@@ -516,13 +529,14 @@ class User(UserMixin):
@property @property
def can_view_papierkorb(self): def can_view_papierkorb(self):
"""Papierkorb-Seite: sichtbar sobald mind. eine der fünf dort """Papierkorb-Seite: eigenes, dediziertes Recht (papierkorb.view) --
gelisteten Ressourcen (Geräte/Switche/Zugangsdaten/Benutzer/Gruppen) vorher implizit sichtbar für jeden mit irgendeiner der fünf
View-Recht hat jede Kachel darin ist ohnehin einzeln je Ressource Ressourcen-Leserechte, wodurch praktisch jeder Benutzer den
gated (siehe papierkorb.html).""" Papierkorb sehen konnte (Standardgruppe hat bereits devices.view).
return self.has_any_permission( Innerhalb der Seite ist jede Kachel zusätzlich einzeln je Ressource
["devices.view", "switches.view", "credentials.view", "users.view", "groups.view"] 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 @property
def can_view_live_log(self): 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): def _ldap_connect_service(cfg):
"""Baut Server-Objekt + Service-Konto-Verbindung auf. Wirft nie — """Baut Server-Objekt + Service-Konto-Verbindung auf. Wirft nie —
liefert (server, connection) oder (None, None) samt Fehlermeldung im liefert (server, connection) oder (None, None) samt Fehlermeldung im
@@ -1248,11 +1288,11 @@ def _ldap_connect_service(cfg):
tls = None tls = None
if cfg["use_ssl"] and cfg.get("tls_skip_verify"): if cfg["use_ssl"] and cfg.get("tls_skip_verify"):
tls = Tls(validate=ssl.CERT_NONE) 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) server = Server(cfg["server"], port=port, use_ssl=cfg["use_ssl"], tls=tls, get_info=ALL, connect_timeout=10)
conn = Connection( conn = _ldap_connect_with_retry(lambda: Connection(
server, user=cfg["bind_dn"], password=bind_password, 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 return (server, conn), None
except LDAPException as e: except LDAPException as e:
return None, str(e) return None, str(e)
@@ -1563,10 +1603,10 @@ def _ldap_authenticate(username, password):
pass pass
try: try:
user_conn = Connection( user_conn = _ldap_connect_with_retry(lambda: Connection(
server, user=user_dn, password=password, 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() user_conn.unbind()
except LDAPException: except LDAPException:
return False, None return False, None