From 286a3464f1a7eaf951f32bccec8c80a8f6bd482d Mon Sep 17 00:00:00 2001 From: alientim Date: Sun, 23 Aug 2026 21:33:33 +0200 Subject: [PATCH] Eigenes Recht fuer Tickets (tickets.view/.create/.edit) Tickets liefen im Rechtebaum komplett ueber licenses.view/.create/.edit mit, obwohl Tickets ein eigener, gleichberechtigter Navigationspunkt neben Kunden und Lizenzen ist -- analog zu TESM, wo Clients/Switche/ Zugangsdaten trotz enger fachlicher Verwandtschaft jeweils eigene Rechte haben. Ergaenzt die im vorigen Release (v1.2.4) behobene Lizenz-Rechte-Luecke um diese zweite, gleich gelagerte Luecke. - Neues Rechtepaar tickets.view/tickets.create/tickets.edit (PERMISSIONS/PERMISSION_LABELS), Property can_manage_tickets. - Nav-Sichtbarkeit sowie tickets_list()/ticket_new()/ticket_edit()/ ticket_delete() auf das neue Recht umgestellt. - ticket_detail(): GET-Zugriff jetzt ueber can_manage_tickets. POST jetzt PRO SUB-AKTION geprueft statt eines einzigen Gates: 'toggle_block' (Sperren/Entsperren) -> tickets.edit (neu, Ticket- Domain); 'issue_license' (erstellt eine LIZENZ) bleibt bewusst unveraendert an licenses.edit gebunden, damit sich 'Tickets verwalten' und 'Lizenzen ausstellen' unabhaengig voneinander vergeben lassen, ohne dass irgendjemand automatisch die eine Faehigkeit durch die andere verliert. - tickets.html/ticket_detail.html: can_edit-Variable entsprechend in can_edit_ticket/can_issue_license aufgeteilt. - Migration (_migrated_tickets_permission_v1, einmalig geguarded): jede Gruppe mit licenses.view/.create/.edit bekommt automatisch das gespiegelte tickets.view/.create/.edit dazu -- niemand verliert beim Update stillschweigend eine Faehigkeit, die vorher implizit ueber licenses.* mitlief. - Getestet gegen isolierte Test-DB: Backfill korrekt (Gruppe mit voller Lizenz-Berechtigung bekommt volle Ticket-Berechtigung, Gruppe mit nur .view bekommt nur tickets.view), idempotent, keine Duplikate. PERMISSIONS/PERMISSION_LABELS/NAV_ITEMS erneut vollstaendig konsistent (keine Nav-Keys mehr ohne Rechte-Entsprechung), app.py syntaktisch fehlerfrei kompiliert. Co-Authored-By: Claude Sonnet 5 --- srv/tesm-license/VERSION | 2 +- srv/tesm-license/app.py | 96 ++++++++++++++----- srv/tesm-license/templates/ticket_detail.html | 6 +- 3 files changed, 78 insertions(+), 26 deletions(-) diff --git a/srv/tesm-license/VERSION b/srv/tesm-license/VERSION index e8ea05d..c813fe1 100644 --- a/srv/tesm-license/VERSION +++ b/srv/tesm-license/VERSION @@ -1 +1 @@ -1.2.4 +1.2.5 diff --git a/srv/tesm-license/app.py b/srv/tesm-license/app.py index 387fd3a..a22b86d 100644 --- a/srv/tesm-license/app.py +++ b/srv/tesm-license/app.py @@ -212,6 +212,10 @@ PERMISSIONS = { "label": "Kunden", "rows": {"view": "customers.view", "create": "customers.create", "edit": "customers.edit"}, }, + "tickets": { + "label": "Tickets", + "rows": {"view": "tickets.view", "create": "tickets.create", "edit": "tickets.edit"}, + }, "licenses": { "label": "Lizenzen", "rows": {"view": "licenses.view", "create": "licenses.create", "edit": "licenses.edit"}, @@ -279,6 +283,8 @@ PERMISSION_LABELS = { "licensing_group.view": "Lizenzierung-Bereich anzeigen", "customers.view": "Kunden lesen", "customers.create": "Kunden anlegen", "customers.edit": "Kunden ändern (inkl. Löschen)", + "tickets.view": "Tickets lesen", "tickets.create": "Tickets anlegen", + "tickets.edit": "Tickets ändern (Eckdaten, Sperren/Entsperren, Löschen)", "licenses.view": "Lizenzen und Aktivierungsstatus lesen", "licenses.create": "Lizenzen ausstellen", "licenses.edit": "Lizenzen ändern (Deaktivieren/Widerrufen, erneut versenden)", @@ -355,7 +361,7 @@ def _nav_key_visible(key, user): if key == "index": return True if key == "tickets": - return user.has_permission("licenses.view") + return user.has_permission("tickets.view") if key == "licenses": return user.has_permission("licenses.view") if key == "customers": @@ -544,6 +550,12 @@ class User(UserMixin): ["customers.view", "customers.create", "customers.edit"] ) + @property + def can_manage_tickets(self): + return self.has_any_permission( + ["tickets.view", "tickets.create", "tickets.edit"] + ) + @property def can_manage_licenses(self): return self.has_any_permission( @@ -914,6 +926,38 @@ def _ensure_schema(): "INSERT OR IGNORE INTO settings (key, value) VALUES (?, '1')", (_migration_key_license_perm,) ) + _migration_key_tickets_perm = "_migrated_tickets_permission_v1" + if not conn.execute("SELECT 1 FROM settings WHERE key=?", (_migration_key_tickets_perm,)).fetchone(): + # "Tickets" hatte bislang KEIN eigenes Recht -- Ticketverwaltung + # (anlegen/bearbeiten/sperren/loeschen) lief komplett ueber + # licenses.view/.create/.edit mit, obwohl Tickets ein eigener, + # gleichberechtigter Navigationspunkt neben Kunden und Lizenzen + # ist (genau wie bei TESM Clients/Switche/Zugangsdaten trotz enger + # fachlicher Verwandtschaft jeweils eigene Rechte haben). Backfill: + # jede Gruppe mit licenses.view/.create/.edit bekommt automatisch + # das gespiegelte tickets.view/.create/.edit dazu -- das + # Ausstellen einer Lizenz AUS einem Ticket heraus bleibt bewusst + # weiterhin an licenses.edit gebunden (siehe ticket_detail()), + # nur Sperren/Entsperren/Bearbeiten/Loeschen des Tickets selbst + # wandert auf das neue, unabhaengige Recht. + _license_to_ticket = { + "licenses.view": "tickets.view", + "licenses.create": "tickets.create", + "licenses.edit": "tickets.edit", + } + for license_perm, ticket_perm in _license_to_ticket.items(): + rows = conn.execute( + "SELECT DISTINCT group_id FROM group_permissions WHERE permission=?", (license_perm,) + ).fetchall() + for row in rows: + conn.execute( + "INSERT OR IGNORE INTO group_permissions (group_id, permission) VALUES (?, ?)", + (row["group_id"], ticket_perm), + ) + conn.execute( + "INSERT OR IGNORE INTO settings (key, value) VALUES (?, '1')", (_migration_key_tickets_perm,) + ) + _migration_key_old_logs = "_cleaned_up_legacy_rpi_log_files_v1" if not conn.execute("SELECT 1 FROM settings WHERE key=?", (_migration_key_old_logs,)).fetchone(): for stale_log in glob.glob("/var/log/rpi-*.log"): @@ -2565,7 +2609,7 @@ def ticket_new(): Lizenz mit bestimmten Eckdaten. Erstellt bewusst noch KEINE Lizenz; das übernimmt ein separater Schritt auf der Ticket-Detailseite (durch Admin ODER Kunde per Self-Service).""" - if not current_user.has_permission("licenses.create"): + if not current_user.has_permission("tickets.create"): flash("Keine Berechtigung, Tickets anzulegen.", "danger") return redirect(url_for("index")) conn = get_db_connection() @@ -2636,7 +2680,7 @@ def ticket_new(): @app.route("/tickets/", methods=["GET", "POST"]) @login_required def ticket_detail(ticket_id): - if not current_user.can_manage_licenses: + if not current_user.can_manage_tickets: flash("Keine Berechtigung.", "danger") return redirect(url_for("index")) conn = get_db_connection() @@ -2647,10 +2691,14 @@ def ticket_detail(ticket_id): return redirect(url_for("index")) if request.method == "POST": - if not current_user.has_permission("licenses.edit"): - flash("Keine Berechtigung.", "danger") - elif "issue_license" in request.form: - if not license_active(): + if "issue_license" in request.form: + # Ausstellen erzeugt eine LIZENZ (nicht nur eine Ticket-Aenderung) + # -- bewusst weiterhin ueber das Lizenz-Recht gegated, nicht ueber + # tickets.edit, damit sich "Tickets verwalten" und "Lizenzen + # ausstellen" unabhaengig voneinander vergeben lassen. + if not current_user.has_permission("licenses.edit"): + flash("Keine Berechtigung, Lizenzen auszustellen.", "danger") + elif not license_active(): flash("Der Lizenzserver selbst hat keine gültige Lizenz -- Ausstellen ist deaktiviert.", "danger") elif ticket["status"] != "open": flash("Ticket ist gesperrt.", "danger") @@ -2663,15 +2711,18 @@ def ticket_detail(ticket_id): flash("Lizenz ausgestellt.", "success") return redirect(url_for("license_detail", license_id=license_file["license_id"])) elif "toggle_block" in request.form: - new_status = "blocked" if ticket["status"] == "open" else "open" - conn.execute("UPDATE license_tickets SET status=? WHERE id=?", (new_status, ticket_id)) - conn.commit() - customer = _customer_row(conn, ticket["customer_id"]) - log_action( - "ticket.block" if new_status == "blocked" else "ticket.unblock", - customer["name"] if customer else ticket_id, - ) - flash("Ticket gesperrt." if new_status == "blocked" else "Ticket entsperrt.", "success") + if not current_user.has_permission("tickets.edit"): + flash("Keine Berechtigung, Tickets zu sperren/entsperren.", "danger") + else: + new_status = "blocked" if ticket["status"] == "open" else "open" + conn.execute("UPDATE license_tickets SET status=? WHERE id=?", (new_status, ticket_id)) + conn.commit() + customer = _customer_row(conn, ticket["customer_id"]) + log_action( + "ticket.block" if new_status == "blocked" else "ticket.unblock", + customer["name"] if customer else ticket_id, + ) + flash("Ticket gesperrt." if new_status == "blocked" else "Ticket entsperrt.", "success") conn.close() return redirect(url_for("ticket_detail", ticket_id=ticket_id)) @@ -2691,7 +2742,8 @@ def ticket_detail(ticket_id): master_licensed=license_active(), self_service_url=_customer_facing_url("self_service", ticket_id=ticket_id), portal_request_url=_customer_facing_url("portal_request"), - can_edit=current_user.has_permission("licenses.edit"), + can_edit_ticket=current_user.has_permission("tickets.edit"), + can_issue_license=current_user.has_permission("licenses.edit"), ) @@ -2707,7 +2759,7 @@ def ticket_edit(ticket_id): hier bewusst nicht vorgesehen (das Self-Service-Ticket ist an genau diesen Kunden gebunden, u.a. weil er ueber dessen Portal-Token erreichbar ist).""" - if not current_user.has_permission("licenses.edit"): + if not current_user.has_permission("tickets.edit"): flash("Keine Berechtigung, Tickets zu bearbeiten.", "danger") return redirect(url_for("tickets_list")) conn = get_db_connection() @@ -2760,7 +2812,7 @@ def ticket_delete(ticket_id): System verschwinden). Etwaige bereits widerrufene/deaktivierte Lizenzen darunter sind reine Historie und werden mitgeloescht, sonst blieben verwaiste Zeilen mit einer toten ticket_id zurueck.""" - if not current_user.has_permission("licenses.edit"): + if not current_user.has_permission("tickets.edit"): flash("Keine Berechtigung, Tickets zu löschen.", "danger") return redirect(url_for("tickets_list")) conn = get_db_connection() @@ -3712,7 +3764,7 @@ def tickets_list(): Dashboards auf eine rein informative Kacheluebersicht (siehe index()) lebt die eigentliche Verwaltung hier, eigenstaendig ueber die Navigation erreichbar.""" - if not current_user.has_permission("licenses.view"): + if not current_user.has_permission("tickets.view"): flash("Keine Berechtigung, Tickets anzusehen.", "danger") return redirect(url_for("index")) conn = get_db_connection() @@ -3745,8 +3797,8 @@ def tickets_list(): return render_template( "tickets.html", tickets=tickets_view, type_labels=LICENSE_TYPE_LABELS, module_labels=MODULE_LABELS, all_modules=licensing.ALL_MODULES, license_types=licensing.LICENSE_TYPES, - can_create=current_user.has_permission("licenses.create"), - can_edit=current_user.has_permission("licenses.edit"), + can_create=current_user.has_permission("tickets.create"), + can_edit=current_user.has_permission("tickets.edit"), ) diff --git a/srv/tesm-license/templates/ticket_detail.html b/srv/tesm-license/templates/ticket_detail.html index c733f52..2c65ed0 100644 --- a/srv/tesm-license/templates/ticket_detail.html +++ b/srv/tesm-license/templates/ticket_detail.html @@ -27,7 +27,7 @@ der Kunde fordert ihn selbst über {{ portal_request_url }} an (zeitlich begrenzter Zugang, keine dauerhafte Freigabe). - {% if can_edit %} + {% if can_edit_ticket %}
-{% if can_edit %} +{% if can_edit_ticket %}