Compare commits

..
2 Commits
Author SHA1 Message Date
alientimandClaude Sonnet 5 8577cd24cd Export-Kategorien: kuerzere Namen, Auditlog/Benutzer-Hinweise als Icon (weiterhin v1.1.11)
Sichtbare Checkbox-Namen im Export-Formular verkuerzt (nur die Kachel-
Ansicht betroffen, Funktionalitaet/Schluessel unveraendert):
Custom-Gruppen -> Gruppen, LDAP/AD-Einstellungen -> LDAP/AD,
DHCP-Einstellungen -> DHCP, NGINX-Einstellungen -> NGINX,
Benutzer (lokal) -> Benutzer.

Neue Hinweis-Icons (Klammerzusaetze wandern konsistent zu den bereits
bestehenden bei Zugangsdaten/NGINX ins "i"-Icon statt in den Namen):
- Auditlog: Hinweis, dass nur die aktuell in der DB stehenden Eintraege
  exportiert werden (bereits archivierte Tage liegen als eigene Dateien
  vor).
- Benutzer: Hinweis, dass AD/LDAP-Benutzer NICHT gesichert werden (nur
  lokale Konten sind Teil des Exports -- AD-Konten legen sich beim
  naechsten Login ohnehin automatisch neu an).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 22:35:49 +02:00
alientimandClaude Sonnet 5 877903bd77 NGINX-Export inkl. Zertifikat/Schluessel, Kategorie-Hinweise als Icon (weiterhin v1.1.11)
- NGINX-Export nimmt jetzt zusaetzlich das aktuell hinterlegte TLS-
  Zertifikat und den privaten Schluessel selbst mit (Base64, falls
  vorhanden) -- der gesamte Export ist ohnehin passphrasenverschluesselt,
  genau wie andere Geheimnisse (Zugangsdaten-Passwoerter, LDAP-Bind-
  Passwort) bereits im Klartext darin landen. Bewusst weiterhin
  AUSGESCHLOSSEN bleibt jeglicher Let's-Encrypt-/certbot-Zustand
  (ssl_source, ACME-Konto, Renewal-Timer) -- an genau diesen Host/diese
  Domain-Registrierung gebunden, kann nicht mitziehen. Ein Import setzt
  ssl_source deshalb immer auf "upload", auch wenn urspruenglich per Let's
  Encrypt ausgestellt.
- Die erklaerenden Klammerzusaetze bei "Zugangsdaten" (know_hosts-Hinweis)
  und "NGINX-Einstellungen" (Zertifikat/Schluessel-Hinweis) sind jetzt ein
  "i"-Hinweis-Icon statt Fliesstext im Label, passend zum bestehenden
  Hover-Tooltip-/Klick-Modal-Muster (siehe v1.1.10).

Verifiziert per isoliertem Test mit einem echten (selbstsignierten)
Test-Zertifikat: Export->Import-Roundtrip liefert bitidentische Cert-/
Key-Dateien mit korrekten Berechtigungen (0600 Schluessel, 0644
Zertifikat) auf einem eigenen Zielpfad, sowie live per Browser gegen
POETEST (Hover zeigt korrekten Tooltip-Text, 2 Icons im Export-Formular).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 22:33:31 +02:00
2 changed files with 68 additions and 16 deletions
+67 -16
View File
@@ -4224,6 +4224,7 @@ def settings_import_export():
"settings_import_export.html", "settings_import_export.html",
export_sections=EXPORT_SECTIONS, export_sections=EXPORT_SECTIONS,
admin_only_sections=IMPORT_EXPORT_ADMIN_ONLY_SECTIONS, admin_only_sections=IMPORT_EXPORT_ADMIN_ONLY_SECTIONS,
export_section_hints=EXPORT_SECTION_HINTS,
) )
@@ -5435,16 +5436,26 @@ def _merge_known_hosts_content(new_content):
EXPORT_SECTIONS = [ EXPORT_SECTIONS = [
("devices", "Clients"), ("devices", "Clients"),
("switches", "Switche"), ("switches", "Switche"),
("credentials", "Zugangsdaten (inkl. bekannter SSH-Host-Keys aus known_hosts)"), ("credentials", "Zugangsdaten"),
("users", "Benutzer (lokal)"), ("users", "Benutzer"),
("groups", "Custom-Gruppen"), ("groups", "Gruppen"),
("ldap", "LDAP/AD-Einstellungen"), ("ldap", "LDAP/AD"),
("dhcp", "DHCP-Einstellungen"), ("dhcp", "DHCP"),
("nginx", "NGINX-Einstellungen (Domain, Ports, SSL/HSTS — ohne Zertifikat/Schlüssel)"), ("nginx", "NGINX"),
("logs", "Auditlog (alle aktuell in der Datenbank vorhandenen Einträge)"), ("logs", "Auditlog"),
] ]
EXPORT_SECTION_LABELS = dict(EXPORT_SECTIONS) EXPORT_SECTION_LABELS = dict(EXPORT_SECTIONS)
# Zusätzlicher Hinweistext je Kategorie fürs "i"-Icon neben dem Label
# (siehe _hint_icon.html) -- nur für Kategorien mit erklärungsbedürftigem
# 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.",
"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).",
}
IMPORT_EXPORT_ADMIN_ONLY_SECTIONS = {"users", "groups", "ldap"} IMPORT_EXPORT_ADMIN_ONLY_SECTIONS = {"users", "groups", "ldap"}
LDAP_SETTING_KEYS = [ LDAP_SETTING_KEYS = [
@@ -5580,17 +5591,31 @@ NGINX_SETTING_KEYS = ["nginx_server_name", "nginx_http_port", "nginx_https_port"
def _export_nginx(conn): def _export_nginx(conn):
"""Exportiert nur portable Konfiguration (Domain, Ports, SSL/HSTS- """Exportiert Domain/Ports/SSL-HSTS-Schalter sowie, falls vorhanden,
Schalter) -- bewusst OHNE ssl_source, TLS-Zertifikat/Schlüssel oder das aktuell hinterlegte TLS-Zertifikat+Schlüssel selbst (Base64, da
Let's-Encrypt-/certbot-Zustand: das sind entweder host-gebundene reale JSON keine Binärdaten kann) -- anders als bei avatar_filename ist das
Dateien (TESM_SSL_CERT_PATH/TESM_SSL_KEY_PATH) oder eine ACME- hier sinnvoll: Zertifikat/Schlüssel liegen bereits vollständig auf
Registrierung für genau diesen Host/diese Domain, kein portabler diesem Host vor und lassen sich 1:1 auf ein Zielsystem übertragen,
Einstellungswert (gleiche Begründung wie bei avatar_filename in genau wie andere Geheimnisse (Zugangsdaten-Passwörter, LDAP-Bind-
_export_users()).""" Passwort), die ebenfalls im Klartext in den -- als Ganzes
passphrasenverschlüsselten -- Export wandern. Bewusst AUSGESCHLOSSEN
bleibt trotzdem jeglicher Let's-Encrypt-/certbot-Zustand (ssl_source,
ACME-Konto, Renewal-Timer): das ist an genau diesen Host und dessen
Domain-Registrierung gebunden und kann nicht mitziehen -- ein Import
setzt ssl_source deshalb immer auf "upload", siehe _import_nginx()."""
settings_data = {k: get_setting(k) for k in NGINX_SETTING_KEYS if get_setting(k) not in (None, "")} settings_data = {k: get_setting(k) for k in NGINX_SETTING_KEYS if get_setting(k) not in (None, "")}
if not settings_data: data = {"settings": settings_data}
try:
if os.path.isfile(TESM_SSL_CERT_PATH) and os.path.isfile(TESM_SSL_KEY_PATH):
with open(TESM_SSL_CERT_PATH, "rb") as f:
data["cert_pem"] = base64.b64encode(f.read()).decode("ascii")
with open(TESM_SSL_KEY_PATH, "rb") as f:
data["key_pem"] = base64.b64encode(f.read()).decode("ascii")
except OSError:
pass
if not (settings_data or "cert_pem" in data):
return None return None
return {"settings": settings_data} return data
def _export_logs(conn): def _export_logs(conn):
@@ -5835,6 +5860,31 @@ def _import_nginx(conn, data):
(key, value), (key, value),
) )
n += 1 n += 1
if data.get("cert_pem") and data.get("key_pem"):
try:
cert_bytes = base64.b64decode(data["cert_pem"])
key_bytes = base64.b64decode(data["key_pem"])
ok, _msg, _info = _validate_cert_key_pair(cert_bytes, key_bytes)
if ok:
os.makedirs(TESM_SSL_DIR, exist_ok=True)
with open(TESM_SSL_CERT_PATH, "wb") as f:
f.write(cert_bytes)
with open(TESM_SSL_KEY_PATH, "wb") as f:
f.write(key_bytes)
os.chmod(TESM_SSL_KEY_PATH, 0o600)
os.chmod(TESM_SSL_CERT_PATH, 0o644)
# ACME-/certbot-Zustand kommt bewusst nie mit (siehe
# _export_nginx) -- nach dem Import ist es technisch nur
# noch ein hochgeladenes Zertifikat, keine automatische
# Verlängerung mehr, auch wenn es ursprünglich per Let's
# Encrypt ausgestellt wurde.
conn.execute(
"INSERT INTO settings (key, value) VALUES ('ssl_source', 'upload') "
"ON CONFLICT(key) DO UPDATE SET value=excluded.value"
)
n += 1
except (OSError, ValueError, TypeError):
pass
return n return n
@@ -5983,6 +6033,7 @@ def settings_import_preview(token):
import_preview={"token": token, "sections": preview_sections}, import_preview={"token": token, "sections": preview_sections},
export_sections=EXPORT_SECTIONS, export_sections=EXPORT_SECTIONS,
admin_only_sections=IMPORT_EXPORT_ADMIN_ONLY_SECTIONS, admin_only_sections=IMPORT_EXPORT_ADMIN_ONLY_SECTIONS,
export_section_hints=EXPORT_SECTION_HINTS,
) )
@@ -82,6 +82,7 @@
{% if key not in admin_only_sections or current_user.is_admin %}checked{% endif %} {% if key not in admin_only_sections or current_user.is_admin %}checked{% endif %}
{% if key in admin_only_sections and not current_user.is_admin %}disabled{% endif %}> {% if key in admin_only_sections and not current_user.is_admin %}disabled{% endif %}>
{{ label }} {{ 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 %} {% if key in admin_only_sections %}<span class="pill user" style="font-size:10px; padding:2px 7px;">Nur Admin</span>{% endif %}
</label> </label>
{% endfor %} {% endfor %}