Hostname-Einstellung, DHCP-Range-Pflicht, Topbar-Ampel, Text-Straffung, Quelltext-Huerde
Hostname (Systemeinstellungen, neben Pruefintervall): - hostnamectl set-hostname ueber die App, Validierung (RFC-1123-artiges Label), kein Revert-Timer noetig (kappt die Erreichbarkeit nicht wie eine IP-Aenderung). Live getestet inkl. Validierung und Revert. DHCP: Range ist jetzt Pflicht, kein geratener Default mehr: - dhcp_range_start/end/dns starten leer (Platzhaltertext statt fake- echt aussehendem Default) - ein Zufalls-Range haette den Dienst sonst unbemerkt mit einer zum echten Netz nicht passenden Konfiguration starten lassen koennen. - write_dhcp_file und dhcp_service_action=enable_restart verweigern sich ohne eingetragene Range; _render_kea_config laesst "pools" ohne Range komplett weg statt einen kaputten Pool-String zu erzeugen. - Neues, separates dhcp_gateway-Feld (optional) fuer einen vom automatisch erkannten Gateway abweichenden Router fuer die Clients. Topbar-Ampel fuer den Kea-Dienst: - Gruener/roter Punkt neben dem Pruefintervall-Timer, nur sichtbar mit settings_dhcp.view (ein einzelner, kurzer systemctl-Aufruf pro Request, nicht die volle Status-Erkennung). Live verifiziert (rot wenn gestoppt, gruen wenn gestartet). Reservierungstabelle zeigt jetzt pro Client, welche eigene DHCP-Option greift (global oder Client-Override, mit Wert im Tooltip). Text-Straffung: die laengsten Hint-Texte und Code-Kommentare in Templates/app.py gekuerzt (u.a. groups.html, settings_dhcp.html, account.html, devices/switches/users/credentials.html, zwei grosse Migrations-/DHCP-Kommentarbloecke in app.py) - Kernaussagen erhalten, Redundanz entfernt. Quelltext-Huerde (KEINE echte Sicherheit, nur Abschreckung): Rechtsklick und DevTools-/Quelltext-Shortcuts per JS blockiert. Klar dokumentiert in Kommentar + README, dass der Browser HTML/CSS/JS immer vollstaendig ausliefert und das in Sekunden umgehbar ist - echte Absicherung bleiben ausschliesslich die serverseitigen Rechteprüfungen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+81
-51
@@ -17,7 +17,7 @@ from cryptography.fernet import Fernet
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from datetime import datetime
|
||||
import base64, ipaddress, shutil, sqlite3, glob, json, os, re, secrets, subprocess, threading, time, traceback
|
||||
import base64, ipaddress, shutil, socket, sqlite3, glob, json, os, re, secrets, subprocess, threading, time, traceback
|
||||
import paramiko
|
||||
|
||||
|
||||
@@ -623,24 +623,12 @@ def _ensure_schema():
|
||||
(cur.lastrowid, sw["hostname"]),
|
||||
)
|
||||
|
||||
# Migration (Einführung des granularen, an die Navbar gespiegelten
|
||||
# Rechtebaums): alte Permission-Keys auf die neuen übertragen, damit
|
||||
# bestehende Gruppen durch das Update keinen Zugriff verlieren.
|
||||
# - devices.toggle ist jetzt Teil von devices.edit (Ändern).
|
||||
# - Zugangsdaten hatten bisher keine eigenen Rechte, sondern hingen an
|
||||
# switches.* — bestehende Gruppen bekommen die entsprechenden
|
||||
# credentials.*-Rechte gespiegelt, künftig sind beide unabhängig.
|
||||
# - Jede Gruppe, die bereits ein Kind-Recht in einem Bereich hatte,
|
||||
# bekommt automatisch auch das neue übergeordnete "Bereich
|
||||
# sichtbar"-Recht (devices_group.view/logs_group.view/
|
||||
# settings_group.view), da Kind-Rechte jetzt ohne dieses Recht wirkungslos
|
||||
# wären (siehe User.has_permission).
|
||||
# WICHTIG: Läuft nur EINMALIG (Guard über settings), nicht bei jedem
|
||||
# Start — sonst würde das übergeordnete "Bereich sichtbar"-Recht bei
|
||||
# jedem Neustart automatisch wieder gesetzt, sobald irgendein Kind-Recht
|
||||
# noch aktiv ist. Das würde den eigentlichen Zweck des Kill-Switches
|
||||
# zunichtemachen (ein Admin könnte den ganzen Bereich nie dauerhaft
|
||||
# sperren, ohne zuvor jedes einzelne Kind-Recht zu entfernen).
|
||||
# Migration (granularer Rechtebaum): devices.toggle -> devices.edit,
|
||||
# switches.* -> gespiegelte credentials.*, sowie Backfill des neuen
|
||||
# "Bereich sichtbar"-Rechts für Gruppen mit bereits gesetzten Kind-Rechten.
|
||||
# WICHTIG: nur EINMALIG (Guard über settings) — sonst würde das
|
||||
# Bereichs-Recht bei jedem Neustart automatisch neu gesetzt und den
|
||||
# Kill-Switch (siehe User.has_permission) wirkungslos machen.
|
||||
_migration_key = "_migrated_permission_hierarchy_v1"
|
||||
if not conn.execute("SELECT 1 FROM settings WHERE key=?", (_migration_key,)).fetchone():
|
||||
toggle_groups = conn.execute(
|
||||
@@ -1077,6 +1065,22 @@ def inject_check_timer():
|
||||
}
|
||||
|
||||
|
||||
@app.context_processor
|
||||
def inject_dhcp_topbar_status():
|
||||
"""Ampel-Punkt für den Kea-Dienst neben dem Prüfintervall-Timer — nur
|
||||
für Benutzer mit mindestens Leserecht auf DHCP, und nur ein einzelner,
|
||||
kurzer systemctl-Aufruf (nicht die volle Status-Erkennung von
|
||||
_detect_dhcp_status), damit das nicht jede Seite spürbar verlangsamt."""
|
||||
if not current_user.is_authenticated or not current_user.has_permission("settings_dhcp.view"):
|
||||
return {"dhcp_topbar_active": None}
|
||||
try:
|
||||
result = subprocess.run(["systemctl", "is-active", DHCP_SERVICE], capture_output=True, text=True, timeout=2)
|
||||
active = result.stdout.strip() == "active"
|
||||
except Exception:
|
||||
active = None
|
||||
return {"dhcp_topbar_active": active}
|
||||
|
||||
|
||||
def get_device_status(devices):
|
||||
"""
|
||||
Ermittelt aus dem aktuellsten Logfile in einem Durchlauf je Gerät:
|
||||
@@ -1229,21 +1233,12 @@ def run_check_now():
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# System-Netzwerkeinstellungen (IP/DNS/DHCP DIESES Hosts — nicht zu
|
||||
# verwechseln mit dem Kea-DHCP-*Server* für Client-Geräte weiter unten).
|
||||
#
|
||||
# Höchste Vorsicht geboten: eine falsche Änderung hier kann die Erreichbarkeit
|
||||
# dieses Hosts (SSH/Web-UI) komplett kappen. Deshalb:
|
||||
# - Erkennung des tatsächlich aktiven Netzwerk-Backends (NetworkManager
|
||||
# oder dhcpcd) statt blind eine Methode anzunehmen; ist keins von beiden
|
||||
# aktiv (z.B. in dieser WSL-Testumgebung), bleibt die Seite bewusst rein
|
||||
# lesend statt zu raten.
|
||||
# - Vor jeder Änderung wird die vorherige Konfiguration gesichert.
|
||||
# - Nach dem Anwenden läuft ein Sicherheits-Timer im Hintergrund: wird die
|
||||
# neue Konfiguration nicht innerhalb von NETWORK_REVERT_SECONDS explizit
|
||||
# bestätigt (das ist nur möglich, wenn die Web-UI über die NEUE
|
||||
# Konfiguration noch erreichbar ist), wird automatisch die gesicherte
|
||||
# Konfiguration wiederhergestellt — analog zu "netplan try".
|
||||
# System-Netzwerkeinstellungen (IP/DNS/DHCP DIESES Hosts — nicht der
|
||||
# Kea-DHCP-*Server* für Client-Geräte weiter unten). Eine falsche Änderung
|
||||
# kann die Erreichbarkeit kappen, daher: Backend-Erkennung statt Annahme
|
||||
# (unbekannt -> rein lesend), Backup vor jeder Änderung, und ein
|
||||
# Sicherheits-Timer (analog "netplan try") der ohne explizite Bestätigung
|
||||
# innerhalb von NETWORK_REVERT_SECONDS automatisch zurückrollt.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
NETWORK_REVERT_SECONDS = 45
|
||||
@@ -1470,6 +1465,18 @@ def settings():
|
||||
else:
|
||||
flash(f"Anwenden fehlgeschlagen: {out}", "danger")
|
||||
|
||||
elif "hostname" in request.form:
|
||||
new_hostname = request.form.get("hostname", "").strip()
|
||||
if not re.match(r"^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$", new_hostname):
|
||||
flash("Ungültiger Hostname (nur Buchstaben, Ziffern und Bindestrich, max. 63 Zeichen).", "danger")
|
||||
else:
|
||||
ok, out = _dhcp_run_privileged(["hostnamectl", "set-hostname", new_hostname], timeout=10)
|
||||
if ok:
|
||||
log_action("settings.update", "Hostname", new_hostname)
|
||||
flash(f"Hostname auf „{new_hostname}“ gesetzt.", "success")
|
||||
else:
|
||||
flash(f"Hostname konnte nicht gesetzt werden: {out}", "danger")
|
||||
|
||||
elif "confirm_network" in request.form:
|
||||
token = request.form.get("confirm_network")
|
||||
entry = _pending_network_revert.pop(token, None)
|
||||
@@ -1486,9 +1493,14 @@ def settings():
|
||||
net_interface = get_setting("net_interface") or (_list_network_interfaces() or [None])[0]
|
||||
net_state = _read_network_state(net_interface, net_backend) if net_interface else None
|
||||
pending_token = next(iter(_pending_network_revert), None)
|
||||
try:
|
||||
current_hostname = socket.gethostname()
|
||||
except Exception:
|
||||
current_hostname = None
|
||||
return render_template(
|
||||
"settings.html",
|
||||
interval=interval,
|
||||
current_hostname=current_hostname,
|
||||
net_backend=net_backend,
|
||||
net_interfaces=_list_network_interfaces(),
|
||||
net_interface=net_interface,
|
||||
@@ -1510,17 +1522,11 @@ def settings_import_export():
|
||||
# ---------------------------------------------------------------------------
|
||||
# DHCP-Reservierungen (Kea DHCP)
|
||||
#
|
||||
# Backend bewusst Kea (kea-dhcp4-server), nicht das ältere isc-dhcp-server:
|
||||
# Kea wird von ISC selbst aktiv weiterentwickelt (isc-dhcp-server ist seit
|
||||
# 2022 EOL) und bildet "globaler Wert, pro Client überschreibbar" nativ ab
|
||||
# (Reservation-Options überschreiben in Kea automatisch gleichnamige
|
||||
# globale/Subnet-Options) — genau das von den Custom-Options gefordert.
|
||||
#
|
||||
# Installation & Dienststeuerung sind eigene, einzeln bestätigte Aktionen
|
||||
# (apt-get install / systemctl enable+restart) statt automatisch an
|
||||
# "Konfiguration speichern" gekoppelt — ein unerwartet aktiver oder
|
||||
# fehlkonfigurierter DHCP-Server kann ein ganzes Netzsegment stören, das
|
||||
# soll immer ein bewusster, separater Klick bleiben.
|
||||
# Backend bewusst Kea statt isc-dhcp-server (seit 2022 EOL) — Kea bildet
|
||||
# "globaler Wert, pro Client überschreibbar" nativ über Reservation-Options
|
||||
# ab. Installation & Dienststeuerung sind eigene, einzeln bestätigte
|
||||
# Aktionen statt an "Konfiguration speichern" gekoppelt, da ein
|
||||
# unerwarteter DHCP-Server ein Netzsegment stören kann.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
DHCP_PACKAGE = "kea-dhcp4-server"
|
||||
@@ -1528,15 +1534,26 @@ DHCP_SERVICE = "kea-dhcp4-server"
|
||||
|
||||
DHCP_DEFAULTS = {
|
||||
"dhcp_interface": "eth0",
|
||||
"dhcp_range_start": "192.168.1.100",
|
||||
"dhcp_range_end": "192.168.1.200",
|
||||
"dhcp_dns": "192.168.1.1",
|
||||
# Bewusst KEINE vorausgefüllten Range-/DNS-Defaults, die wie eine echte
|
||||
# Konfiguration aussehen könnten, aber zufällig nicht zum echten Netz
|
||||
# passen — leer + Platzhaltertext im Formular ist ehrlicher, als einen
|
||||
# Dienst unbemerkt mit falschen Werten starten zu lassen.
|
||||
"dhcp_range_start": "",
|
||||
"dhcp_range_end": "",
|
||||
"dhcp_dns": "",
|
||||
# Optionaler Override — leer bedeutet "automatisch erkanntes Gateway
|
||||
# dieses Hosts verwenden" (siehe _render_kea_config).
|
||||
"dhcp_gateway": "",
|
||||
"dhcp_domain": "local",
|
||||
"dhcp_lease_default": "600",
|
||||
"dhcp_lease_max": "7200",
|
||||
"dhcp_output_path": "/etc/kea/kea-dhcp4.conf",
|
||||
}
|
||||
|
||||
|
||||
def _dhcp_config_is_complete(cfg):
|
||||
return bool(cfg.get("dhcp_range_start") and cfg.get("dhcp_range_end"))
|
||||
|
||||
# Auswahl an Kea-Datentypen für eigene Options — bewusst eine kompatible
|
||||
# Teilmenge statt der vollen Liste, orientiert an den in der Praxis
|
||||
# gebrauchten Fällen (Text, IP, Zahl, Wahrheitswert, Rohdaten/Hex).
|
||||
@@ -1718,7 +1735,10 @@ def _render_kea_config(cfg, net_info, reservations, option_defs, option_values):
|
||||
Options für genau diesen Client, ohne dass wir das selbst
|
||||
zusammenrechnen müssen."""
|
||||
dns_servers = ", ".join(s.strip() for s in cfg["dhcp_dns"].split(",") if s.strip())
|
||||
router = net_info.get("gateway") or (cfg["dhcp_dns"].split(",")[0].strip() if cfg["dhcp_dns"] else "")
|
||||
# Gateway: expliziter Override hat Vorrang, sonst automatisch erkanntes
|
||||
# Gateway dieses Hosts — für den Fall, dass Clients einen anderen Router
|
||||
# bekommen sollen als den, über den dieser Host selbst geht.
|
||||
router = cfg.get("dhcp_gateway", "").strip() or net_info.get("gateway") or ""
|
||||
|
||||
option_def_list = [
|
||||
{"name": d["name"], "code": d["code"], "type": d["type"], "space": "dhcp4"}
|
||||
@@ -1765,7 +1785,11 @@ def _render_kea_config(cfg, net_info, reservations, option_defs, option_values):
|
||||
{
|
||||
"id": 1,
|
||||
"subnet": f"{net_info['network'].network_address}/{net_info['network'].prefixlen}",
|
||||
"pools": [{"pool": f"{cfg['dhcp_range_start']} - {cfg['dhcp_range_end']}"}],
|
||||
# Kein Pool ohne eingetragene Range — ein leerer/falscher
|
||||
# Pool-String wäre eine Kea-Konfiguration, die entweder
|
||||
# gar nicht startet oder (schlimmer) mit einer geratenen
|
||||
# Range startet, die nicht zum echten Netz passt.
|
||||
**({"pools": [{"pool": f"{cfg['dhcp_range_start']} - {cfg['dhcp_range_end']}"}]} if _dhcp_config_is_complete(cfg) else {}),
|
||||
"reservations": kea_reservations,
|
||||
}
|
||||
] if net_info.get("ok") else [],
|
||||
@@ -1882,6 +1906,10 @@ def settings_dhcp():
|
||||
|
||||
elif "write_dhcp_file" in request.form:
|
||||
cfg = _load_dhcp_settings()
|
||||
if not _dhcp_config_is_complete(cfg):
|
||||
flash("Range Start/Ende müssen eingetragen sein, bevor eine Konfiguration geschrieben wird.", "danger")
|
||||
conn.close()
|
||||
return redirect(url_for("settings_dhcp"))
|
||||
net_info = _detect_interface_network(cfg["dhcp_interface"])
|
||||
if not net_info["ok"]:
|
||||
flash(f"Netzwerk-Erkennung für „{cfg['dhcp_interface']}“ fehlgeschlagen: {net_info['error']} — nichts geschrieben.", "danger")
|
||||
@@ -1927,7 +1955,9 @@ def settings_dhcp():
|
||||
|
||||
elif "dhcp_service_action" in request.form:
|
||||
action = request.form.get("dhcp_service_action")
|
||||
if action == "enable_restart":
|
||||
if action == "enable_restart" and not _dhcp_config_is_complete(_load_dhcp_settings()):
|
||||
flash("Range Start/Ende müssen eingetragen und die Konfiguration geschrieben sein, bevor der Dienst gestartet wird.", "danger")
|
||||
elif action == "enable_restart":
|
||||
ok_enable, _ = _dhcp_run_privileged(["systemctl", "enable", DHCP_SERVICE], timeout=15)
|
||||
ok_restart, restart_out = _dhcp_run_privileged(["systemctl", "restart", DHCP_SERVICE], timeout=30)
|
||||
log_action("settings.dhcp_service", DHCP_SERVICE, "enable+restart " + ("ok" if (ok_enable and ok_restart) else "fehlgeschlagen"))
|
||||
|
||||
Reference in New Issue
Block a user