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:
@@ -109,34 +109,43 @@ Ein Benutzer kann mehreren Gruppen angehören, die Rechte addieren sich
|
||||
|
||||
### Zweistufiger Rechtebaum
|
||||
|
||||
Der Baum hat genau zwei Ebenen, exakt gespiegelt an den drei Sidebar-Gruppen:
|
||||
Der Baum hat genau zwei Ebenen, exakt gespiegelt an den drei Sidebar-Gruppen.
|
||||
Pro Bereich gibt es eine kompakte Tabelle: Zeilen = der Bereich selbst
|
||||
„(gesamt)“ + je ein Unterpunkt, Spalten = die Rechte **R**(ead/Lesen),
|
||||
**W**(rite/Schreiben, Anlegen), **E**(dit/Ändern) nebeneinander. **„Ändern“
|
||||
deckt Löschen überall mit ab** — es gibt bewusst kein eigenes
|
||||
Löschen-Recht. Eine vierte Spalte **D** existiert nur bei Clients und
|
||||
bedeutet dort **nicht** Löschen, sondern *PoE-Neustart über das
|
||||
Dashboard-Popup auslösen* (automatische Neustarts bei Ausfall über
|
||||
`poe.sh` sind davon komplett unabhängig und laufen immer). Für Zeilen/Spalten
|
||||
ohne passendes Recht (z.B. „W“ beim Bereich selbst, oder „D“ bei
|
||||
Switche/Zugangsdaten/Benutzer/Gruppen) wird eine ausgegraute, nicht
|
||||
anklickbare Checkbox angezeigt statt die Zelle wegzulassen:
|
||||
|
||||
```
|
||||
Geräte (devices_group.view — Bereich an/aus)
|
||||
├─ Clients Lesen · Schreiben (Anlegen) · Ändern (inkl. Akt./Deakt.) · Löschen · PoE-Neustart
|
||||
├─ Switche Lesen · Schreiben · Ändern · Löschen
|
||||
└─ Zugangsdaten Lesen · Schreiben · Ändern · Löschen
|
||||
Logs (logs_group.view — Bereich an/aus)
|
||||
├─ Live Lesen
|
||||
└─ Änderungen Lesen
|
||||
Einstellungen (settings_group.view — Bereich an/aus)
|
||||
├─ Benutzer Lesen · Schreiben · Ändern · Löschen
|
||||
├─ Gruppen Lesen · Schreiben · Ändern · Löschen
|
||||
├─ Systemeinstellungen Lesen · Ändern
|
||||
└─ Im-/Export Lesen (Export) · Ändern (Import)
|
||||
Geräte (gesamt) R
|
||||
├─ Clients R · W · E · D (D = PoE-Neustart über Dashboard)
|
||||
├─ Switche R · W · E
|
||||
└─ Zugangsdaten R · W · E
|
||||
Logs (gesamt) R
|
||||
├─ Live R
|
||||
└─ Änderungen R
|
||||
Einstellungen (gesamt) R
|
||||
├─ Benutzer R · W · E
|
||||
├─ Gruppen R · W · E
|
||||
├─ Systemeinstellungen R · E
|
||||
└─ Im-/Export R (Export) · E (Import)
|
||||
```
|
||||
|
||||
Das jeweilige „Bereich an/aus“-Recht (`devices_group.view` /
|
||||
`logs_group.view` / `settings_group.view`) wirkt als **Kill-Switch**: ist es
|
||||
für eine Gruppe nicht gesetzt, greift kein einziges Recht darunter mehr —
|
||||
selbst wenn z.B. `devices.view` einzeln noch angehakt ist. So lässt sich ein
|
||||
ganzer Bereich mit einem Klick sperren, ohne jedes Unterrecht einzeln
|
||||
zurücknehmen zu müssen (`User.has_permission()` in `app.py`).
|
||||
|
||||
Auf der **Gruppen**-Seite wird der Baum als Akkordeon pro Gruppe angezeigt:
|
||||
eine Zeile pro Top-Level-Bereich mit eigenem Kästchen, darunter eingerückt
|
||||
die Unterpunkte mit ihren Einzelrechten — ohne den Bereichsnamen in jedem
|
||||
Unterpunkt zu wiederholen.
|
||||
`logs_group.view` / `settings_group.view`, die „(gesamt)“-Zeile) wirkt als
|
||||
**Kill-Switch**: ist es für eine Gruppe nicht gesetzt, greift kein einziges
|
||||
Recht darunter mehr — selbst wenn z.B. `devices.view` einzeln noch angehakt
|
||||
ist. So lässt sich ein ganzer Bereich mit einem Klick sperren, ohne jedes
|
||||
Unterrecht einzeln zurücknehmen zu müssen (`User.has_permission()` in
|
||||
`app.py`). Auf der Gruppen-Seite sind die Unterpunkt-Zeilen einer Tabelle per
|
||||
JavaScript so lange gesperrt (ausgegraut, nicht anklickbar), bis die
|
||||
„(gesamt)“-Zeile angehakt ist.
|
||||
|
||||
### Systemgruppen
|
||||
|
||||
@@ -150,8 +159,8 @@ beiden frei verwaltbar:
|
||||
angelegte Nicht-Admin automatisch zugeordnet wird. Fester Rechtesatz:
|
||||
Geräte-Bereich + Clients/Switche lesen, Logs-Bereich + Live-Log lesen.
|
||||
|
||||
Alle anderen Gruppen sind vom jeweiligen Rechteinhaber (`groups.edit` bzw.
|
||||
`groups.create`/`groups.delete`) frei konfigurierbar.
|
||||
Alle anderen Gruppen sind vom jeweiligen Rechteinhaber (`groups.edit`, das
|
||||
auch Anlegen und Löschen von Gruppen umfasst) frei konfigurierbar.
|
||||
|
||||
### Eskalationsschutz
|
||||
|
||||
@@ -174,10 +183,12 @@ Formulare) ist an das jeweilige Recht gekoppelt — im Frontend ausgeblendet
|
||||
Datenmodell: `groups` (inkl. `is_default`- und `is_system`-Flag),
|
||||
`group_permissions` (Gruppe → Recht), `user_groups` (Benutzer → Gruppe).
|
||||
Bestehende Datenbanken werden beim App-Start automatisch migriert
|
||||
(`_ensure_schema()` in `app.py`, u.a. `devices.toggle` → `devices.edit`,
|
||||
(`_ensure_schema()` in `app.py`): `devices.toggle` → `devices.edit`,
|
||||
`switches.*` → gespiegelte `credentials.*`, nachträgliches Setzen der neuen
|
||||
Bereichs-Rechte für bereits vergebene Unterrechte) — läuft **nur einmalig**
|
||||
über einen Guard in der `settings`-Tabelle, damit ein bewusst deaktiviertes
|
||||
Bereichs-Rechte für bereits vergebene Unterrechte, sowie (spätere Revision)
|
||||
sämtliche `*.delete`-Rechte → `*.edit` (Löschen wurde nachträglich überall in
|
||||
Ändern gefaltet). Jede dieser Migrationen läuft **nur einmalig** über einen
|
||||
eigenen Guard in der `settings`-Tabelle, damit z.B. ein bewusst deaktiviertes
|
||||
Bereichs-Recht nicht bei jedem Neustart automatisch wieder gesetzt wird.
|
||||
|
||||
## Zugangsdaten (wiederverwendbare SSH-Logins)
|
||||
|
||||
+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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1043,26 +1043,42 @@ select {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Zweistufiger Rechte-Baum (Gruppen-Detailansicht): eine Zeile pro
|
||||
Top-Level-Bereich (Geräte/Logs/Einstellungen) mit eigenem "Bereich
|
||||
anzeigen"-Kästchen, darunter eingerückt die Unterpunkte. */
|
||||
.permission-area { margin-bottom: 22px; }
|
||||
/* Rechte-Tabelle (Gruppen-Detailansicht): eine kompakte Tabelle pro
|
||||
Top-Level-Bereich (Geräte/Logs/Einstellungen). Erste Zeile = der Bereich
|
||||
selbst "(gesamt)" (nur die R-Spalte aktiv, das ist der Kill-Switch),
|
||||
weitere Zeilen = je ein Unterpunkt (Clients, Switche, ...). Spalten =
|
||||
R/W/E/D nebeneinander (siehe Legende im Seitenkopf). */
|
||||
.permission-area { margin-bottom: 20px; }
|
||||
.permission-area:last-child { margin-bottom: 0; }
|
||||
.permission-area-toggle {
|
||||
font-size: 14.5px;
|
||||
padding-bottom: 8px;
|
||||
margin-bottom: 14px;
|
||||
.permission-table {
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.permission-table th, .permission-table td {
|
||||
padding: 7px 14px;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid var(--border-soft);
|
||||
}
|
||||
.permission-area-toggle strong { font-weight: 700; }
|
||||
.permission-area-children {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
padding-left: 22px;
|
||||
border-left: 2px solid var(--border-soft);
|
||||
margin-left: 5px;
|
||||
.permission-table th {
|
||||
font-size: 11.5px;
|
||||
font-weight: 650;
|
||||
color: var(--text-faint);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.permission-table th:first-child { text-transform: none; font-family: inherit; letter-spacing: normal; }
|
||||
.permission-table td:first-child, .permission-table th:first-child { text-align: left; padding-right: 20px; }
|
||||
.permission-table tbody tr:last-child td { border-bottom: none; }
|
||||
.permission-row-label { color: var(--text-dim); }
|
||||
.permission-table input[type="checkbox"] {
|
||||
width: 16px; height: 16px;
|
||||
accent-color: var(--accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
.permission-table input[type="checkbox"]:disabled { cursor: not-allowed; }
|
||||
.permission-cb-na { opacity: 0.25; accent-color: var(--text-faint) !important; }
|
||||
|
||||
.check-list { display: flex; flex-direction: column; gap: 9px; }
|
||||
.check-row {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% set active_page = "credentials" %}
|
||||
{% set can_create = current_user.has_permission('credentials.create') %}
|
||||
{% set can_edit = current_user.has_permission('credentials.edit') %}
|
||||
{% set can_delete = current_user.has_permission('credentials.delete') %}
|
||||
{% set can_delete = current_user.has_permission('credentials.edit') %}
|
||||
{% block page_title %}Zugangsdaten{% endblock %}
|
||||
{% block page_sub %}<div class="topbar-sub">{{ credentials|length }} Zugangsdaten</div>{% endblock %}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% set can_toggle = current_user.has_permission('devices.edit') %}
|
||||
{% set can_create = current_user.has_permission('devices.create') %}
|
||||
{% set can_edit = current_user.has_permission('devices.edit') %}
|
||||
{% set can_delete = current_user.has_permission('devices.delete') %}
|
||||
{% set can_delete = current_user.has_permission('devices.edit') %}
|
||||
{% set show_actions_col = can_edit or can_delete %}
|
||||
{% set col_count = 5 + (1 if can_toggle else 0) + (1 if show_actions_col else 0) %}
|
||||
{% block page_title %}Clients{% endblock %}
|
||||
|
||||
@@ -5,38 +5,77 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
{# Rendert den zweistufigen Rechte-Baum: eine Zeile pro Top-Level-Bereich
|
||||
(Geräte/Logs/Einstellungen) mit eigenem "Bereich anzeigen"-Kästchen,
|
||||
darunter eingerückt die Unterpunkte mit ihren jeweiligen Einzelrechten.
|
||||
readonly=true zeigt nur den aktuellen Stand (Admin/Systemgruppen bzw.
|
||||
fehlendes groups.edit), sonst ist der Baum Teil eines <form>. #}
|
||||
{# Rendert einen Bereich (Geräte/Logs/Einstellungen) als kompakte Tabelle:
|
||||
Zeilen = Bereich selbst (nur die R-Spalte aktiv, das ist der Kill-Switch)
|
||||
+ je eine Zeile pro Unterpunkt (Clients, Switche, ...). Spalten = die
|
||||
kanonischen Rechte R/W/E/D nebeneinander (permission_row_types).
|
||||
"Ändern" (E) deckt Löschen überall mit ab — es gibt nirgends ein eigenes
|
||||
Löschen-Recht. "D" existiert nur bei Clients und bedeutet dort NICHT
|
||||
Löschen, sondern PoE-Neustart über das Dashboard-Popup. Zellen ohne
|
||||
passendes Recht für die jeweilige Zeile (z.B. "W" bei "Geräte" selbst,
|
||||
oder "D" bei Switche/Zugangsdaten) werden als ausgegraute, nicht
|
||||
anklickbare Checkbox dargestellt statt zu fehlen — dadurch bleiben alle
|
||||
Spalten bündig untereinander. readonly=true zeigt nur den aktuellen Stand
|
||||
(Admin/Systemgruppen bzw. fehlendes groups.edit). Die JS-Funktion
|
||||
applyPermissionGating() sperrt Kind-Rechte, solange das Bereich-Lesen
|
||||
(Kill-Switch) nicht gesetzt ist. #}
|
||||
{% macro permission_table(group, checked_keys, readonly) %}
|
||||
<div class="permission-area">
|
||||
<div style="overflow-x:auto;">
|
||||
<table class="permission-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ group['label'] }}</th>
|
||||
{% for row_key, row_letter, row_label in permission_row_types %}
|
||||
<th title="{{ row_label }}">{{ row_letter }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="permission-row-label">{{ group['label'] }} (gesamt)</td>
|
||||
{% for row_key, row_letter, row_label in permission_row_types %}
|
||||
<td>
|
||||
{% if row_key == 'view' %}
|
||||
<input type="checkbox" name="permissions" value="{{ group['view_key'] }}"
|
||||
title="{{ permission_labels.get(group['view_key'], group['label']) }}"
|
||||
{% if group['view_key'] in checked_keys %}checked{% endif %}
|
||||
{% if readonly %}disabled{% endif %}
|
||||
class="permission-area-toggle-cb">
|
||||
{% else %}
|
||||
<input type="checkbox" disabled class="permission-cb-na" tabindex="-1">
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% for child_key, child in group['children'].items() %}
|
||||
<tr>
|
||||
<td class="permission-row-label">{{ child['label'] }}</td>
|
||||
{% for row_key, row_letter, row_label in permission_row_types %}
|
||||
{% set perm_key = child['rows'].get(row_key) %}
|
||||
<td>
|
||||
{% if perm_key %}
|
||||
<input type="checkbox" name="permissions" value="{{ perm_key }}"
|
||||
title="{{ permission_labels.get(perm_key, perm_key) }}"
|
||||
{% if perm_key in checked_keys %}checked{% endif %}
|
||||
{% if readonly %}disabled{% endif %}
|
||||
class="permission-child-cb">
|
||||
{% else %}
|
||||
<input type="checkbox" disabled class="permission-cb-na" tabindex="-1">
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro permission_tree(checked_keys, readonly) %}
|
||||
{% for group_key, group in permission_catalog.items() %}
|
||||
<div class="permission-area">
|
||||
<label class="check-row permission-area-toggle" {% if readonly %}style="cursor:default; color:var(--text-faint);"{% endif %}>
|
||||
<input type="checkbox" name="permissions" value="{{ group['view_key'] }}"
|
||||
{% if group['view_key'] in checked_keys %}checked{% endif %}
|
||||
{% if readonly %}disabled{% endif %}>
|
||||
<strong>{{ group['label'] }}</strong>
|
||||
</label>
|
||||
<div class="permission-area-children">
|
||||
{% for child_key, child in group['children'].items() %}
|
||||
<div>
|
||||
<div class="permission-group-title">{{ child['label'] }}</div>
|
||||
<div class="check-list">
|
||||
{% for key, label in child['items'].items() %}
|
||||
<label class="check-row" {% if readonly %}style="cursor:default; color:var(--text-faint);"{% endif %}>
|
||||
<input type="checkbox" name="permissions" value="{{ key }}"
|
||||
{% if key in checked_keys %}checked{% endif %}
|
||||
{% if readonly %}disabled{% endif %}>
|
||||
{{ label }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{{ permission_table(group, checked_keys, readonly) }}
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
|
||||
@@ -47,7 +86,12 @@
|
||||
Über Gruppen lassen sich Rechte für jeden Bereich der App granular vergeben.
|
||||
Ein Benutzer kann mehreren Gruppen angehören — die Rechte addieren sich. Auf „Rechte“ klicken,
|
||||
um eine Gruppe aufzuklappen und die Berechtigungen im Detail zu sehen bzw. zu ändern.
|
||||
„Admin“ und „Benutzer“ sind feste Systemgruppen und nicht veränderbar.
|
||||
„Admin“ und „Benutzer“ sind feste Systemgruppen und nicht veränderbar. Die Zeile „... (gesamt)“
|
||||
muss gelesen werden dürfen, bevor die Zeilen darunter greifen — ausgegraute Kästchen gelten für
|
||||
die jeweilige Zeile nicht.
|
||||
<strong>R</strong> = Lesen (Read) · <strong>W</strong> = Schreiben/Anlegen (Write) ·
|
||||
<strong>E</strong> = Ändern inkl. Löschen (Edit) ·
|
||||
<strong>D</strong> = Dashboard-Neustart (nur bei Clients; automatische Neustarts bei Ausfall bleiben davon unberührt)
|
||||
</div>
|
||||
</div>
|
||||
{% if current_user.has_permission('groups.create') %}
|
||||
@@ -112,7 +156,7 @@
|
||||
<button class="icon-btn" title="Mitglieder verwalten" data-open-modal="membersModal{{ loop.index }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="8" r="3.2"/><path d="M2.5 20c0-3.6 2.9-6 6.5-6s6.5 2.4 6.5 6"/><circle cx="17.5" cy="8.5" r="2.4"/><path d="M15.8 14.2c2.7.3 4.7 2.4 4.7 5.3"/></svg>
|
||||
</button>
|
||||
{% if current_user.has_permission('groups.delete') and not g.is_default and not g.is_system %}
|
||||
{% if current_user.has_permission('groups.edit') and not g.is_default and not g.is_system %}
|
||||
<form method="post" data-confirm="Gruppe „{{ g.name }}“ wirklich löschen? Mitglieder verlieren die zugehörigen Rechte.">
|
||||
<input type="hidden" name="delete_group" value="{{ g.id }}">
|
||||
<button type="submit" class="icon-btn" style="color:var(--danger);" title="Löschen">
|
||||
@@ -254,5 +298,28 @@ function toggleDetail(id) {
|
||||
const chev = document.getElementById(id.replace("detail-", "chev-"));
|
||||
if (chev) chev.style.transform = row.classList.contains("hidden") ? "" : "rotate(180deg)";
|
||||
}
|
||||
|
||||
// Kind-Rechte einer Kategorie sind erst vergebbar, wenn das übergeordnete
|
||||
// "Bereich anzeigen"-Recht (Kill-Switch, erste Spalte/L-Zeile) gesetzt ist —
|
||||
// spiegelt serverseitig User.has_permission() (PERMISSION_PARENT_GROUP).
|
||||
// Bearbeitbare Tabellen bekommen einen Live-Listener, Nur-Lese-Tabellen
|
||||
// (Admin/Systemgruppen) bleiben unangetastet, deren Checkboxen sind ohnehin
|
||||
// alle disabled.
|
||||
function applyPermissionGating() {
|
||||
document.querySelectorAll(".permission-area").forEach(function (area) {
|
||||
const toggle = area.querySelector(".permission-area-toggle-cb");
|
||||
if (!toggle || toggle.disabled) return;
|
||||
const children = area.querySelectorAll(".permission-child-cb");
|
||||
const sync = function () {
|
||||
children.forEach(function (cb) {
|
||||
cb.disabled = !toggle.checked;
|
||||
if (!toggle.checked) cb.checked = false;
|
||||
});
|
||||
};
|
||||
toggle.addEventListener("change", sync);
|
||||
sync();
|
||||
});
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", applyPermissionGating);
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% endblock %}
|
||||
{% set can_create = current_user.has_permission('switches.create') %}
|
||||
{% set can_edit = current_user.has_permission('switches.edit') %}
|
||||
{% set can_delete = current_user.has_permission('switches.delete') %}
|
||||
{% set can_delete = current_user.has_permission('switches.edit') %}
|
||||
{% block page_title %}Switche{% endblock %}
|
||||
{% block page_sub %}<div class="topbar-sub">{{ switches|length }} Switche</div>{% endblock %}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="2.5"/><circle cx="6" cy="12" r="2.5"/><circle cx="18" cy="19" r="2.5"/><path d="M8.2 10.7l7.6-4.4M8.2 13.3l7.6 4.4"/></svg>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if current_user.has_permission('users.delete') and may_touch_target %}
|
||||
{% if current_user.has_permission('users.edit') and may_touch_target %}
|
||||
<form method="post" data-confirm="Willst du „{{ u['username'] }}“ wirklich löschen?">
|
||||
<button type="submit" name="delete_user" value="{{ u['id'] }}" class="icon-btn" style="color:var(--danger);" title="Löschen">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0l-1 14a2 2 0 01-2 2H7a2 2 0 01-2-2L4 6"/></svg>
|
||||
|
||||
Reference in New Issue
Block a user