diff --git a/srv/tesm/app.py b/srv/tesm/app.py index d00c517..1094bd9 100644 --- a/srv/tesm/app.py +++ b/srv/tesm/app.py @@ -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 diff --git a/srv/tesm/templates/settings_import_export.html b/srv/tesm/templates/settings_import_export.html index 47ae753..6685821 100644 --- a/srv/tesm/templates/settings_import_export.html +++ b/srv/tesm/templates/settings_import_export.html @@ -27,7 +27,7 @@ data-confirm="Ausgewählte Kategorien wirklich importieren? Bestehende Einträge mit gleichem Namen/Hostname/MAC/Benutzernamen werden überschrieben.">
- +
{% for s in import_preview.sections %} {% endfor %}
@@ -74,7 +74,7 @@ {% if current_user.has_permission('settings_importexport.export') %}
- +
{% for key, label in export_sections %} {% endfor %}