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 %}