Lizenzserver 2.8.13: Ticket um Tage verlaengern, Lizenz auf das Ticket setzen
This commit is contained in:
1 parent
641ea22b3c
commit
98c2b2aa14
6 files changed
+217
-2
No files matched your search
@@ -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">
|
||||
|
||||
Reference in new issue
Block a user