From 0024a116cefd4801698da5e80959ee050202fab1 Mon Sep 17 00:00:00 2001 From: Tim Eertmoed Date: Wed, 16 Sep 2026 22:40:32 +0200 Subject: [PATCH] Lizenzserver 2.8.15: auch eine unbefristete Lizenz laesst sich zurueckholen --- apps/tesm-license/pyproject.toml | 2 +- .../src/tesm_license/blueprints/licenses.py | 15 +++- .../src/tesm_license/services/issuance.py | 74 ++++++++++++++++--- .../templates/license/license_detail.html | 14 ++-- 4 files changed, 85 insertions(+), 20 deletions(-) diff --git a/apps/tesm-license/pyproject.toml b/apps/tesm-license/pyproject.toml index b028cee..1bb1307 100644 --- a/apps/tesm-license/pyproject.toml +++ b/apps/tesm-license/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tesm-license" -version = "2.8.14" +version = "2.8.15" description = "TESM-Lizenzserver -- Kunden, Tickets, Lizenzen, Aktivierung" requires-python = ">=3.11" dependencies = [ diff --git a/apps/tesm-license/src/tesm_license/blueprints/licenses.py b/apps/tesm-license/src/tesm_license/blueprints/licenses.py index 478d88f..4f6a2b3 100644 --- a/apps/tesm-license/src/tesm_license/blueprints/licenses.py +++ b/apps/tesm-license/src/tesm_license/blueprints/licenses.py @@ -78,9 +78,11 @@ def detail(license_id: str): "license/license_detail.html", license=row, state=issuance.display_state(row), - # Bis wohin traegt das Ticket? Die Seite zeigt eine Abweichung an, - # statt sie erst in einer Fehlermeldung auftauchen zu lassen. + # Bis wohin traegt das Ticket, und weicht die Lizenz davon ab? Die + # Seite zeigt es an, statt es erst in einer Fehlermeldung auftauchen zu + # lassen. ticket_grenze=issuance.ticket_grenze(conn, license_id), + abweichung=issuance.ticket_abweichung(conn, license_id), document=document, document_json=json.dumps(document, indent=2, ensure_ascii=False, sort_keys=True), deliveries=Database.all( @@ -214,8 +216,13 @@ def align_ticket(license_id: str): log_event( "license.resigned", target=license_id[:12], - detail=f"Revision {neu['seq']}: Laufzeit auf die Ticketlaufzeit gesetzt", - severity="warning", + detail=( + f"Revision {neu['seq']}: Laufzeit auf die Ticketlaufzeit gesetzt " + f"({'unbefristet' if neu.get('lifetime') else neu.get('expires_at')})" + ), + # Was etwas wegnimmt, steht als Warnung im Protokoll -- was etwas gibt, + # als Notiz. Beides ist derselbe Knopf, aber nicht dasselbe Ereignis. + severity="notice" if neu.get("lifetime") else "warning", ) reporting.report_soon(core()) flash( diff --git a/apps/tesm-license/src/tesm_license/services/issuance.py b/apps/tesm-license/src/tesm_license/services/issuance.py index 7487a6e..c6df816 100644 --- a/apps/tesm-license/src/tesm_license/services/issuance.py +++ b/apps/tesm-license/src/tesm_license/services/issuance.py @@ -893,6 +893,54 @@ def lizenz_dem_ticket_nachziehen( ) +def ticket_abweichung(conn: sqlite3.Connection, license_id: str) -> dict[str, Any] | None: + """Weicht die Laufzeit dieser Lizenz von ihrem Ticket ab? Sonst ``None``. + + Vier Faelle, und sie sind nicht symmetrisch: + + =================== =================== ==================================== + Ticket Lizenz Abgleich + =================== =================== ==================================== + unbefristet unbefristet nichts zu tun + unbefristet befristet wird unbefristet -- ein Gewinn + befristet unbefristet wird befristet -- eine **Ruecknahme** + befristet befristet auf den Tag des Tickets + =================== =================== ==================================== + + ``verkuerzt`` sagt, ob der Abgleich etwas wegnimmt. Die Oberflaeche faerbt + danach ihre Rueckfrage, und das Aenderungsprotokoll seine Stufe: eine + unbefristete Zusage zu befristen ist die schaerfste Form davon. + + Testlizenzen bleiben aussen vor -- sie leben nach eigenen Regeln. + """ + row = get_license(conn, license_id) + if row is None or not row["ticket_id"] or str(row["type"]) == "trial": + return None + ticket = get_ticket(conn, str(row["ticket_id"])) + if ticket is None: + return None + + ticket_unbefristet = bool(ticket["lifetime"]) + lizenz_unbefristet = bool(row["lifetime"]) + if ticket_unbefristet and lizenz_unbefristet: + return None + if ticket_unbefristet: + return {"grenze": None, "unbefristet": True, "verkuerzt": False} + + grenze = ticket_grenze(conn, license_id) + if grenze is None: + return None + if not lizenz_unbefristet and str(row["expires_at"]) == grenze: + return None + return { + "grenze": grenze, + "unbefristet": False, + # Unbefristet zu befristen nimmt immer etwas weg; sonst entscheidet + # das Datum. + "verkuerzt": lizenz_unbefristet or grenze < str(row["expires_at"]), + } + + def lizenz_auf_ticket_setzen( conn: sqlite3.Connection, license_id: str, *, master_private_key: str ) -> dict[str, Any] | None: @@ -913,21 +961,27 @@ def lizenz_auf_ticket_setzen( row = get_license(conn, license_id) if row is None: raise IssuanceError("Diese Lizenz existiert nicht.") - if bool(row["lifetime"]): - raise IssuanceError("Diese Lizenz ist unbefristet.") - grenze = ticket_grenze(conn, license_id) - if grenze is None: - raise IssuanceError( - "Zu dieser Lizenz gibt es keine begrenzte Ticketlaufzeit, an der sie " - "auszurichten wäre." - ) - if str(row["expires_at"]) == grenze: + abweichung = ticket_abweichung(conn, license_id) + if abweichung is None: return None + + if abweichung["unbefristet"]: + # Das Ticket gewaehrt unbefristet, die Lizenz laeuft ab. Der Abgleich + # gibt etwas -- hier ist nichts zurueckzunehmen. + changes: dict[str, Any] = {"lifetime": True} + else: + # Auch aus einer unbefristeten Lizenz wird hier wieder eine befristete. + # Das ist die schaerfste Ruecknahme, die dieses System kennt, und + # genau deshalb hat sie einen eigenen Knopf mit eigener Rueckfrage -- + # statt unmoeglich zu sein. Unmoeglich hiesse: eine unbefristete + # Zusage, die versehentlich entstanden ist, bleibt fuer immer stehen. + changes = {"lifetime": False, "expires_at": abweichung["grenze"]} + return resign_license( conn, license_id, master_private_key=master_private_key, - changes={"expires_at": grenze}, + changes=changes, ueber_ticket=True, ) diff --git a/apps/tesm-license/src/tesm_license/templates/license/license_detail.html b/apps/tesm-license/src/tesm_license/templates/license/license_detail.html index 730d867..d62ac91 100644 --- a/apps/tesm-license/src/tesm_license/templates/license/license_detail.html +++ b/apps/tesm-license/src/tesm_license/templates/license/license_detail.html @@ -141,14 +141,18 @@ {# Anspruch und Urkunde wieder zur Deckung bringen. In beide Richtungen -- und wenn es verkürzt, ist das eine Rücknahme von Laufzeit: deshalb mit Rückfrage und eigener Zeile im Änderungsprotokoll. #} - {% if ticket_grenze and ticket_grenze != license.expires_at %} + {% if abweichung %}
{{ icon('shield', size=17) }} Laufzeit weicht vom Ticket ab
- Diese Lizenz läuft bis {{ license.expires_at | dt }}, das Ticket - gewährt bis {{ ticket_grenze | dt }}. + Diese Lizenz läuft + {% if license.lifetime %}unbefristet + {% else %}bis {{ license.expires_at | dt }}{% endif %}, + das Ticket gewährt + {% if abweichung.unbefristet %}unbefristet + {% else %}bis {{ abweichung.grenze | dt }}{% endif %}.
@@ -157,10 +161,10 @@