LDAP-Export: Sperrstatus + individuelle Gruppenzuweisung von AD-Nutzern (weiterhin v1.1.11)
_export_users()/_import_users() schliessen AD/LDAP-Konten bewusst komplett aus (sie werden ueber Active Directory verwaltet). Das hatte aber eine Luecke: weder eine manuelle Sperrung/Entsperrung noch eine manuell (per "Gruppe zuweisen") vergebene individuelle Gruppenmitgliedschaft eines AD-Benutzers wurden bisher irgendwo gesichert -- bei einem Umzug auf ein neues System (der eigentliche Zweck dieser Seite) waere ein zuvor gesperrtes AD-Konto dort wieder entsperrt gewesen, und individuell vergebene Rechte waeren beim naechsten Login des Nutzers verloren gegangen. Neu in der "ldap"-Kategorie: ad_user_states exportiert fuer jeden bereits bekannten AD-Benutzer Sperrstatus + aktuelle Gruppenmitgliedschaft (inkl. Admin als Sentinel, analog den bestehenden AD-Gruppenzuordnungen). Beim Import wird ein bereits vorhandenes AD-Konto entsprechend aktualisiert; existiert es auf dem Zielsystem noch nicht (Nutzer hat sich dort noch nie eingeloggt), wird ein Platzhalter-Konto mit auth_source='ldap' und korrektem Sperrstatus VORAB angelegt -- der Login-Pfad prueft is_locked bereits vor der eigentlichen AD-Authentifizierung, ein vorab gesperrtes Konto bleibt also gesperrt. Gruppenzuweisungen sind rein additiv (insert or ignore), die naechste automatische AD-Gruppenzuordnung beim Login nimmt nichts davon wieder weg. Ausserdem: - Die pauschalen "Was exportieren?"/"Was importieren?"-Hinweise entfernt (waren durch die praeziseren Kategorie-Hinweise bei Benutzer/LDAP/ Zugangsdaten/NGINX/Auditlog laengst ueberholt und teils sachlich veraltet). - Kategorie-Hinweise jetzt auch in der Import-Vorschau sichtbar (vorher nur beim Export). - "Nur Admin"-Kennzeichnung aus beiden Kategorie-Listen entfernt (rein optisch, die serverseitige Rechtepruefung bleibt unveraendert). Verifiziert per isoliertem Roundtrip-Test: gesperrter AD-Nutzer bleibt nach Reset+Import gesperrt, manuell zugewiesene Gruppe eines AD-Nutzers uebersteht Loeschen+Import, und ein komplett neuer (nie eingeloggter) AD-Nutzer wird korrekt mit Sperrstatus/Gruppen/Admin-Flag vorangelegt -- inkl. Bestaetigung, dass der echte Login-Sperr-Check dieses vorab angelegte Konto tatsaechlich blockiert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+58
-3
@@ -5451,6 +5451,7 @@ EXPORT_SECTION_LABELS = dict(EXPORT_SECTIONS)
|
||||
# Zusatzverhalten, das über den reinen Namen hinausgeht.
|
||||
EXPORT_SECTION_HINTS = {
|
||||
"users": "Nur lokale Konten. AD/LDAP-Benutzer werden NICHT gesichert — sie werden über Active Directory verwaltet, nicht über diese App, und legen sich beim nächsten Login automatisch wieder an.",
|
||||
"ldap": "Gesichert werden Server-/Bind-Einstellungen, die automatischen AD-Gruppen- und Fileshare-Zuordnungen sowie für bereits bekannte AD-Benutzer deren Sperrstatus und individuelle Gruppenzuweisungen — damit ein gesperrtes AD-Konto auf dem Zielsystem gesperrt bleibt und manuell vergebene Rechte erhalten bleiben, statt beim nächsten Login verloren zu gehen.",
|
||||
"credentials": "Enthält zusätzlich den Inhalt von known_hosts (bereits bestätigte SSH-Host-Keys) — wird beim Import ergänzend (nicht überschreibend) eingespielt.",
|
||||
"nginx": "Enthält Domain, Ports und SSL/HSTS-Schalter sowie — falls vorhanden — das aktuell hinterlegte TLS-Zertifikat und den privaten Schlüssel selbst.",
|
||||
"logs": "Alle aktuell in der Datenbank vorhandenen Einträge (bereits archivierte Auditlog-Tage liegen als eigene Dateien unter Verlauf und sind kein Teil dieses Exports).",
|
||||
@@ -5537,9 +5538,18 @@ def _export_ldap(conn):
|
||||
(frische Installation vergibt Gruppen-IDs neu). Exportiert wird daher
|
||||
der Gruppenname (analog _export_switches()' credential_name), _import_ldap
|
||||
löst ihn auf dem Zielsystem wieder zur dortigen ID auf -- gleiches
|
||||
"admin"/g.name-Mapping wie in _ldap_group_mappings() weiter oben."""
|
||||
"admin"/g.name-Mapping wie in _ldap_group_mappings() weiter oben.
|
||||
|
||||
ad_user_states: Sperrstatus + aktuelle Gruppenzugehörigkeit (inkl.
|
||||
Admin-Sentinel) je bereits bekanntem AD-Benutzer -- bewusst hier bei
|
||||
LDAP statt bei "users" (das AD-Konten komplett ausschließt, siehe
|
||||
_export_users()), da es explizit um AD-Konten geht: ein gesperrtes
|
||||
AD-Konto soll auf dem Zielsystem gesperrt bleiben (nicht durch die
|
||||
Migration wieder hereinkommen), und individuell/manuell vergebene
|
||||
Rechte sollen erhalten bleiben statt beim nächsten Login verloren zu
|
||||
gehen."""
|
||||
settings_data = {k: get_setting(k) for k in LDAP_SETTING_KEYS if get_setting(k) not in (None, "")}
|
||||
data = {"settings": settings_data, "group_mappings": [], "fileshare_mappings": []}
|
||||
data = {"settings": settings_data, "group_mappings": [], "fileshare_mappings": [], "ad_user_states": []}
|
||||
bind = conn.execute(
|
||||
"SELECT username, password FROM service_accounts WHERE purpose=?", (LDAP_BIND_SERVICE_ACCOUNT_PURPOSE,)
|
||||
).fetchone()
|
||||
@@ -5554,7 +5564,19 @@ def _export_ldap(conn):
|
||||
data["fileshare_mappings"] = [dict(m) for m in conn.execute(
|
||||
"SELECT ad_group_dn, ad_group_name, share_label, share_unc FROM ldap_fileshare_mappings"
|
||||
).fetchall()]
|
||||
if not (settings_data or "bind_username" in data or data["group_mappings"] or data["fileshare_mappings"]):
|
||||
ad_users = conn.execute(
|
||||
"SELECT id, username, is_admin, is_locked FROM users WHERE auth_source='ldap' AND deleted_at IS NULL"
|
||||
).fetchall()
|
||||
for u in ad_users:
|
||||
groups = [g["name"] for g in conn.execute(
|
||||
"SELECT g.name FROM user_groups ug JOIN groups g ON g.id=ug.group_id "
|
||||
"WHERE ug.user_id=? AND g.deleted_at IS NULL", (u["id"],)
|
||||
).fetchall()]
|
||||
if u["is_admin"]:
|
||||
groups.append("Admin")
|
||||
data["ad_user_states"].append({"username": u["username"], "is_locked": u["is_locked"], "groups": groups})
|
||||
if not (settings_data or "bind_username" in data or data["group_mappings"] or data["fileshare_mappings"]
|
||||
or data["ad_user_states"]):
|
||||
return None
|
||||
return data
|
||||
|
||||
@@ -5800,6 +5822,39 @@ def _import_ldap(conn, data):
|
||||
(m["ad_group_dn"], m["ad_group_name"], m["share_label"], m["share_unc"]),
|
||||
)
|
||||
n += 1
|
||||
for u in data.get("ad_user_states", []):
|
||||
groups = u.get("groups", [])
|
||||
is_admin = 1 if "Admin" in groups else 0
|
||||
existing = conn.execute(
|
||||
"SELECT id FROM users WHERE username=? AND auth_source='ldap'", (u["username"],)
|
||||
).fetchone()
|
||||
if existing:
|
||||
user_id = existing["id"]
|
||||
conn.execute(
|
||||
"UPDATE users SET is_locked=?, is_admin=? WHERE id=?",
|
||||
(u.get("is_locked", 0), is_admin, user_id),
|
||||
)
|
||||
else:
|
||||
# Konto hat sich auf dem Zielsystem noch nie eingeloggt -- ein
|
||||
# Platzhalter-Passwort-Hash wird nie geprüft (der Login-Zweig
|
||||
# für auth_source='ldap' authentifiziert immer gegen AD, siehe
|
||||
# login()), er füllt nur das NOT-NULL-Feld. Wichtig ist einzig,
|
||||
# dass is_locked schon VOR dem ersten Login greift.
|
||||
placeholder_hash = bcrypt.generate_password_hash(secrets.token_hex(32)).decode("utf-8")
|
||||
cur = conn.execute(
|
||||
"INSERT INTO users (username, password, is_admin, auth_source, is_locked) VALUES (?, ?, ?, 'ldap', ?)",
|
||||
(u["username"], placeholder_hash, is_admin, u.get("is_locked", 0)),
|
||||
)
|
||||
user_id = cur.lastrowid
|
||||
for gname in groups:
|
||||
if gname == "Admin":
|
||||
continue
|
||||
grow = conn.execute(
|
||||
"SELECT id FROM groups WHERE name=? AND deleted_at IS NULL", (gname,)
|
||||
).fetchone()
|
||||
if grow:
|
||||
conn.execute("INSERT OR IGNORE INTO user_groups (user_id, group_id) VALUES (?, ?)", (user_id, grow["id"]))
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
data-confirm="Ausgewählte Kategorien wirklich importieren? Bestehende Einträge mit gleichem Namen/Hostname/MAC/Benutzernamen werden überschrieben.">
|
||||
<input type="hidden" name="import_token" value="{{ import_preview.token }}">
|
||||
<div class="field">
|
||||
<label>Was importieren? {{ hi.hint_icon("AD/LDAP-Benutzerkonten sind hiervon unberührt — sie werden über Active Directory/Windows verwaltet, nicht über diese App, und beim nächsten Login automatisch neu angelegt.", "Was importieren?") }}</label>
|
||||
<label>Was importieren?</label>
|
||||
<div class="check-list">
|
||||
{% for s in import_preview.sections %}
|
||||
<label class="check-row">
|
||||
@@ -35,7 +35,7 @@
|
||||
{% if s.key not in admin_only_sections or current_user.is_admin %}checked{% endif %}
|
||||
{% if s.admin_only and not current_user.is_admin %}disabled{% endif %}>
|
||||
{{ s.label }} <span class="text-faint">({{ s.count }})</span>
|
||||
{% if s.admin_only %}<span class="pill user" style="font-size:10px; padding:2px 7px;">Nur Admin</span>{% endif %}
|
||||
{% if export_section_hints and s.key in export_section_hints %}{{ hi.hint_icon(export_section_hints[s.key], s.label) }}{% endif %}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -74,7 +74,7 @@
|
||||
{% if current_user.has_permission('settings_importexport.export') %}
|
||||
<form method="post" action="{{ url_for('export_data') }}">
|
||||
<div class="field">
|
||||
<label>Was exportieren? {{ hi.hint_icon("AD/LDAP-Benutzerkonten werden nie mitexportiert — sie werden über Active Directory/Windows verwaltet (nicht über diese App) und legen sich beim nächsten Login automatisch wieder an. Beim LDAP-Export wird lediglich das Bind-Konto (verschlüsselt) sowie die Gruppenzuordnungen gesichert.", "Was exportieren?") }}</label>
|
||||
<label>Was exportieren?</label>
|
||||
<div class="check-list">
|
||||
{% for key, label in export_sections %}
|
||||
<label class="check-row">
|
||||
@@ -83,7 +83,6 @@
|
||||
{% if key in admin_only_sections and not current_user.is_admin %}disabled{% endif %}>
|
||||
{{ label }}
|
||||
{% if export_section_hints and key in export_section_hints %}{{ hi.hint_icon(export_section_hints[key], label) }}{% endif %}
|
||||
{% if key in admin_only_sections %}<span class="pill user" style="font-size:10px; padding:2px 7px;">Nur Admin</span>{% endif %}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user