Compare commits
1
Commits
v1.1.10
...
2b05d012ee
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b05d012ee |
+1
-1
@@ -1 +1 @@
|
|||||||
1.1.10
|
1.1.11
|
||||||
|
|||||||
+171
-32
@@ -5392,47 +5392,99 @@ def save_nav_order():
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _read_known_hosts_content():
|
||||||
|
"""Für den Export: der Inhalt von known_hosts gehört inhaltlich zu den
|
||||||
|
Zugangsdaten (jeder per SSH bereits bestätigte Host-Key eines Geräts/
|
||||||
|
Switches, das über gespeicherte Zugangsdaten erreicht wurde) -- landet
|
||||||
|
daher NICHT als eigene EXPORT_BUILDERS-Kategorie, sondern als
|
||||||
|
zusätzlicher Payload-Schlüssel "known_hosts", sobald "credentials"
|
||||||
|
mitexportiert wird (siehe export_data())."""
|
||||||
|
try:
|
||||||
|
with open(SSH_KNOWN_HOSTS_PATH, "r", encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
return content if content.strip() else None
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_known_hosts_content(new_content):
|
||||||
|
"""Für den Import: ergänzt die eigene known_hosts-Datei um die im
|
||||||
|
Export enthaltenen Zeilen -- bewusst ergänzend statt überschreibend
|
||||||
|
(die Zielumgebung kann bereits eigene, unabhängig bestätigte Host-Keys
|
||||||
|
haben) und mit Deduplizierung nach vollständiger Zeile, damit ein
|
||||||
|
wiederholter Import derselben Exportdatei nicht bei jedem Mal weitere
|
||||||
|
Duplikate anhäuft. Gibt die Anzahl tatsächlich neu hinzugefügter
|
||||||
|
Zeilen zurück."""
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
with open(SSH_KNOWN_HOSTS_PATH, "r", encoding="utf-8") as f:
|
||||||
|
existing_lines = set(f.read().splitlines())
|
||||||
|
except OSError:
|
||||||
|
existing_lines = set()
|
||||||
|
new_lines = [ln for ln in new_content.splitlines() if ln.strip() and ln not in existing_lines]
|
||||||
|
if not new_lines:
|
||||||
|
return 0
|
||||||
|
os.makedirs(os.path.dirname(SSH_KNOWN_HOSTS_PATH) or ".", exist_ok=True)
|
||||||
|
with open(SSH_KNOWN_HOSTS_PATH, "a", encoding="utf-8") as f:
|
||||||
|
f.write("\n".join(new_lines) + "\n")
|
||||||
|
return len(new_lines)
|
||||||
|
except OSError:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
EXPORT_SECTIONS = [
|
EXPORT_SECTIONS = [
|
||||||
("devices", "Clients"),
|
("devices", "Clients"),
|
||||||
("switches", "Switche"),
|
("switches", "Switche"),
|
||||||
("credentials", "Zugangsdaten"),
|
("credentials", "Zugangsdaten (inkl. bekannter SSH-Host-Keys aus known_hosts)"),
|
||||||
("users", "Benutzer (lokal)"),
|
("users", "Benutzer (lokal)"),
|
||||||
("groups", "Custom-Gruppen"),
|
("groups", "Custom-Gruppen"),
|
||||||
("ldap", "LDAP/AD-Einstellungen"),
|
("ldap", "LDAP/AD-Einstellungen"),
|
||||||
("dhcp", "DHCP-Einstellungen"),
|
("dhcp", "DHCP-Einstellungen"),
|
||||||
("logs", f"Logs (letzte {500} Einträge)"),
|
("nginx", "NGINX-Einstellungen (Domain, Ports, SSL/HSTS — ohne Zertifikat/Schlüssel)"),
|
||||||
|
("logs", "Auditlog (alle aktuell in der Datenbank vorhandenen Einträge)"),
|
||||||
]
|
]
|
||||||
EXPORT_SECTION_LABELS = dict(EXPORT_SECTIONS)
|
EXPORT_SECTION_LABELS = dict(EXPORT_SECTIONS)
|
||||||
|
|
||||||
IMPORT_EXPORT_ADMIN_ONLY_SECTIONS = {"users", "groups", "ldap"}
|
IMPORT_EXPORT_ADMIN_ONLY_SECTIONS = {"users", "groups", "ldap"}
|
||||||
|
|
||||||
LOG_EXPORT_LIMIT = 500
|
|
||||||
LDAP_SETTING_KEYS = [
|
LDAP_SETTING_KEYS = [
|
||||||
"ldap_enabled", "ldap_server", "ldap_port", "ldap_use_ssl", "ldap_tls_skip_verify",
|
"ldap_enabled", "ldap_server", "ldap_port", "ldap_use_ssl", "ldap_tls_skip_verify",
|
||||||
"ldap_base_dn", "ldap_user_filter_attr", "ldap_default_group",
|
"ldap_base_dn", "ldap_user_filter_attr", "ldap_default_group", "ldap_required_login_group",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def _export_devices(conn):
|
def _export_devices(conn):
|
||||||
return [dict(r) for r in conn.execute(
|
return [dict(r) for r in conn.execute("""
|
||||||
"SELECT mac, ip, port, name, switch_hostname, is_active FROM devices WHERE deleted_at IS NULL"
|
SELECT devices.mac, devices.ip, devices.port, devices.name, devices.switch_hostname,
|
||||||
).fetchall()]
|
devices.is_active, devices.ssh_port, credentials.name AS credential_name
|
||||||
|
FROM devices LEFT JOIN credentials ON credentials.id = devices.credential_id
|
||||||
|
WHERE devices.deleted_at IS NULL
|
||||||
|
""").fetchall()]
|
||||||
|
|
||||||
|
|
||||||
def _export_switches(conn):
|
def _export_switches(conn):
|
||||||
return [dict(r) for r in conn.execute("""
|
return [dict(r) for r in conn.execute("""
|
||||||
SELECT switches.hostname, switches.ip, credentials.name AS credential_name
|
SELECT switches.hostname, switches.ip, switches.ssh_port, credentials.name AS credential_name
|
||||||
FROM switches LEFT JOIN credentials ON credentials.id = switches.credential_id
|
FROM switches LEFT JOIN credentials ON credentials.id = switches.credential_id
|
||||||
WHERE switches.deleted_at IS NULL
|
WHERE switches.deleted_at IS NULL
|
||||||
""").fetchall()]
|
""").fetchall()]
|
||||||
|
|
||||||
|
|
||||||
def _export_credentials(conn):
|
def _export_credentials(conn):
|
||||||
rows = conn.execute("SELECT name, username, password FROM credentials WHERE deleted_at IS NULL").fetchall()
|
rows = conn.execute("SELECT name, username, password, category FROM credentials WHERE deleted_at IS NULL").fetchall()
|
||||||
return [{"name": r["name"], "username": r["username"], "password": decrypt_password(r["password"])} for r in rows]
|
return [
|
||||||
|
{"name": r["name"], "username": r["username"], "password": decrypt_password(r["password"]), "category": r["category"]}
|
||||||
|
for r in rows
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def _export_users(conn):
|
def _export_users(conn):
|
||||||
|
"""avatar_filename wird bewusst NICHT exportiert -- das ist nur ein
|
||||||
|
Dateiname, die eigentliche Bilddatei liegt unter static/uploads/avatars
|
||||||
|
auf DIESEM Host und würde auf dem Zielsystem nicht mitkommen; ein
|
||||||
|
exportierter Verweis auf eine dort nicht existierende Datei wäre
|
||||||
|
schlimmer als gar keiner (kaputtes <img>, siehe auch die analoge
|
||||||
|
Begründung bei _export_nginx() für TLS-Zertifikat/Schlüssel)."""
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"SELECT id, username, password, first_name, last_name, email, is_admin, is_locked "
|
"SELECT id, username, password, first_name, last_name, email, is_admin, is_locked "
|
||||||
"FROM users WHERE auth_source='local' AND deleted_at IS NULL"
|
"FROM users WHERE auth_source='local' AND deleted_at IS NULL"
|
||||||
@@ -5453,7 +5505,7 @@ def _export_users(conn):
|
|||||||
|
|
||||||
|
|
||||||
def _export_groups(conn):
|
def _export_groups(conn):
|
||||||
rows = conn.execute("SELECT id, name FROM groups WHERE is_system=0 AND deleted_at IS NULL").fetchall()
|
rows = conn.execute("SELECT id, name, is_default FROM groups WHERE is_system=0 AND deleted_at IS NULL").fetchall()
|
||||||
result = []
|
result = []
|
||||||
for g in rows:
|
for g in rows:
|
||||||
perms = [p["permission"] for p in conn.execute(
|
perms = [p["permission"] for p in conn.execute(
|
||||||
@@ -5464,23 +5516,34 @@ def _export_groups(conn):
|
|||||||
"WHERE ug.group_id=? AND u.deleted_at IS NULL",
|
"WHERE ug.group_id=? AND u.deleted_at IS NULL",
|
||||||
(g["id"],),
|
(g["id"],),
|
||||||
).fetchall()]
|
).fetchall()]
|
||||||
result.append({"name": g["name"], "permissions": perms, "members": members})
|
result.append({"name": g["name"], "is_default": g["is_default"], "permissions": perms, "members": members})
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _export_ldap(conn):
|
def _export_ldap(conn):
|
||||||
|
"""app_group_id ist eine rohe, lokale groups.id (oder der Sonderwert
|
||||||
|
"admin") -- über eine Umgebungsgrenze hinweg garantiert NICHT stabil
|
||||||
|
(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."""
|
||||||
settings_data = {k: get_setting(k) for k in LDAP_SETTING_KEYS if get_setting(k) not in (None, "")}
|
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": []}
|
data = {"settings": settings_data, "group_mappings": [], "fileshare_mappings": []}
|
||||||
bind = conn.execute(
|
bind = conn.execute(
|
||||||
"SELECT username, password FROM service_accounts WHERE purpose=?", (LDAP_BIND_SERVICE_ACCOUNT_PURPOSE,)
|
"SELECT username, password FROM service_accounts WHERE purpose=?", (LDAP_BIND_SERVICE_ACCOUNT_PURPOSE,)
|
||||||
).fetchone()
|
).fetchone()
|
||||||
if bind:
|
if bind:
|
||||||
data["bind_username"] = bind["username"]
|
data["bind_username"] = bind["username"]
|
||||||
data["bind_password"] = decrypt_password(bind["password"])
|
data["bind_password"] = decrypt_password(bind["password"])
|
||||||
data["group_mappings"] = [dict(m) for m in conn.execute(
|
data["group_mappings"] = [dict(m) for m in conn.execute("""
|
||||||
"SELECT ad_group_dn, ad_group_name, app_group_id FROM ldap_group_mappings"
|
SELECT m.ad_group_dn, m.ad_group_name,
|
||||||
|
CASE WHEN m.app_group_id = 'admin' THEN 'Admin' ELSE g.name END AS app_group_name
|
||||||
|
FROM ldap_group_mappings m LEFT JOIN groups g ON g.id = m.app_group_id AND g.deleted_at IS NULL
|
||||||
|
""").fetchall()]
|
||||||
|
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()]
|
).fetchall()]
|
||||||
if not (settings_data or "bind_username" in data or data["group_mappings"]):
|
if not (settings_data or "bind_username" in data or data["group_mappings"] or data["fileshare_mappings"]):
|
||||||
return None
|
return None
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@@ -5513,9 +5576,31 @@ def _export_dhcp(conn):
|
|||||||
return {"settings": settings_data, "subnets": subnets, "reservations": reservations, "options": options}
|
return {"settings": settings_data, "subnets": subnets, "reservations": reservations, "options": options}
|
||||||
|
|
||||||
|
|
||||||
|
NGINX_SETTING_KEYS = ["nginx_server_name", "nginx_http_port", "nginx_https_port", "ssl_enabled", "hsts_enabled"]
|
||||||
|
|
||||||
|
|
||||||
|
def _export_nginx(conn):
|
||||||
|
"""Exportiert nur portable Konfiguration (Domain, Ports, SSL/HSTS-
|
||||||
|
Schalter) -- bewusst OHNE ssl_source, TLS-Zertifikat/Schlüssel oder
|
||||||
|
Let's-Encrypt-/certbot-Zustand: das sind entweder host-gebundene reale
|
||||||
|
Dateien (TESM_SSL_CERT_PATH/TESM_SSL_KEY_PATH) oder eine ACME-
|
||||||
|
Registrierung für genau diesen Host/diese Domain, kein portabler
|
||||||
|
Einstellungswert (gleiche Begründung wie bei avatar_filename in
|
||||||
|
_export_users())."""
|
||||||
|
settings_data = {k: get_setting(k) for k in NGINX_SETTING_KEYS if get_setting(k) not in (None, "")}
|
||||||
|
if not settings_data:
|
||||||
|
return None
|
||||||
|
return {"settings": settings_data}
|
||||||
|
|
||||||
|
|
||||||
def _export_logs(conn):
|
def _export_logs(conn):
|
||||||
|
"""Exportiert das komplette, aktuell in der DB stehende Auditlog --
|
||||||
|
bereits archivierte Tage (siehe _archive_old_audit_log_rows) liegen
|
||||||
|
als eigene Dateien unter AUDIT_ARCHIVE_DIR und sind bewusst NICHT Teil
|
||||||
|
dieses Exports, das Auditlog-Export-Formular exportiert nur die
|
||||||
|
Datenbank-Kategorien."""
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"SELECT ts, username, action, target, details FROM audit_log ORDER BY id DESC LIMIT ?", (LOG_EXPORT_LIMIT,)
|
"SELECT ts, username, action, target, details FROM audit_log ORDER BY id DESC"
|
||||||
).fetchall()
|
).fetchall()
|
||||||
return [dict(r) for r in rows] if rows else None
|
return [dict(r) for r in rows] if rows else None
|
||||||
|
|
||||||
@@ -5523,19 +5608,28 @@ def _export_logs(conn):
|
|||||||
EXPORT_BUILDERS = {
|
EXPORT_BUILDERS = {
|
||||||
"devices": _export_devices, "switches": _export_switches, "credentials": _export_credentials,
|
"devices": _export_devices, "switches": _export_switches, "credentials": _export_credentials,
|
||||||
"users": _export_users, "groups": _export_groups, "ldap": _export_ldap, "dhcp": _export_dhcp,
|
"users": _export_users, "groups": _export_groups, "ldap": _export_ldap, "dhcp": _export_dhcp,
|
||||||
"logs": _export_logs,
|
"nginx": _export_nginx, "logs": _export_logs,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _import_devices(conn, items):
|
def _import_devices(conn, items):
|
||||||
n = 0
|
n = 0
|
||||||
for d in items:
|
for d in items:
|
||||||
|
cred_id = None
|
||||||
|
if d.get("credential_name"):
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT id FROM credentials WHERE name=? AND deleted_at IS NULL", (d["credential_name"],)
|
||||||
|
).fetchone()
|
||||||
|
cred_id = row["id"] if row else None
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""INSERT INTO devices (mac, ip, port, name, switch_hostname, is_active) VALUES (?, ?, ?, ?, ?, ?)
|
"""INSERT INTO devices (mac, ip, port, name, switch_hostname, is_active, ssh_port, credential_id)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(mac) DO UPDATE SET ip=excluded.ip, port=excluded.port, name=excluded.name,
|
ON CONFLICT(mac) DO UPDATE SET ip=excluded.ip, port=excluded.port, name=excluded.name,
|
||||||
switch_hostname=excluded.switch_hostname, is_active=excluded.is_active
|
switch_hostname=excluded.switch_hostname, is_active=excluded.is_active,
|
||||||
|
ssh_port=excluded.ssh_port, credential_id=excluded.credential_id
|
||||||
WHERE devices.deleted_at IS NULL""",
|
WHERE devices.deleted_at IS NULL""",
|
||||||
(d["mac"], d["ip"], d.get("port"), d["name"], d.get("switch_hostname"), d.get("is_active", 1)),
|
(d["mac"], d["ip"], d.get("port"), d["name"], d.get("switch_hostname"), d.get("is_active", 1),
|
||||||
|
d.get("ssh_port"), cred_id),
|
||||||
)
|
)
|
||||||
n += 1
|
n += 1
|
||||||
return n
|
return n
|
||||||
@@ -5551,10 +5645,11 @@ def _import_switches(conn, items):
|
|||||||
).fetchone()
|
).fetchone()
|
||||||
cred_id = row["id"] if row else None
|
cred_id = row["id"] if row else None
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""INSERT INTO switches (hostname, ip, credential_id) VALUES (?, ?, ?)
|
"""INSERT INTO switches (hostname, ip, ssh_port, credential_id) VALUES (?, ?, ?, ?)
|
||||||
ON CONFLICT(hostname) DO UPDATE SET ip=excluded.ip, credential_id=excluded.credential_id
|
ON CONFLICT(hostname) DO UPDATE SET ip=excluded.ip, ssh_port=excluded.ssh_port,
|
||||||
|
credential_id=excluded.credential_id
|
||||||
WHERE switches.deleted_at IS NULL""",
|
WHERE switches.deleted_at IS NULL""",
|
||||||
(s["hostname"], s["ip"], cred_id),
|
(s["hostname"], s["ip"], s.get("ssh_port"), cred_id),
|
||||||
)
|
)
|
||||||
n += 1
|
n += 1
|
||||||
return n
|
return n
|
||||||
@@ -5564,10 +5659,11 @@ def _import_credentials(conn, items):
|
|||||||
n = 0
|
n = 0
|
||||||
for c in items:
|
for c in items:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""INSERT INTO credentials (name, username, password) VALUES (?, ?, ?)
|
"""INSERT INTO credentials (name, username, password, category) VALUES (?, ?, ?, ?)
|
||||||
ON CONFLICT(name) DO UPDATE SET username=excluded.username, password=excluded.password
|
ON CONFLICT(name) DO UPDATE SET username=excluded.username, password=excluded.password,
|
||||||
|
category=excluded.category
|
||||||
WHERE credentials.deleted_at IS NULL""",
|
WHERE credentials.deleted_at IS NULL""",
|
||||||
(c["name"], c["username"], encrypt_password(c["password"])),
|
(c["name"], c["username"], encrypt_password(c["password"]), c.get("category", "switch")),
|
||||||
)
|
)
|
||||||
n += 1
|
n += 1
|
||||||
return n
|
return n
|
||||||
@@ -5620,7 +5716,10 @@ def _import_groups(conn, items):
|
|||||||
if existing:
|
if existing:
|
||||||
group_id = existing["id"]
|
group_id = existing["id"]
|
||||||
else:
|
else:
|
||||||
cur = conn.execute("INSERT INTO groups (name, is_default, is_system) VALUES (?, 0, 0)", (g["name"],))
|
cur = conn.execute(
|
||||||
|
"INSERT INTO groups (name, is_default, is_system) VALUES (?, ?, 0)",
|
||||||
|
(g["name"], g.get("is_default", 0)),
|
||||||
|
)
|
||||||
group_id = cur.lastrowid
|
group_id = cur.lastrowid
|
||||||
conn.execute("DELETE FROM group_permissions WHERE group_id=?", (group_id,))
|
conn.execute("DELETE FROM group_permissions WHERE group_id=?", (group_id,))
|
||||||
valid_perms = [p for p in g.get("permissions", []) if p in ALL_PERMISSION_KEYS]
|
valid_perms = [p for p in g.get("permissions", []) if p in ALL_PERMISSION_KEYS]
|
||||||
@@ -5653,10 +5752,27 @@ def _import_ldap(conn, data):
|
|||||||
)
|
)
|
||||||
n = 0
|
n = 0
|
||||||
for m in data.get("group_mappings", []):
|
for m in data.get("group_mappings", []):
|
||||||
|
app_group_name = m.get("app_group_name")
|
||||||
|
if app_group_name == "Admin":
|
||||||
|
app_group_id = "admin"
|
||||||
|
else:
|
||||||
|
grow = conn.execute(
|
||||||
|
"SELECT id FROM groups WHERE name=? AND deleted_at IS NULL", (app_group_name,)
|
||||||
|
).fetchone()
|
||||||
|
if not grow:
|
||||||
|
continue # Rechtegruppe existiert auf dem Zielsystem (noch) nicht -- Zuordnung überspringen statt kaputt anzulegen
|
||||||
|
app_group_id = grow["id"]
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO ldap_group_mappings (ad_group_dn, ad_group_name, app_group_id) VALUES (?, ?, ?) "
|
"INSERT INTO ldap_group_mappings (ad_group_dn, ad_group_name, app_group_id) VALUES (?, ?, ?) "
|
||||||
"ON CONFLICT(ad_group_dn) DO UPDATE SET ad_group_name=excluded.ad_group_name, app_group_id=excluded.app_group_id",
|
"ON CONFLICT(ad_group_dn) DO UPDATE SET ad_group_name=excluded.ad_group_name, app_group_id=excluded.app_group_id",
|
||||||
(m["ad_group_dn"], m["ad_group_name"], m["app_group_id"]),
|
(m["ad_group_dn"], m["ad_group_name"], app_group_id),
|
||||||
|
)
|
||||||
|
n += 1
|
||||||
|
for m in data.get("fileshare_mappings", []):
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO ldap_fileshare_mappings (ad_group_dn, ad_group_name, share_label, share_unc) VALUES (?, ?, ?, ?) "
|
||||||
|
"ON CONFLICT(ad_group_dn, share_unc) DO UPDATE SET ad_group_name=excluded.ad_group_name, share_label=excluded.share_label",
|
||||||
|
(m["ad_group_dn"], m["ad_group_name"], m["share_label"], m["share_unc"]),
|
||||||
)
|
)
|
||||||
n += 1
|
n += 1
|
||||||
return n
|
return n
|
||||||
@@ -5709,6 +5825,19 @@ def _import_dhcp(conn, data):
|
|||||||
return n
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
def _import_nginx(conn, data):
|
||||||
|
n = 0
|
||||||
|
for key, value in data.get("settings", {}).items():
|
||||||
|
if key in NGINX_SETTING_KEYS:
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO settings (key, value) VALUES (?, ?) "
|
||||||
|
"ON CONFLICT(key) DO UPDATE SET value=excluded.value",
|
||||||
|
(key, value),
|
||||||
|
)
|
||||||
|
n += 1
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
def _import_logs(conn, items):
|
def _import_logs(conn, items):
|
||||||
conn.executemany(
|
conn.executemany(
|
||||||
"INSERT INTO audit_log (ts, username, action, target, details) VALUES (?, ?, ?, ?, ?)",
|
"INSERT INTO audit_log (ts, username, action, target, details) VALUES (?, ?, ?, ?, ?)",
|
||||||
@@ -5720,7 +5849,7 @@ def _import_logs(conn, items):
|
|||||||
IMPORT_APPLIERS = {
|
IMPORT_APPLIERS = {
|
||||||
"devices": _import_devices, "switches": _import_switches, "credentials": _import_credentials,
|
"devices": _import_devices, "switches": _import_switches, "credentials": _import_credentials,
|
||||||
"users": _import_users, "groups": _import_groups, "ldap": _import_ldap, "dhcp": _import_dhcp,
|
"users": _import_users, "groups": _import_groups, "ldap": _import_ldap, "dhcp": _import_dhcp,
|
||||||
"logs": _import_logs,
|
"nginx": _import_nginx, "logs": _import_logs,
|
||||||
}
|
}
|
||||||
|
|
||||||
_pending_imports = {}
|
_pending_imports = {}
|
||||||
@@ -5766,6 +5895,11 @@ def export_data():
|
|||||||
flash("Keine Daten in den ausgewählten Kategorien vorhanden — nichts exportiert.", "danger")
|
flash("Keine Daten in den ausgewählten Kategorien vorhanden — nichts exportiert.", "danger")
|
||||||
return redirect(url_for("settings_import_export"))
|
return redirect(url_for("settings_import_export"))
|
||||||
|
|
||||||
|
if "credentials" in payload:
|
||||||
|
known_hosts_content = _read_known_hosts_content()
|
||||||
|
if known_hosts_content:
|
||||||
|
payload["known_hosts"] = known_hosts_content
|
||||||
|
|
||||||
salt = secrets.token_bytes(16)
|
salt = secrets.token_bytes(16)
|
||||||
export_fernet = _derive_export_fernet(passphrase, salt)
|
export_fernet = _derive_export_fernet(passphrase, salt)
|
||||||
encrypted_payload = export_fernet.encrypt(json.dumps(payload).encode("utf-8"))
|
encrypted_payload = export_fernet.encrypt(json.dumps(payload).encode("utf-8"))
|
||||||
@@ -5778,8 +5912,10 @@ def export_data():
|
|||||||
"payload": encrypted_payload.decode("utf-8"),
|
"payload": encrypted_payload.decode("utf-8"),
|
||||||
}
|
}
|
||||||
|
|
||||||
summary = ", ".join(f"{EXPORT_SECTION_LABELS[k]}: {counts[k]}" for k in payload)
|
summary = ", ".join(f"{EXPORT_SECTION_LABELS[k]}: {counts[k]}" for k in payload if k in EXPORT_SECTION_LABELS)
|
||||||
log_action("data.export", ", ".join(EXPORT_SECTION_LABELS[k] for k in payload), summary)
|
if "known_hosts" in payload:
|
||||||
|
summary += " (inkl. known_hosts)"
|
||||||
|
log_action("data.export", ", ".join(EXPORT_SECTION_LABELS[k] for k in payload if k in EXPORT_SECTION_LABELS), summary)
|
||||||
|
|
||||||
filename = f"tesm_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
filename = f"tesm_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
||||||
response = jsonify(envelope)
|
response = jsonify(envelope)
|
||||||
@@ -5878,6 +6014,9 @@ def import_apply():
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
summary = ", ".join(f"{EXPORT_SECTION_LABELS.get(k, k)}: {results[k]}" for k in selected)
|
summary = ", ".join(f"{EXPORT_SECTION_LABELS.get(k, k)}: {results[k]}" for k in selected)
|
||||||
|
if "credentials" in selected and payload.get("known_hosts"):
|
||||||
|
added = _merge_known_hosts_content(payload["known_hosts"])
|
||||||
|
summary += f", known_hosts: {added} neue Zeile(n)"
|
||||||
log_action("data.import", ", ".join(EXPORT_SECTION_LABELS.get(k, k) for k in selected), summary)
|
log_action("data.import", ", ".join(EXPORT_SECTION_LABELS.get(k, k) for k in selected), summary)
|
||||||
flash(f"Import abgeschlossen: {summary}.", "success")
|
flash(f"Import abgeschlossen: {summary}.", "success")
|
||||||
return redirect(url_for("settings_import_export"))
|
return redirect(url_for("settings_import_export"))
|
||||||
|
|||||||
Reference in New Issue
Block a user