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]
|
[project]
|
||||||
name = "tesm-license"
|
name = "tesm-license"
|
||||||
version = "2.8.12"
|
version = "2.8.13"
|
||||||
description = "TESM-Lizenzserver -- Kunden, Tickets, Lizenzen, Aktivierung"
|
description = "TESM-Lizenzserver -- Kunden, Tickets, Lizenzen, Aktivierung"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -78,6 +78,9 @@ def detail(license_id: str):
|
|||||||
"license/license_detail.html",
|
"license/license_detail.html",
|
||||||
license=row,
|
license=row,
|
||||||
state=issuance.display_state(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=document,
|
||||||
document_json=json.dumps(document, indent=2, ensure_ascii=False, sort_keys=True),
|
document_json=json.dumps(document, indent=2, ensure_ascii=False, sort_keys=True),
|
||||||
deliveries=Database.all(
|
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")
|
@bp.post("/<license_id>/resign")
|
||||||
@permission_required("license_renew.execute")
|
@permission_required("license_renew.execute")
|
||||||
@reauth_required
|
@reauth_required
|
||||||
|
|||||||
@@ -217,6 +217,56 @@ def update(ticket_id: str):
|
|||||||
return redirect(url_for("tickets.detail", ticket_id=ticket_id))
|
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")
|
@bp.post("/<ticket_id>/lock")
|
||||||
@permission_required("tickets.write")
|
@permission_required("tickets.write")
|
||||||
def toggle_block(ticket_id: str):
|
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(
|
def lizenz_dem_ticket_nachziehen(
|
||||||
conn: sqlite3.Connection, ticket_id: str, *, master_private_key: str
|
conn: sqlite3.Connection, ticket_id: str, *, master_private_key: str
|
||||||
) -> dict[str, Any] | None:
|
) -> 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]:
|
def dashboard_summary(conn: sqlite3.Connection) -> dict[str, Any]:
|
||||||
rows = list_licenses(conn)
|
rows = list_licenses(conn)
|
||||||
buckets: dict[str, list[dict[str, Any]]] = {"red": [], "orange": [], "green": []}
|
buckets: dict[str, list[dict[str, Any]]] = {"red": [], "orange": [], "green": []}
|
||||||
|
|||||||
@@ -130,13 +130,43 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{{ field('extend_days', 'Laufzeit verlängern (Tage)', type='number',
|
{{ field('extend_days', 'Laufzeit verlängern (Tage)', type='number',
|
||||||
placeholder='0',
|
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>
|
||||||
<div class="card__footer">
|
<div class="card__footer">
|
||||||
<button class="btn btn--primary" type="submit">Neue Revision signieren</button>
|
<button class="btn btn--primary" type="submit">Neue Revision signieren</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</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 %}
|
{% endif %}
|
||||||
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
|
|||||||
@@ -189,6 +189,7 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ field('valid_days', 'Laufzeit (Tage)', value=ticket.valid_days, type='number',
|
{{ 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')) }}
|
disabled=not current_user.has('tickets.write')) }}
|
||||||
{{ checkbox('lifetime', 'Unbefristet', checked=ticket.lifetime,
|
{{ checkbox('lifetime', 'Unbefristet', checked=ticket.lifetime,
|
||||||
disabled=not current_user.has('tickets.write')) }}
|
disabled=not current_user.has('tickets.write')) }}
|
||||||
@@ -208,6 +209,34 @@
|
|||||||
</form>
|
</form>
|
||||||
</section>
|
</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') %}
|
{% if current_user.has('tickets.write') or current_user.has('tickets.delete') %}
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="card__header">
|
<div class="card__header">
|
||||||
|
|||||||
Reference in new issue
Block a user