Lizenzserver 2.8.13: Ticket um Tage verlaengern, Lizenz auf das Ticket setzen

This commit is contained in:
alientim committed 2026-09-16 22:19:12 +02:00
1 parent 641ea22b3c
commit 98c2b2aa14
6 files changed
+217 -2

No files matched your search

+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "tesm-license"
version = "2.8.12"
version = "2.8.13"
description = "TESM-Lizenzserver -- Kunden, Tickets, Lizenzen, Aktivierung"
requires-python = ">=3.11"
dependencies = [
@@ -78,6 +78,9 @@ 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.
ticket_grenze=issuance.ticket_grenze(conn, license_id),
document=document,
document_json=json.dumps(document, indent=2, ensure_ascii=False, sort_keys=True),
deliveries=Database.all(
@@ -181,6 +184,48 @@ def download(license_id: str):
)
@bp.post("/<license_id>/align-ticket")
@permission_required("license_renew.execute")
@reauth_required
def align_ticket(license_id: str):
"""Bringt die Laufzeit der Lizenz mit ihrem Ticket zur Deckung.
In beide Richtungen. Verkuerzt sie, ist das eine Ruecknahme von Laufzeit --
deshalb nur auf ausdruecklichen Zugriff, mit Rueckfrage in der Oberflaeche
und mit Zeile im Aenderungsprotokoll.
"""
conn = db()
if issuance.get_license(conn, license_id) is None:
abort(404)
key = signing_key()
try:
with transaction():
neu = issuance.lizenz_auf_ticket_setzen(
conn, license_id, master_private_key=key.private_key
)
except (issuance.IssuanceError, ValueError) as exc:
flash(str(exc), "danger")
return redirect(url_for("licenses.detail", license_id=license_id))
if neu is None:
flash("Die Laufzeit entspricht bereits dem Ticket.", "info")
return redirect(url_for("licenses.detail", license_id=license_id))
log_event(
"license.resigned",
target=license_id[:12],
detail=f"Revision {neu['seq']}: Laufzeit auf die Ticketlaufzeit gesetzt",
severity="warning",
)
reporting.report_soon(core())
flash(
f"Neue Revision {neu['seq']}: Die Laufzeit entspricht jetzt dem Ticket. Der Client "
"erhält sie beim nächsten Heartbeat.",
"success",
)
return redirect(url_for("licenses.detail", license_id=license_id))
@bp.post("/<license_id>/resign")
@permission_required("license_renew.execute")
@reauth_required
@@ -217,6 +217,56 @@ def update(ticket_id: str):
return redirect(url_for("tickets.detail", ticket_id=ticket_id))
@bp.post("/<ticket_id>/extend")
@permission_required("tickets.write")
def extend(ticket_id: str):
"""Schlaegt Tage auf die Laufzeit des Tickets -- und zieht die Lizenz nach."""
conn = db()
if issuance.get_ticket(conn, ticket_id) is None:
abort(404)
try:
tage = int(request.form.get("days") or 0)
except ValueError:
flash("Die Verlängerung muss eine Zahl sein.", "danger")
return redirect(url_for("tickets.detail", ticket_id=ticket_id))
try:
with transaction():
neu = issuance.ticket_verlaengern(conn, ticket_id, tage)
nachgezogen = issuance.lizenz_dem_ticket_nachziehen(
conn, ticket_id, master_private_key=signing_key().private_key
)
except (issuance.IssuanceError, ValueError) as exc:
flash(str(exc), "danger")
return redirect(url_for("tickets.detail", ticket_id=ticket_id))
log_event(
"ticket.updated",
target=ticket_id[:12],
detail=f"Laufzeit +{tage} Tage (jetzt {neu})",
severity="notice",
scope="ticket",
)
if nachgezogen is not None:
log_event(
"license.resigned",
target=str(nachgezogen["license_id"])[:12],
detail=f"Revision {nachgezogen['seq']}: Laufzeit dem Ticket angeglichen",
severity="notice",
)
reporting.report_soon(core())
flash(
f"Ticket auf {neu} Tage verlängert."
+ (
f" Die laufende Lizenz wurde mitverlängert (Revision {nachgezogen['seq']})."
if nachgezogen is not None
else ""
),
"success",
)
return redirect(url_for("tickets.detail", ticket_id=ticket_id))
@bp.post("/<ticket_id>/lock")
@permission_required("tickets.write")
def toggle_block(ticket_id: str):
@@ -814,6 +814,28 @@ def display_state(row: dict[str, Any], *, now: datetime | None = None) -> dict[s
}
def ticket_verlaengern(conn: sqlite3.Connection, ticket_id: str, tage: int) -> int:
"""Schlaegt Tage auf die Laufzeit des Tickets auf. Rueckgabe: die neue Zahl.
Dasselbe Bedienmuster wie bei der Lizenz ("+ X Tage"), und aus demselben
Grund: wer verlaengert, rechnet in Tagen ab heute und nicht in einer neuen
Gesamtzahl, die er sich erst aus dem alten Wert zusammensuchen muss.
"""
ticket = get_ticket(conn, ticket_id)
if ticket is None:
raise IssuanceError("Dieses Ticket existiert nicht.")
if bool(ticket["lifetime"]):
raise IssuanceError("Dieses Ticket ist unbefristet -- da gibt es nichts zu verlängern.")
if int(tage) <= 0:
raise IssuanceError("Die Verlängerung muss eine Zahl größer als null sein.")
neu = int(ticket["valid_days"]) + int(tage)
conn.execute(
"UPDATE license_tickets SET valid_days=?, updated_at=? WHERE id=?",
(neu, _now(), ticket_id),
)
return neu
def lizenz_dem_ticket_nachziehen(
conn: sqlite3.Connection, ticket_id: str, *, master_private_key: str
) -> dict[str, Any] | None:
@@ -864,6 +886,45 @@ def lizenz_dem_ticket_nachziehen(
)
def lizenz_auf_ticket_setzen(
conn: sqlite3.Connection, license_id: str, *, master_private_key: str
) -> dict[str, Any] | None:
"""Setzt die Laufzeit einer Lizenz auf das, was ihr Ticket hergibt.
In beide Richtungen -- das ist der Unterschied zum Nachziehen bei einer
Ticketverlaengerung, das nur verlaengert. Hier greift jemand ausdruecklich
zu, um Anspruch und Urkunde wieder zur Deckung zu bringen: eine Lizenz, die
laenger laeuft als ihr Ticket, ist eine Zusage ohne Grundlage.
**Das verkuerzt eine bereits ausgestellte Zusage.** Der Client uebernimmt
die neue Revision beim naechsten Heartbeat, und danach laeuft die
Installation frueher ab. Deshalb passiert es nur auf ausdruecklichen
Zugriff und nie nebenbei.
Rueckgabe: die neue Revision, oder ``None``, wenn nichts zu tun war.
"""
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:
return None
return resign_license(
conn,
license_id,
master_private_key=master_private_key,
changes={"expires_at": grenze},
ueber_ticket=True,
)
def dashboard_summary(conn: sqlite3.Connection) -> dict[str, Any]:
rows = list_licenses(conn)
buckets: dict[str, list[dict[str, Any]]] = {"red": [], "orange": [], "green": []}
@@ -130,13 +130,43 @@
{% endif %}
{{ field('extend_days', 'Laufzeit verlängern (Tage)', type='number',
placeholder='0',
hint_text='Wird auf das aktülle Ablaufdatum aufgeschlagen.') }}
hint_text=('Höchstens bis zum Ende der Ticketlaufzeit (' ~ (ticket_grenze | dt) ~ ') -- weiter geht es nur über das Ticket.') if ticket_grenze else 'Wird auf das aktuelle Ablaufdatum aufgeschlagen.') }}
</div>
<div class="card__footer">
<button class="btn btn--primary" type="submit">Neue Revision signieren</button>
</div>
</form>
</section>
{# 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 %}
<section class="card">
<div class="card__header">
<div class="flex-1">
<div class="card__title">{{ icon('shield', size=17) }} Laufzeit weicht vom Ticket ab</div>
<div class="card__sub">
Diese Lizenz läuft bis <strong>{{ license.expires_at | dt }}</strong>, das Ticket
gewährt bis <strong>{{ ticket_grenze | dt }}</strong>.
</div>
</div>
</div>
<form method="post"
action="{{ url_for('licenses.align_ticket', license_id=license.license_id) }}">
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
<div class="card__footer">
<button class="btn" type="submit"
data-confirm="Die Laufzeit dieser Lizenz auf {{ ticket_grenze | dt }} setzen? {% if ticket_grenze < license.expires_at %}Das nimmt bereits zugesagte Laufzeit zurück -- der Client übernimmt es beim nächsten Heartbeat.{% endif %}"
data-confirm-title="Auf die Ticketlaufzeit setzen"
data-confirm-ok="Setzen"
data-confirm-style="{{ 'danger' if ticket_grenze < license.expires_at else 'primary' }}">
Auf die Ticketlaufzeit setzen
</button>
</div>
</form>
</section>
{% endif %}
{% endif %}
<section class="card">
@@ -189,6 +189,7 @@
</fieldset>
{% endif %}
{{ field('valid_days', 'Laufzeit (Tage)', value=ticket.valid_days, type='number',
hint_text='Gesamtlaufzeit ab Ausstellung. Aufschlagen geht unten.',
disabled=not current_user.has('tickets.write')) }}
{{ checkbox('lifetime', 'Unbefristet', checked=ticket.lifetime,
disabled=not current_user.has('tickets.write')) }}
@@ -208,6 +209,34 @@
</form>
</section>
{# Dasselbe Bedienmuster wie bei der Lizenz: wer verlängert, rechnet in
Tagen ab jetzt und nicht in einer neuen Gesamtzahl, die er sich erst aus
dem alten Wert zusammensuchen muss. Und weil das Ticket der Anspruch
ist, zieht die laufende Lizenz gleich mit. #}
{% if current_user.has('tickets.write') and not ticket.lifetime %}
<section class="card">
<div class="card__header">
<div class="flex-1">
<div class="card__title">{{ icon('clock', size=17) }} Laufzeit verlängern</div>
<div class="card__sub">
Schlägt Tage auf die {{ ticket.valid_days }} Tage dieses Tickets auf.
Eine laufende Lizenz wird dabei mitverlängert – sie erhält eine neue
Revision und übernimmt sie beim nächsten Heartbeat.
</div>
</div>
</div>
<form method="post" action="{{ url_for('tickets.extend', ticket_id=ticket.id) }}">
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
<div class="card__body">
{{ field('days', 'Verlängern um (Tage)', type='number', placeholder='30') }}
</div>
<div class="card__footer">
<button class="btn btn--primary" type="submit">Verlängern</button>
</div>
</form>
</section>
{% endif %}
{% if current_user.has('tickets.write') or current_user.has('tickets.delete') %}
<section class="card">
<div class="card__header">