Version 2.7.1: Signaturverzeichnis ueberlebt die Archivierung
This commit is contained in:
1 parent
ed17bce40a
commit
11a55b20f8
8 files changed
+163
-28
No files matched your search
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tesm"
|
||||
version = "2.7.0"
|
||||
version = "2.7.1"
|
||||
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
||||
@@ -348,6 +348,19 @@ def save_form(token: str):
|
||||
beschreibung,
|
||||
severity="notice",
|
||||
)
|
||||
if unterschriften:
|
||||
# Zusaetzlich ins Signaturverzeichnis: das Protokoll wird irgendwann
|
||||
# archiviert, die Frage "ist diese Datei noch die unterschriebene?"
|
||||
# stellt sich aber noch Jahre spaeter.
|
||||
with transaction() as conn:
|
||||
service.signatur_merken(
|
||||
conn,
|
||||
sha256=pruefsumme,
|
||||
label=ziel_mount.label,
|
||||
pfad=wohin,
|
||||
actor=current_user.username,
|
||||
namen=unterschriften,
|
||||
)
|
||||
return {"message": f"Gespeichert als {wohin}.", "path": wohin, "sha256": pruefsumme}
|
||||
|
||||
|
||||
|
||||
@@ -308,6 +308,69 @@ INSERT OR IGNORE INTO group_permissions (group_id, permission)
|
||||
"""
|
||||
|
||||
|
||||
# Die Pruefung "unveraendert seit der Unterschrift" hing am Aenderungsprotokoll
|
||||
# -- und das wird ab 20.000 Zeilen tageweise in Dateien ausgelagert. Mit dem
|
||||
# ausgelagerten Tag waere die Pruefsumme aus der Datenbank verschwunden und
|
||||
# jede aeltere unterschriebene Fassung waere still zu "unbekannt" geworden.
|
||||
# Schlimmer noch: auch eine **veraenderte** Fassung, denn ohne Vergleichswert
|
||||
# gibt es keinen Widerspruch.
|
||||
#
|
||||
# Deshalb eine eigene, winzige Tabelle: eine Zeile je Unterschrift, nie
|
||||
# archiviert. Sie ist ein Verzeichnis, keine Wahrheit -- die Wahrheit bleibt
|
||||
# das Aenderungsprotokoll mit seiner Hash-Kette, auch im Archiv.
|
||||
V7 = """
|
||||
CREATE TABLE IF NOT EXISTS document_signatures (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sha256 TEXT NOT NULL,
|
||||
share_label TEXT NOT NULL DEFAULT '',
|
||||
path TEXT NOT NULL DEFAULT '',
|
||||
signed_at TEXT NOT NULL,
|
||||
actor TEXT NOT NULL DEFAULT '',
|
||||
names TEXT NOT NULL DEFAULT '',
|
||||
audit_id INTEGER
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_document_signatures_sha
|
||||
ON document_signatures(sha256);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_document_signatures_ort
|
||||
ON document_signatures(share_label, path);
|
||||
"""
|
||||
|
||||
|
||||
def uebernehme_signaturen(conn) -> None: # type: ignore[no-untyped-def]
|
||||
"""Traegt die bereits protokollierten Unterschriften ins Verzeichnis ein.
|
||||
|
||||
Ohne diesen Schritt waeren alle vor dem Update unterschriebenen Fassungen
|
||||
ab sofort "unbekannt" -- richtig waere das nicht, die Eintraege stehen ja
|
||||
noch da.
|
||||
"""
|
||||
from .services import fileshare as service
|
||||
|
||||
zeilen = conn.execute(
|
||||
"SELECT id, ts, actor, detail FROM audit_log WHERE action='fileshare.form_signed' "
|
||||
"ORDER BY id"
|
||||
).fetchall()
|
||||
for zeile in zeilen:
|
||||
detail = str(zeile["detail"] or "")
|
||||
gelesen = service.signaturdetail_lesen(detail)
|
||||
if not gelesen["sha256"]:
|
||||
continue
|
||||
conn.execute(
|
||||
"INSERT INTO document_signatures (sha256, share_label, path, signed_at, actor, "
|
||||
"names, audit_id) VALUES (?,?,?,?,?,?,?)",
|
||||
(
|
||||
gelesen["sha256"],
|
||||
gelesen["label"],
|
||||
gelesen["pfad"],
|
||||
str(zeile["ts"]),
|
||||
str(zeile["actor"] or ""),
|
||||
", ".join(gelesen["namen"]),
|
||||
int(zeile["id"]),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
TESM_MIGRATIONS: tuple[Migration, ...] = (
|
||||
Migration(version=1, name="baseline", sql=V1),
|
||||
Migration(version=2, name="seed-dhcp-options", fn=seed_dhcp_options),
|
||||
@@ -315,4 +378,5 @@ TESM_MIGRATIONS: tuple[Migration, ...] = (
|
||||
Migration(version=4, name="fileshare-pdf-rechte", sql=V4),
|
||||
Migration(version=5, name="terminal-getrennt", sql=V5),
|
||||
Migration(version=6, name="wartung-eigener-bereich", sql=V6),
|
||||
Migration(version=7, name="signaturverzeichnis", sql=V7, fn=uebernehme_signaturen),
|
||||
)
|
||||
@@ -620,6 +620,55 @@ def pruefsumme(pfad: Path) -> str:
|
||||
return kessel.hexdigest()
|
||||
|
||||
|
||||
def signaturdetail_lesen(detail: str) -> dict[str, Any]:
|
||||
"""Zerlegt die Protokollzeile einer Unterschrift.
|
||||
|
||||
Aufbau: ``<Freigabe>:<Pfad> sha256=<Summe> -- angegeben unterschrieben
|
||||
von: <Namen>``. Gelesen wird sie an zwei Stellen -- beim Uebernehmen
|
||||
alter Eintraege und wenn das Verzeichnis nichts weiss.
|
||||
"""
|
||||
ort, _, rest = detail.partition(" sha256=")
|
||||
label, _, pfad = ort.partition(":")
|
||||
summe = rest.split(" ", 1)[0].strip() if rest else ""
|
||||
return {
|
||||
"sha256": summe,
|
||||
"label": label.strip(),
|
||||
"pfad": pfad.strip(),
|
||||
"namen": _namen_aus_detail(detail),
|
||||
}
|
||||
|
||||
|
||||
def signatur_merken(
|
||||
conn: sqlite3.Connection,
|
||||
*,
|
||||
sha256: str,
|
||||
label: str,
|
||||
pfad: str,
|
||||
actor: str,
|
||||
namen: Iterable[str],
|
||||
audit_id: int | None = None,
|
||||
) -> None:
|
||||
"""Haelt eine Unterschrift im Verzeichnis fest.
|
||||
|
||||
Das Aenderungsprotokoll bleibt der Beleg -- mit Hash-Kette und, nach der
|
||||
Archivierung, in einer Datei. Dieses Verzeichnis ist nur der schnelle Weg
|
||||
dorthin und wird nie ausgelagert; eine Zeile misst ein paar hundert Byte.
|
||||
"""
|
||||
conn.execute(
|
||||
"INSERT INTO document_signatures (sha256, share_label, path, signed_at, actor, "
|
||||
"names, audit_id) VALUES (?,?,?,?,?,?,?)",
|
||||
(
|
||||
sha256,
|
||||
label,
|
||||
pfad,
|
||||
_iso(_now()),
|
||||
actor,
|
||||
", ".join(namen),
|
||||
audit_id,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def signaturstand(conn: sqlite3.Connection, mount: "Mount", relative: str) -> dict[str, Any]:
|
||||
"""Ist diese Datei die, die unterschrieben wurde?
|
||||
|
||||
@@ -633,8 +682,12 @@ def signaturstand(conn: sqlite3.Connection, mount: "Mount", relative: str) -> di
|
||||
* ``unbekannt`` -- kein Eintrag. Kein Verdacht: die meisten Dateien in
|
||||
einer Freigabe sind nie durch TESM gegangen.
|
||||
|
||||
Gesucht wird nur in der Datenbank. Ausgelagerte Protokolltage (Archiv)
|
||||
bleiben aussen vor -- ein Dateidialog darf nicht minutenlang suchen.
|
||||
Nachgeschlagen wird im Signaturverzeichnis (``document_signatures``) und
|
||||
nicht im Aenderungsprotokoll: das wird ab 20.000 Zeilen tageweise in
|
||||
Dateien ausgelagert, und mit dem ausgelagerten Tag waere die Pruefsumme aus
|
||||
der Datenbank verschwunden. Eine aeltere unterschriebene Fassung waere
|
||||
still zu "unbekannt" geworden -- und eine **veraenderte** ebenso, weil ohne
|
||||
Vergleichswert niemand widerspricht. Das Verzeichnis wird nie archiviert.
|
||||
"""
|
||||
ziel = resolve_path(mount, relative)
|
||||
if not ziel.is_file():
|
||||
@@ -643,37 +696,36 @@ def signaturstand(conn: sqlite3.Connection, mount: "Mount", relative: str) -> di
|
||||
|
||||
zeile = Database.one(
|
||||
conn,
|
||||
"SELECT ts, actor, detail FROM audit_log WHERE action=? AND detail LIKE ? "
|
||||
"SELECT signed_at, actor, names FROM document_signatures WHERE sha256=? "
|
||||
"ORDER BY id DESC LIMIT 1",
|
||||
(SIGNATUR_AKTION, f"%sha256={hash_}%"),
|
||||
(hash_,),
|
||||
)
|
||||
if zeile is not None:
|
||||
return {
|
||||
"status": "unveraendert",
|
||||
"sha256": hash_,
|
||||
"zeit": str(zeile["ts"]),
|
||||
"konto": str(zeile["actor"]),
|
||||
"namen": _namen_aus_detail(str(zeile["detail"])),
|
||||
}
|
||||
return _stand("unveraendert", hash_, zeile)
|
||||
|
||||
# Nichts mit dieser Pruefsumme -- aber vielleicht etwas zu diesem Ort.
|
||||
zeile = Database.one(
|
||||
conn,
|
||||
"SELECT ts, actor, detail FROM audit_log WHERE action=? AND detail LIKE ? "
|
||||
"ORDER BY id DESC LIMIT 1",
|
||||
(SIGNATUR_AKTION, f"{mount.label}:{relative} sha256=%"),
|
||||
"SELECT signed_at, actor, names FROM document_signatures WHERE share_label=? "
|
||||
"AND path=? ORDER BY id DESC LIMIT 1",
|
||||
(mount.label, relative),
|
||||
)
|
||||
if zeile is not None:
|
||||
return {
|
||||
"status": "veraendert",
|
||||
"sha256": hash_,
|
||||
"zeit": str(zeile["ts"]),
|
||||
"konto": str(zeile["actor"]),
|
||||
"namen": _namen_aus_detail(str(zeile["detail"])),
|
||||
}
|
||||
return _stand("veraendert", hash_, zeile)
|
||||
return {"status": "unbekannt", "sha256": hash_}
|
||||
|
||||
|
||||
def _stand(status: str, hash_: str, zeile: Any) -> dict[str, Any]:
|
||||
namen = [teil.strip() for teil in str(zeile["names"] or "").split(",") if teil.strip()]
|
||||
return {
|
||||
"status": status,
|
||||
"sha256": hash_,
|
||||
"zeit": str(zeile["signed_at"]),
|
||||
"konto": str(zeile["actor"] or ""),
|
||||
"namen": namen,
|
||||
}
|
||||
|
||||
|
||||
def _namen_aus_detail(detail: str) -> list[str]:
|
||||
"""Die angegebenen Namen aus dem Protokolltext -- ohne sie zu deuten."""
|
||||
marke = "angegeben unterschrieben von: "
|
||||
|
||||
Reference in new issue
Block a user