From ad0195bab001fcc10d13757d6e49c5febe6f66e6 Mon Sep 17 00:00:00 2001 From: Tim Eertmoed Date: Sat, 12 Sep 2026 00:26:22 +0200 Subject: [PATCH] Version 2.7.4: Unterschriften in der Sicherung, unterschrieben ist abgeschlossen --- apps/tesm/pyproject.toml | 2 +- apps/tesm/src/tesm/blueprints/fileshare.py | 22 ++++++- apps/tesm/src/tesm/services/fileshare.py | 5 +- apps/tesm/src/tesm/services/transfer.py | 49 +++++++++++++++ packages/tesm-core/pyproject.toml | 2 +- .../tesm-core/src/tesm_core/app_factory.py | 2 +- .../static/tesm_core/css/components.css | 61 ++++++++++++++++++- .../tesm_core/static/tesm_core/js/viewer.js | 21 +++++-- .../templates/tesm_core/logs_audit.html | 23 ++++--- packages/tesm-licensing/pyproject.toml | 2 +- 10 files changed, 168 insertions(+), 21 deletions(-) diff --git a/apps/tesm/pyproject.toml b/apps/tesm/pyproject.toml index 6b2827e..c5046eb 100644 --- a/apps/tesm/pyproject.toml +++ b/apps/tesm/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tesm" -version = "2.7.3" +version = "2.7.4" description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen" requires-python = ">=3.11" dependencies = [ diff --git a/apps/tesm/src/tesm/blueprints/fileshare.py b/apps/tesm/src/tesm/blueprints/fileshare.py index 29204af..bf66e3d 100644 --- a/apps/tesm/src/tesm/blueprints/fileshare.py +++ b/apps/tesm/src/tesm/blueprints/fileshare.py @@ -272,6 +272,19 @@ def save_form(token: str): if upload_file is None or not upload_file.filename: return {"error": "Es kam keine Datei an."}, 400 + # Ein unterschriebenes Dokument ist abgeschlossen. Es als Vorlage fuer eine + # neue Fassung zu benutzen hiesse, eine Unterschrift unter einen anderen + # Text zu stellen -- die Oberflaeche blendet das aus, hier wird es + # abgewiesen. + if service.signaturstand(db(), mount, relative).get("status") in { + "unveraendert", + "veraendert", + }: + return { + "error": "Dieses Dokument ist unterschrieben und damit abgeschlossen. " + "Legen Sie eine neue Fassung aus der ursprünglichen Vorlage an." + }, 409 + grenze = uploads.limit_bytes(db()) daten = upload_file.stream.read(grenze + 1) if len(daten) > grenze: @@ -343,10 +356,13 @@ def save_form(token: str): # da: der Name **in** der Unterschrift ist eine Angabe des # Unterschreibenden -- wie auf Papier. Wer die Datei abgelegt hat, steht # ohnehin als Handelnder in der Zeile und ist belegt. - beschreibung = f"{ziel_mount.label}:{wohin} sha256={pruefsumme}" + # Zwei Zeilen: oben, was ein Mensch liest; unten die Pruefsumme, die + # niemand liest, aber jeder vergleichen koennen muss. In einer einzigen + # Zeile drueckte der Hash den Rest aus der Spalte. + kopf = f"{'Signiert' if unterschriften else 'Gespeichert'}: {wohin} ({ziel_mount.label})" if unterschriften: - namen = ", ".join(unterschriften) - beschreibung = f"{beschreibung} -- angegeben unterschrieben von: {namen}" + kopf += f" – angegeben von: {', '.join(unterschriften)}" + beschreibung = f"{kopf}{chr(10)}sha256={pruefsumme}" _audit( "fileshare.form_signed" if unterschriften else "fileshare.form_saved", f"{mount.label}:{relative}", diff --git a/apps/tesm/src/tesm/services/fileshare.py b/apps/tesm/src/tesm/services/fileshare.py index ef2576c..6bcc417 100644 --- a/apps/tesm/src/tesm/services/fileshare.py +++ b/apps/tesm/src/tesm/services/fileshare.py @@ -696,7 +696,10 @@ def signaturdetail_lesen(detail: str) -> dict[str, Any]: von: ``. Gelesen wird sie an zwei Stellen -- beim Uebernehmen alter Eintraege und wenn das Verzeichnis nichts weiss. """ - ort, _, rest = detail.partition(" sha256=") + # Der Trenner ist heute ein Zeilenumbruch, frueher ein Leerzeichen -- die + # Zerlegung muss beides vertragen, sonst sind alte Eintraege stumm. + ort, _, rest = detail.partition("sha256=") + ort = ort.strip() label, _, pfad = ort.partition(":") summe = rest.split(" ", 1)[0].strip() if rest else "" return { diff --git a/apps/tesm/src/tesm/services/transfer.py b/apps/tesm/src/tesm/services/transfer.py index e6aaa79..645096e 100644 --- a/apps/tesm/src/tesm/services/transfer.py +++ b/apps/tesm/src/tesm/services/transfer.py @@ -64,6 +64,13 @@ CATEGORIES: tuple[Category, ...] = ( Category("users", "Benutzer", "Lokale Konten mit Passwort-Hash", admin_only=True), Category("groups", "Gruppen und Rechte", "Rechtematrix", admin_only=True), Category("settings", "Einstellungen", "Laufzeiteinstellungen der Anwendung", admin_only=True), + Category( + "signatures", + "Unterschriften", + "Pruefsummen unterschriebener Dokumente. Ohne sie sagt die Vorschau nach einer " + "Wiederherstellung zu jeder alten Fassung \"unbekannt\" – auch zu einer " + "veraenderten.", + ), ) CATEGORY_BY_KEY = {category.key: category for category in CATEGORIES} @@ -186,6 +193,16 @@ def build_export( # Sicherung -- sonst wuerde ein Restore eine fremde Aktivierung erben. if not str(row["key"]).startswith("license_") } + if "signatures" in wanted: + # Das Verzeichnis der Unterschriften. Der Beleg selbst bleibt das + # Aenderungsprotokoll (Hash-Kette) und das Journal neben dem Archiv -- + # beides wandert nicht in die Sicherung. Ohne diese Zeilen waere nach + # einer Wiederherstellung jede unterschriebene Fassung "unbekannt". + payload["signatures"] = Database.all( + conn, + "SELECT sha256, share_label, path, signed_at, actor, names " + "FROM document_signatures ORDER BY id", + ) return payload @@ -625,6 +642,38 @@ def apply_import( (user_id, int(group_id)), ) + if "signatures" in wanted: + for eintrag in payload.get("signatures") or []: + if not isinstance(eintrag, dict): + continue + summe = str(eintrag.get("sha256") or "").strip() + pfad = str(eintrag.get("path") or "").strip() + if not summe: + continue + # Zweimal einlesen darf nichts verdoppeln: verglichen wird ueber + # Pruefsumme und Ablageort, genau wie beim Journal. + vorhanden = Database.value( + conn, + "SELECT 1 FROM document_signatures WHERE sha256=? AND path=? LIMIT 1", + (summe, pfad), + ) + if vorhanden: + bump("signatures", "skipped") + continue + conn.execute( + "INSERT INTO document_signatures (sha256, share_label, path, signed_at, " + "actor, names, audit_id) VALUES (?,?,?,?,?,?,NULL)", + ( + summe, + str(eintrag.get("share_label") or ""), + pfad, + str(eintrag.get("signed_at") or stamp), + str(eintrag.get("actor") or ""), + str(eintrag.get("names") or ""), + ), + ) + bump("signatures", "created") + if "settings" in wanted: for key, value in (payload.get("settings") or {}).items(): if str(key).startswith("license_"): diff --git a/packages/tesm-core/pyproject.toml b/packages/tesm-core/pyproject.toml index 8c9888f..d6baaec 100644 --- a/packages/tesm-core/pyproject.toml +++ b/packages/tesm-core/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tesm-core" -version = "2.7.3" +version = "2.7.4" description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver" requires-python = ">=3.11" dependencies = [ diff --git a/packages/tesm-core/src/tesm_core/app_factory.py b/packages/tesm-core/src/tesm_core/app_factory.py index 32aae1c..fb892f9 100644 --- a/packages/tesm-core/src/tesm_core/app_factory.py +++ b/packages/tesm-core/src/tesm_core/app_factory.py @@ -148,7 +148,7 @@ def create_base_app( #: Wortmarke, ohne Zeile darunter; die Marke selbst zeigen beide #: Anwendungen. auth_tagline: str = "", - app_version: str = "2.7.3", + app_version: str = "2.7.4", built_in_trusted_keys: tuple[str, ...] = (), enable_license: bool = True, template_folder: str | None = None, diff --git a/packages/tesm-core/src/tesm_core/static/tesm_core/css/components.css b/packages/tesm-core/src/tesm_core/static/tesm_core/css/components.css index 4962bae..5d05daa 100644 --- a/packages/tesm-core/src/tesm_core/static/tesm_core/css/components.css +++ b/packages/tesm-core/src/tesm_core/static/tesm_core/css/components.css @@ -1486,7 +1486,7 @@ font-size: 5.6cqw; line-height: 1.15; color: #111; - border-top: 1px solid rgba(0, 0, 0, 0.35); + border-top: 1px solid rgba(0, 0, 0, 0.85); padding-top: 1px; pointer-events: none; white-space: nowrap; @@ -1581,3 +1581,62 @@ color: var(--danger); background: rgba(201, 42, 42, 0.12); } + +/* -- Tabelle, die schmal zu Bloecken wird -------------------------------- */ +/* Eine Protokollzeile besteht aus sieben Angaben, von denen eine lang ist: + das Detail. Auf einem breiten Schirm passt das nebeneinander. Wird es eng, + ist eine Spalte von zwei Zentimetern fuer eine Pruefsumme keine Loesung -- + dann steht jede Angabe auf ihrer eigenen Zeile, beschriftet. */ + +.table--stapelbar .td--detail { + /* Zeilenumbrueche aus dem Eintrag bleiben stehen: die Pruefsumme steht + unter dem lesbaren Teil, nicht mitten darin. Und ein Hash ohne Trennstelle + darf notfalls hart umbrechen. */ + white-space: pre-line; + overflow-wrap: anywhere; + max-width: 52ch; +} + +@media (max-width: 1100px) { + .table--stapelbar thead { + display: none; + } + + .table--stapelbar, + .table--stapelbar tbody, + .table--stapelbar tr, + .table--stapelbar td { + display: block; + width: auto; + } + + .table--stapelbar tr { + padding: var(--sp-3) 0; + border-bottom: 1px solid var(--border); + } + + .table--stapelbar td { + display: grid; + grid-template-columns: 8.5rem 1fr; + gap: var(--sp-2); + align-items: baseline; + border: 0; + padding: 2px var(--sp-3); + white-space: normal; + } + + .table--stapelbar td::before { + content: attr(data-spalte); + color: var(--text-muted); + font-size: var(--fs-xs); + } + + .table--stapelbar .td--detail { + max-width: none; + } + + /* Leere Zellen brauchen keine Zeile. */ + .table--stapelbar td:empty { + display: none; + } +} diff --git a/packages/tesm-core/src/tesm_core/static/tesm_core/js/viewer.js b/packages/tesm-core/src/tesm_core/static/tesm_core/js/viewer.js index 748832b..8d08add 100644 --- a/packages/tesm-core/src/tesm_core/static/tesm_core/js/viewer.js +++ b/packages/tesm-core/src/tesm_core/static/tesm_core/js/viewer.js @@ -308,11 +308,14 @@ async function nachbearbeiten( color: PDFLib.rgb(0.1, 0.1, 0.1), }); } + // Kraeftig genug, um neben den Linien eines Formulars als eigener Strich + // erkennbar zu bleiben: ein halber Punkt in hellem Grau ging zwischen + // den Tabellenlinien schlicht unter. seite.drawLine({ - start: { x: eintrag.x, y: eintrag.y - 1 }, - end: { x: eintrag.x + textbreite, y: eintrag.y - 1 }, - thickness: 0.5, - color: PDFLib.rgb(0.4, 0.4, 0.4), + start: { x: eintrag.x, y: eintrag.y - 1.5 }, + end: { x: eintrag.x + textbreite, y: eintrag.y - 1.5 }, + thickness: 0.9, + color: PDFLib.rgb(0.15, 0.15, 0.15), }); } } @@ -901,6 +904,7 @@ export function initFilePreview(wurzel = document) { leiste.hidden = art === "text"; if (speichern) speichern.hidden = art !== "pdf"; if (unterschreibenKnopf) unterschreibenKnopf.hidden = art !== "pdf"; + if (speichern) speichern.hidden = art !== "pdf"; if (unterschriftPanel) unterschriftPanel.hidden = true; }; @@ -942,6 +946,15 @@ export function initFilePreview(wurzel = document) { if (!adresse) return; try { const stand = await window.tesm.api(adresse); + if (stand.status === "unveraendert" || stand.status === "veraendert") { + // Ein unterschriebenes Dokument ist abgeschlossen. Wer daraus eine + // neue Fassung machen koennte, koennte auch eine Unterschrift unter + // einen veraenderten Text setzen -- also gibt es hier nichts mehr zu + // bearbeiten. Der Server weist es zusaetzlich ab. + if (unterschreibenKnopf) unterschreibenKnopf.hidden = true; + if (speichern) speichern.hidden = true; + if (zeile) zeile.hidden = true; + } if (stand.status === "unveraendert") { pdfBehaelter.classList.add("viewer__pdf--unveraendert"); if (pruefzeile) { diff --git a/packages/tesm-core/src/tesm_core/templates/tesm_core/logs_audit.html b/packages/tesm-core/src/tesm_core/templates/tesm_core/logs_audit.html index c6a2f0e..80be4d4 100644 --- a/packages/tesm-core/src/tesm_core/templates/tesm_core/logs_audit.html +++ b/packages/tesm-core/src/tesm_core/templates/tesm_core/logs_audit.html @@ -44,8 +44,11 @@ Zurücksetzen + {# "table--stapelbar": schmal wird aus jeder Zeile ein kleiner Block. Vorher + quetschte sich das Detail mit Pruefsumme in eine Spalte von zwei + Zentimetern und lief ueber zehn Zeilen -- lesbar war das nicht. #}
- +
@@ -60,12 +63,16 @@ {% for row in rows %} - - - - - - + + + + + - + {% endfor %} diff --git a/packages/tesm-licensing/pyproject.toml b/packages/tesm-licensing/pyproject.toml index 523725c..0ebe07a 100644 --- a/packages/tesm-licensing/pyproject.toml +++ b/packages/tesm-licensing/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tesm-licensing" -version = "2.7.3" +version = "2.7.4" description = "Ed25519-basiertes Lizenz- und Aktivierungsprotokoll fuer TESM und TESM-Lizenzserver" requires-python = ">=3.11" dependencies = ["cryptography>=42"]
Zeit
{{ row.ts | dt }}{{ row.actor }}{{ row.action }}{{ row.target }}{{ row.detail | audit_detail }} + + {{ row.ts | dt }} + {{ row.actor }}{{ row.action }}{{ row.target }} + {{ row.detail | audit_detail }} + {# Ampel: Info grün, Hinweis gelb, Warnung rot. Vorher war Info grau und Hinweis blau -- dabei war nicht zu sehen, was harmlos ist und was Aufmerksamkeit braucht. "kritisch" bekommt eine @@ -76,7 +83,7 @@ {% elif row.severity == 'notice' %}{{ pill('Hinweis', 'warning') }} {% else %}{{ pill('Info', 'online') }}{% endif %} {{ row.ip or '–' | safe }}{{ row.ip or '–' | safe }}