Rechtesystem: Löschen überall in Ändern falten, Gruppen-Tabelle transponieren
- "Ändern" deckt jetzt ausnahmslos überall Löschen mit ab (auch bei Clients) — es gibt kein eigenes *.delete-Recht mehr. Betroffen: devices/switches/credentials/users/groups. - "D" in der Rechtetabelle bedeutet ausschließlich Dashboard-Neustart (devices.restart, PoE-Neustart über das Dashboard-Popup) und existiert nur bei Clients. Automatische Neustarts bei Ausfall (poe.sh) bleiben davon komplett unabhängig. - Gruppen-Rechtetabelle transponiert: Kategorien jetzt als Zeilen, Rechte R(ead)/W(rite)/E(dit)/D(ashboard-Neustart) nebeneinander als Spalten statt untereinander. Deckt sich besser mit dem Legenden-Format und braucht weniger vertikalen Platz. - Neue, einmalig laufende Migration (_migrated_permission_delete_fold_v2) überführt bestehende *.delete-Grants verlustfrei in *.edit, bevor die veralteten Rechte-Zeilen entfernt werden. - Routen (delete_device/delete_switch/delete_credential/delete_user/ delete_group) und die zugehörigen can_delete-Template-Variablen prüfen jetzt einheitlich *.edit statt *.delete. - README-Rechtesystem-Abschnitt und Legende an das neue Tabellen-Layout angepasst. - Live getestet: Kill-Switch, alle 5 Delete-Foldings (mit/ohne Recht), devices.restart-Gating unabhängig von devices.edit, Eskalationsschutz (Admin-Löschschutz, Systemgruppen-Schutz) — alles bestätigt korrekt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+117
-56
@@ -125,10 +125,20 @@ login_manager.init_app(app)
|
||||
# ---------------------------------------------------------------------------
|
||||
# Zweistufig verschachtelt, gespiegelt an der Navbar-Struktur: jeder
|
||||
# Top-Level-Bereich (Geräte/Logs/Einstellungen) hat ein eigenes "Bereich
|
||||
# anzeigen"-Recht (view_key) und darunter mehrere Unterpunkte mit jeweils
|
||||
# eigenen Lesen/Schreiben/Ändern/Löschen-Rechten. Der Bereichs-Name wird
|
||||
# dadurch nicht in jedem Unterpunkt wiederholt (siehe groups.html: eine
|
||||
# Zeile pro Top-Level-Bereich, Unterpunkte darunter eingerückt).
|
||||
# anzeigen"-Recht (view_key) und darunter mehrere Unterpunkte (Spalten in der
|
||||
# Gruppen-Tabelle). Jeder Unterpunkt liefert "rows": eine Teilmenge der vier
|
||||
# kanonischen Rechte-Zeilen (view/create/edit/delete = L/S/Ä/D, siehe
|
||||
# PERMISSION_ROW_TYPES) — fehlt eine Zeile für einen Unterpunkt (z.B.
|
||||
# "delete" bei Systemeinstellungen), wird die Zelle in der Tabelle grau und
|
||||
# nicht setzbar dargestellt statt einfach zu fehlen, damit alle Spalten
|
||||
# eines Bereichs bündig nebeneinander in einer kompakten Tabelle stehen.
|
||||
PERMISSION_ROW_TYPES = [
|
||||
("view", "R", "Lesen (Read)"),
|
||||
("create", "W", "Schreiben/Anlegen (Write)"),
|
||||
("edit", "E", "Ändern inkl. Löschen (Edit)"),
|
||||
("extra", "D", "Dashboard-Neustart (nur bei Clients — kein Löschen-Recht)"),
|
||||
]
|
||||
|
||||
PERMISSIONS = {
|
||||
"devices_group": {
|
||||
"label": "Geräte",
|
||||
@@ -136,30 +146,33 @@ PERMISSIONS = {
|
||||
"children": {
|
||||
"devices": {
|
||||
"label": "Clients",
|
||||
"items": {
|
||||
"devices.view": "Lesen",
|
||||
"devices.create": "Schreiben (Anlegen)",
|
||||
"devices.edit": "Ändern (inkl. Aktivieren/Deaktivieren)",
|
||||
"devices.delete": "Löschen",
|
||||
"devices.restart": "PoE-Neustart über das Dashboard",
|
||||
# "Ändern" deckt Löschen überall mit ab, auch hier — es gibt
|
||||
# nirgends ein eigenes Löschen-Recht. Die vierte Spalte (D)
|
||||
# ist bei Clients KEIN Löschen, sondern das Recht, einen
|
||||
# PoE-Neustart über das Dashboard-Popup auszulösen
|
||||
# (automatische Neustarts bei Ausfall bleiben davon unberührt).
|
||||
"rows": {
|
||||
"view": "devices.view",
|
||||
"create": "devices.create",
|
||||
"edit": "devices.edit",
|
||||
"extra": "devices.restart",
|
||||
},
|
||||
},
|
||||
"switches": {
|
||||
"label": "Switche",
|
||||
"items": {
|
||||
"switches.view": "Lesen",
|
||||
"switches.create": "Schreiben (Anlegen)",
|
||||
"switches.edit": "Ändern",
|
||||
"switches.delete": "Löschen",
|
||||
# Kein eigenes Löschen-Recht — "Ändern" deckt das mit ab.
|
||||
"rows": {
|
||||
"view": "switches.view",
|
||||
"create": "switches.create",
|
||||
"edit": "switches.edit",
|
||||
},
|
||||
},
|
||||
"credentials": {
|
||||
"label": "Zugangsdaten",
|
||||
"items": {
|
||||
"credentials.view": "Lesen",
|
||||
"credentials.create": "Schreiben (Anlegen)",
|
||||
"credentials.edit": "Ändern",
|
||||
"credentials.delete": "Löschen",
|
||||
"rows": {
|
||||
"view": "credentials.view",
|
||||
"create": "credentials.create",
|
||||
"edit": "credentials.edit",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -168,14 +181,8 @@ PERMISSIONS = {
|
||||
"label": "Logs",
|
||||
"view_key": "logs_group.view",
|
||||
"children": {
|
||||
"logs_live": {
|
||||
"label": "Live",
|
||||
"items": {"logs_live.view": "Lesen"},
|
||||
},
|
||||
"logs_activity": {
|
||||
"label": "Änderungen",
|
||||
"items": {"logs_activity.view": "Lesen"},
|
||||
},
|
||||
"logs_live": {"label": "Live", "rows": {"view": "logs_live.view"}},
|
||||
"logs_activity": {"label": "Änderungen", "rows": {"view": "logs_activity.view"}},
|
||||
},
|
||||
},
|
||||
"settings_group": {
|
||||
@@ -184,40 +191,55 @@ PERMISSIONS = {
|
||||
"children": {
|
||||
"users": {
|
||||
"label": "Benutzer",
|
||||
"items": {
|
||||
"users.view": "Lesen",
|
||||
"users.create": "Schreiben (Anlegen)",
|
||||
"users.edit": "Ändern",
|
||||
"users.delete": "Löschen",
|
||||
"rows": {
|
||||
"view": "users.view",
|
||||
"create": "users.create",
|
||||
"edit": "users.edit",
|
||||
},
|
||||
},
|
||||
"groups": {
|
||||
"label": "Gruppen",
|
||||
"items": {
|
||||
"groups.view": "Lesen",
|
||||
"groups.create": "Schreiben (Anlegen)",
|
||||
"groups.edit": "Ändern",
|
||||
"groups.delete": "Löschen",
|
||||
"rows": {
|
||||
"view": "groups.view",
|
||||
"create": "groups.create",
|
||||
"edit": "groups.edit",
|
||||
},
|
||||
},
|
||||
"settings_system": {
|
||||
"label": "Systemeinstellungen",
|
||||
"items": {
|
||||
"settings_system.view": "Lesen",
|
||||
"settings_system.edit": "Ändern",
|
||||
},
|
||||
"rows": {"view": "settings_system.view", "edit": "settings_system.edit"},
|
||||
},
|
||||
"settings_importexport": {
|
||||
"label": "Im-/Export",
|
||||
"items": {
|
||||
"settings_importexport.view": "Lesen (Export)",
|
||||
"settings_importexport.edit": "Ändern (Import)",
|
||||
},
|
||||
# L = Export (nur lesend), Ä = Import (verändert Bestand).
|
||||
"rows": {"view": "settings_importexport.view", "edit": "settings_importexport.edit"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# Menschenlesbare Einzel-Labels je Recht (Tooltips auf den Checkboxen in der
|
||||
# Tabelle) — aus PERMISSIONS abgeleitet, damit es nur eine Quelle gibt.
|
||||
PERMISSION_LABELS = {
|
||||
"devices_group.view": "Geräte-Bereich anzeigen",
|
||||
"logs_group.view": "Logs-Bereich anzeigen",
|
||||
"settings_group.view": "Einstellungen-Bereich anzeigen",
|
||||
"devices.view": "Clients lesen", "devices.create": "Clients anlegen",
|
||||
"devices.edit": "Clients ändern (inkl. Aktivieren/Deaktivieren, Löschen)",
|
||||
"devices.restart": "PoE-Neustart über das Dashboard ausführen (automatische Neustarts bei Ausfall bleiben unberührt)",
|
||||
"switches.view": "Switche lesen", "switches.create": "Switche anlegen",
|
||||
"switches.edit": "Switche ändern (inkl. Löschen)",
|
||||
"credentials.view": "Zugangsdaten lesen", "credentials.create": "Zugangsdaten anlegen",
|
||||
"credentials.edit": "Zugangsdaten ändern (inkl. Löschen)",
|
||||
"logs_live.view": "Live-Log lesen", "logs_activity.view": "Änderungslog lesen",
|
||||
"users.view": "Benutzer lesen", "users.create": "Benutzer anlegen",
|
||||
"users.edit": "Benutzer ändern (inkl. Löschen)",
|
||||
"groups.view": "Gruppen lesen", "groups.create": "Gruppen anlegen",
|
||||
"groups.edit": "Gruppen ändern (inkl. Löschen)",
|
||||
"settings_system.view": "Systemeinstellungen lesen", "settings_system.edit": "Systemeinstellungen ändern",
|
||||
"settings_importexport.view": "Daten exportieren", "settings_importexport.edit": "Daten importieren",
|
||||
}
|
||||
|
||||
# Aus der verschachtelten Struktur abgeleitet: alle vergebbaren Keys, sowie
|
||||
# eine Zuordnung jedes Kind-Rechts zum "Bereich anzeigen"-Recht seines
|
||||
# Top-Level-Bereichs. Ist das übergeordnete Recht einer Gruppe nicht
|
||||
@@ -230,7 +252,7 @@ PERMISSION_PARENT_GROUP = {}
|
||||
for _group in PERMISSIONS.values():
|
||||
ALL_PERMISSION_KEYS.append(_group["view_key"])
|
||||
for _child in _group["children"].values():
|
||||
for _key in _child["items"]:
|
||||
for _key in _child["rows"].values():
|
||||
ALL_PERMISSION_KEYS.append(_key)
|
||||
PERMISSION_PARENT_GROUP[_key] = _group["view_key"]
|
||||
|
||||
@@ -352,13 +374,13 @@ class User(UserMixin):
|
||||
@property
|
||||
def can_manage_switches(self):
|
||||
return self.has_any_permission(
|
||||
["switches.view", "switches.create", "switches.edit", "switches.delete"]
|
||||
["switches.view", "switches.create", "switches.edit"]
|
||||
)
|
||||
|
||||
@property
|
||||
def can_manage_credentials(self):
|
||||
return self.has_any_permission(
|
||||
["credentials.view", "credentials.create", "credentials.edit", "credentials.delete"]
|
||||
["credentials.view", "credentials.create", "credentials.edit"]
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -372,13 +394,13 @@ class User(UserMixin):
|
||||
@property
|
||||
def can_manage_users(self):
|
||||
return self.has_any_permission(
|
||||
["users.view", "users.create", "users.edit", "users.delete"]
|
||||
["users.view", "users.create", "users.edit"]
|
||||
)
|
||||
|
||||
@property
|
||||
def can_manage_groups(self):
|
||||
return self.has_any_permission(
|
||||
["groups.view", "groups.create", "groups.edit", "groups.delete"]
|
||||
["groups.view", "groups.create", "groups.edit"]
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -595,6 +617,38 @@ def _ensure_schema():
|
||||
"INSERT OR IGNORE INTO settings (key, value) VALUES (?, '1')", (_migration_key,)
|
||||
)
|
||||
|
||||
# Migration v2: "Löschen" als eigenes Recht wieder abgeschafft — "Ändern"
|
||||
# deckt das Löschen jetzt überall mit ab (auch bei Clients, keine
|
||||
# Ausnahme mehr). Gruppen, die bisher nur *.delete ohne *.edit hatten,
|
||||
# würden sonst durch das Update ihr Löschen-Recht stillschweigend
|
||||
# verlieren — deshalb erst *.edit nachtragen, dann die veralteten
|
||||
# *.delete-Zeilen entfernen. Ebenfalls einmalig per Guard, aus demselben
|
||||
# Grund wie bei v1 (kein Zurückschreiben bei jedem Neustart nötig, da
|
||||
# *.delete als Recht komplett entfällt und nie wieder vorkommen kann).
|
||||
_migration_key_v2 = "_migrated_permission_delete_fold_v2"
|
||||
if not conn.execute("SELECT 1 FROM settings WHERE key=?", (_migration_key_v2,)).fetchone():
|
||||
_delete_to_edit = {
|
||||
"devices.delete": "devices.edit",
|
||||
"switches.delete": "switches.edit",
|
||||
"credentials.delete": "credentials.edit",
|
||||
"users.delete": "users.edit",
|
||||
"groups.delete": "groups.edit",
|
||||
}
|
||||
for delete_perm, edit_perm in _delete_to_edit.items():
|
||||
rows = conn.execute(
|
||||
"SELECT DISTINCT group_id FROM group_permissions WHERE permission=?", (delete_perm,)
|
||||
).fetchall()
|
||||
for row in rows:
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO group_permissions (group_id, permission) VALUES (?, ?)",
|
||||
(row["group_id"], edit_perm),
|
||||
)
|
||||
conn.execute("DELETE FROM group_permissions WHERE permission=?", (delete_perm,))
|
||||
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO settings (key, value) VALUES (?, '1')", (_migration_key_v2,)
|
||||
)
|
||||
|
||||
# Standardgruppe 'Benutzer' — SYSTEMGRUPPE: Name und Rechte sind fix und
|
||||
# werden bei jedem Start auf den vorgegebenen Satz zurückgesetzt (nicht
|
||||
# nur bei Erstanlage). Das ist unbedenklich, weil is_system die Gruppe in
|
||||
@@ -1366,7 +1420,8 @@ def devices():
|
||||
flash("Fehler beim Aktualisieren des Geräts!", "danger")
|
||||
|
||||
elif "delete_device" in request.form:
|
||||
if not current_user.has_permission("devices.delete"):
|
||||
# "Ändern" deckt Löschen mit ab (kein eigenes devices.delete-Recht).
|
||||
if not current_user.has_permission("devices.edit"):
|
||||
flash("Keine Berechtigung, Geräte zu löschen.", "danger")
|
||||
return redirect(url_for("devices"))
|
||||
|
||||
@@ -1528,7 +1583,8 @@ def switches():
|
||||
@app.route("/switches/delete/<hostname>", methods=["POST"])
|
||||
@login_required
|
||||
def delete_switch(hostname):
|
||||
if not current_user.has_permission("switches.delete"):
|
||||
# "Ändern" deckt Löschen mit ab (kein eigenes switches.delete-Recht).
|
||||
if not current_user.has_permission("switches.edit"):
|
||||
flash("Keine Berechtigung, Switche zu löschen.", "danger")
|
||||
return redirect(url_for("switches"))
|
||||
|
||||
@@ -1613,7 +1669,8 @@ def credentials():
|
||||
flash("Name und Username müssen ausgefüllt sein!", "danger")
|
||||
|
||||
elif request.method == "POST" and "delete_credential" in request.form:
|
||||
if not current_user.has_permission("credentials.delete"):
|
||||
# "Ändern" deckt Löschen mit ab (kein eigenes credentials.delete-Recht).
|
||||
if not current_user.has_permission("credentials.edit"):
|
||||
flash("Keine Berechtigung, Zugangsdaten zu löschen.", "danger")
|
||||
return redirect(url_for("credentials"))
|
||||
cred_id = request.form.get("delete_credential")
|
||||
@@ -2098,7 +2155,8 @@ def users():
|
||||
flash("Gruppe zugewiesen!", "success")
|
||||
|
||||
elif "delete_user" in request.form:
|
||||
if not current_user.has_permission("users.delete"):
|
||||
# "Ändern" deckt Löschen mit ab (kein eigenes users.delete-Recht).
|
||||
if not current_user.has_permission("users.edit"):
|
||||
flash("Keine Berechtigung, Benutzer zu löschen.", "danger")
|
||||
return redirect(url_for("users"))
|
||||
|
||||
@@ -2254,7 +2312,8 @@ def groups():
|
||||
flash("Admin-Zuweisung aktualisiert.", "success")
|
||||
|
||||
elif "delete_group" in request.form:
|
||||
if not current_user.has_permission("groups.delete"):
|
||||
# "Ändern" deckt Löschen mit ab (kein eigenes groups.delete-Recht).
|
||||
if not current_user.has_permission("groups.edit"):
|
||||
flash("Keine Berechtigung, Gruppen zu löschen.", "danger")
|
||||
return redirect(url_for("groups"))
|
||||
group_id = request.form.get("delete_group")
|
||||
@@ -2318,6 +2377,8 @@ def groups():
|
||||
all_users=all_users,
|
||||
all_users_all=all_users_all,
|
||||
permission_catalog=PERMISSIONS,
|
||||
permission_row_types=PERMISSION_ROW_TYPES,
|
||||
permission_labels=PERMISSION_LABELS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user