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:
+55
-15
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user