diff --git a/apps/tesm-license/pyproject.toml b/apps/tesm-license/pyproject.toml index 926419b..e821290 100644 --- a/apps/tesm-license/pyproject.toml +++ b/apps/tesm-license/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tesm-license" -version = "2.5.0" +version = "2.5.1" description = "TESM-Lizenzserver -- Kunden, Tickets, Lizenzen, Aktivierung" requires-python = ">=3.11" dependencies = [ diff --git a/apps/tesm-license/src/tesm_license/blueprints/settings.py b/apps/tesm-license/src/tesm_license/blueprints/settings.py index 7b409d8..de4b088 100644 --- a/apps/tesm-license/src/tesm_license/blueprints/settings.py +++ b/apps/tesm-license/src/tesm_license/blueprints/settings.py @@ -54,6 +54,8 @@ def system(): "trash_retention_days", "log_rotate_interval", "log_rotate_keep", + "session_idle_minutes", + "session_absolute_hours", ], ), specs={spec.key: spec for spec in extension.settings.specs()}, @@ -98,6 +100,8 @@ def system_save(): "trash_retention_days", "log_rotate_interval", "log_rotate_keep", + "session_idle_minutes", + "session_absolute_hours", ): spec = extension.settings.spec(key) if spec.kind == "bool": diff --git a/apps/tesm-license/src/tesm_license/navigation.py b/apps/tesm-license/src/tesm_license/navigation.py index 13ddebb..5c26a9c 100644 --- a/apps/tesm-license/src/tesm_license/navigation.py +++ b/apps/tesm-license/src/tesm_license/navigation.py @@ -28,7 +28,7 @@ def _is_master(_user: Any) -> bool: NAVIGATION = NavTree( [ - NavItem(key="index", label="Uebersicht", icon="grid", endpoint="dashboard.index"), + NavItem(key="index", label="Übersicht", icon="grid", endpoint="dashboard.index"), NavItem( key="customers", label="Kunden", diff --git a/apps/tesm-license/src/tesm_license/templates/license/settings_system.html b/apps/tesm-license/src/tesm_license/templates/license/settings_system.html index ed095d0..e8560e3 100644 --- a/apps/tesm-license/src/tesm_license/templates/license/settings_system.html +++ b/apps/tesm-license/src/tesm_license/templates/license/settings_system.html @@ -60,9 +60,6 @@ selected=values.log_rotate_interval) }} {{ field('log_rotate_keep', 'Aufbewahrte Rotationen', value=values.log_rotate_keep, type='number') }} - {{ field('session_idle_minutes', 'Automatische Abmeldung (Minuten)', - value=values.session_idle_minutes, type='number', - hint_text=specs['session_idle_minutes'].hint) }} {{ field('trash_retention_days', 'Papierkorb-Aufbewahrung (Tage)', value=values.trash_retention_days, type='number') }} @@ -73,6 +70,34 @@
+ {# Eigene Karte: die Abmeldung stand zwischen Protokollrotation und + Papierkorb -- drei Dinge ohne Zusammenhang. Wer die Sitzungsdauer sucht, + sucht sie nicht unter "Protokolle". #} +
+
+
+
{{ icon('clock', size=17) }} Sitzungen
+
Wie lange eine Anmeldung gilt
+
+
+
+ +
+
+ {{ field('session_idle_minutes', 'Automatische Abmeldung nach Leerlauf (Minuten)', + value=values.session_idle_minutes, type='number', + attrs='min="5" max="1440" step="1"', + hint_text=specs['session_idle_minutes'].hint) }} + {{ field('session_absolute_hours', 'Längstmögliche Sitzungsdauer (Stunden)', + value=values.session_absolute_hours, type='number', + attrs='min="1" max="168" step="1"', + hint_text=specs['session_absolute_hours'].hint) }} +
+
+ +
+
+
{{ icon('server', size=17) }} Host
diff --git a/packages/tesm-core/pyproject.toml b/packages/tesm-core/pyproject.toml index ffa2816..fd5176e 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.5.0" +version = "2.5.1" 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 1d251ce..df3fcbc 100644 --- a/packages/tesm-core/src/tesm_core/app_factory.py +++ b/packages/tesm-core/src/tesm_core/app_factory.py @@ -13,10 +13,12 @@ import secrets from datetime import datetime, timezone from typing import Any, Sequence -from flask import Blueprint, Flask, g, render_template, request +from urllib.parse import urlencode + +from flask import Blueprint, Flask, g, redirect, render_template, request from werkzeug.exceptions import HTTPException -from . import audit, logging_setup, ui +from . import audit, legacy_urls, logging_setup, ui from .config import CoreConfig from .core_schema import CORE_MIGRATIONS from .db import Database @@ -146,7 +148,7 @@ def create_base_app( #: Wortmarke, ohne Zeile darunter; die Marke selbst zeigen beide #: Anwendungen. auth_tagline: str = "", - app_version: str = "2.5.0", + app_version: str = "2.5.1", built_in_trusted_keys: tuple[str, ...] = (), enable_license: bool = True, template_folder: str | None = None, @@ -356,6 +358,16 @@ def create_base_app( # -- Fehlerseiten ------------------------------------------------------ # @app.errorhandler(HTTPException) def _http_error(error: HTTPException): # type: ignore[no-untyped-def] + if error.code == 404: + # Vor der Fehlerseite: war das eine Adresse aus der Zeit vor 2.5.0? + # Erst hier gefragt, damit die Tabelle nie eine bestehende Route + # verdeckt -- auf dem Lizenzserver ist /lizenz/ echt. + ziel = legacy_urls.neue_adresse(request.path) + if ziel is not None: + parameter = legacy_urls.neue_parameter(request.args.to_dict(flat=True)) + if parameter: + ziel = ziel + "?" + urlencode(parameter) + return redirect(ziel) if error.code == 413: # "Request Entity Too Large" allein hilft niemandem weiter: die # Zahl steht in einer Einstellung, und wer hochlaedt, soll sie diff --git a/packages/tesm-core/src/tesm_core/legacy_urls.py b/packages/tesm-core/src/tesm_core/legacy_urls.py new file mode 100644 index 0000000..fcf04a4 --- /dev/null +++ b/packages/tesm-core/src/tesm_core/legacy_urls.py @@ -0,0 +1,242 @@ +"""Alte, deutsche Adressen auf die neuen englischen umlenken. + +Mit 2.5.0 folgen die Adressen der Navigation und sind englisch: +``/monitoring/host-keys`` statt ``/hostschluessel``. Das ist eine gute +Aenderung und eine unangenehme zugleich -- Lesezeichen, offene Browsertabs und +ein Link in einer Mail zeigen weiter auf die alte Adresse. Ein offener Tab ist +dabei der unangenehmste Fall: die Statusuebersicht fragt alle 20 Sekunden ihre +Kacheln ab und bekam nach dem Update jedes Mal einen 404. + +Deshalb diese Tabelle. Sie greift **nur, wenn sonst ein 404 herauskaeme** -- +also nie vor einer bestehenden Route. Das ist keine Feinheit: auf dem +Lizenzserver ist ``/lizenz/`` eine echte, weiterhin gueltige Adresse +(sie steht in verschickten Mails), waehrend dieselbe Wurzel in TESM zur +Lizenzseite fuehrte. Wer zuerst das Routing fragt, muss sich nicht zwischen +beidem entscheiden. + +Die Tabelle ist bewusst vollstaendig und nicht nur auf Wurzeln gekuerzt: eine +Umleitung von ``/dateifreigaben/x/vorschau`` auf eine Uebersichtsseite waere +zwar kein 404 mehr, aber auch nicht das, was jemand angeklickt hat. +""" + +from __future__ import annotations + +from typing import Mapping + +#: Alte Wurzel -> (neue Wurzel, Uebersetzung der Segmente darunter). +WURZELN: dict[str, tuple[str, Mapping[str, str]]] = { + "/dateifreigaben": ( + "/fileshares", + { + "herunterladen": "download", + "vorschau": "preview", + "hochladen": "upload", + "formular": "form", + "ordner": "folder", + "umbenennen": "rename", + "loeschen": "delete", + "freigeben": "release", + "zuordnungen": "mappings", + }, + ), + "/hostschluessel": ( + "/monitoring/host-keys", + {"pruefen": "probe", "freigeben": "trust", "verwerfen": "forget", "verwaist": "orphaned"}, + ), + "/zugangsdaten": ( + "/monitoring/credentials", + {"neu": "new", "loeschen": "delete", "anzeigen": "reveal"}, + ), + "/switche": ( + "/monitoring/switches", + { + "neu": "new", + "loeschen": "delete", + "hostkey": "host-key", + "pruefen": "probe", + "freigeben": "trust", + "verwerfen": "forget", + "kommando": "command", + "konsole": "console", + }, + ), + "/clients": ( + "/monitoring/clients", + { + "neu": "new", + "aktiv": "active", + "loeschen": "delete", + "neustart": "restart", + "hostkey": "host-key", + "pruefen": "probe", + "freigeben": "trust", + "verwerfen": "forget", + "konsole": "console", + }, + ), + "/wartung": ( + "/maintenance", + { + "paket-nachinstallieren": "install-package", + "auftrag": "job", + "ausgabe": "output", + "aktualisieren": "update", + "neustarten": "restart", + }, + ), + "/protokolle": ( + "/logs", + { + "kea": "dhcp", + "neustarts": "restarts", + "laufend": "live", + "inhalt": "content", + "verlauf": "history", + "datei": "file", + "archiv": "archive", + "archiv-export": "archive-export", + "archiv-loeschen": "archive-delete", + "aenderungen": "audit", + }, + ), + "/verwaltung": ( + "/administration", + { + "benutzer": "users", + "neu": "new", + "loeschen": "delete", + "sitzungen-beenden": "end-sessions", + "verzeichnis-suche": "directory-search", + "verzeichnis-uebernehmen": "directory-import", + "gruppen": "groups", + "vorlage-anwenden": "apply-preset", + "sitzungen": "sessions", + "beenden": "end", + }, + ), + "/einstellungen": ( + "/administration", + { + "zeitzone": "timezone", + "pruefung-jetzt": "check-now", + "netzwerk": "network", + "bestaetigen": "confirm", + "webserver": "web", + "uebernehmen": "apply", + "zertifikat": "certificate", + "selbstsigniert": "self-signed", + "verzeichnisdienst": "directory", + "gruppensuche": "group-search", + "zuordnung": "mapping", + "loeschen": "delete", + "anbieter": "vendor", + "logo-entfernen": "remove-logo", + }, + ), + "/sicherung": ( + "/administration/backup", + {"vorschau": "preview", "uebernehmen": "apply", "verwerfen": "discard"}, + ), + "/papierkorb": ( + "/administration/trash", + {"wiederherstellen": "restore", "endgueltig-loeschen": "purge", "leeren": "empty"}, + ), + "/diagnose": ( + "/administration/diagnostics", + {"aufraeumen": "cleanup", "schluessel-rotieren": "rotate-keys"}, + ), + "/eigene-lizenz": ( + "/own-license", + { + "einspielen": "install", + "aktivieren": "activate", + "deaktivieren": "deactivate", + "offline-antwort": "offline-response", + "signaturschluessel": "signing-key", + }, + ), + "/lizenzen": ( + "/licenses", + { + "aus-ticket": "from-ticket", + "datei": "file", + "neu-signieren": "resign", + "widerrufen": "revoke", + "loeschen": "delete", + "versenden": "send", + "sub-lizenzserver": "sub-servers", + }, + ), + "/lizenz": ( + "/administration/license", + { + "einspielen": "install", + "aktivieren": "activate", + "deaktivieren": "deactivate", + "offline-antwort": "offline-response", + "serverzertifikat": "server-certificate", + "aussteller": "issuer", + "entfernen": "remove", + }, + ), + "/kunden": ("/customers", {"neu": "new", "loeschen": "delete"}), + "/konto": ( + "/account", + { + "stammdaten": "profile", + "profilbild": "avatar", + "sicherheit": "security", + "passwort": "password", + "zwei-faktor": "two-factor", + "starten": "start", + "bestaetigen": "confirm", + "deaktivieren": "disable", + "codes-erneuern": "recovery-codes", + "sitzungen": "sessions", + "beenden": "end", + "alle-beenden": "end-all", + }, + ), + "/uebersicht": ("/overview", {"kacheln": "tiles"}), + "/gesundheit": ("/health", {}), + "/bestaetigen": ("/confirm", {}), +} + +#: Abfrageparameter, die mit umbenannt wurden. +PARAMETER = { + "freigabe": "share", + "pfad": "path", + "aktion": "action", + "benutzer": "actor", + "stufe": "severity", + "seite": "page", + "methode": "method", + "kunde": "customer", + "freischalten": "unlocked", +} + + +#: Wurzeln, unter denen eine unbekannte Unterseite **nicht** umgelenkt wird. +#: ``/lizenz/`` ist auf dem Lizenzserver eine echte Adresse; ein +#: abgelaufenes oder falsches Ticket gehoert dort auf die Fehlerseite und nicht +#: in die Verwaltung. +STRENG = {"/lizenz"} + + +def neue_adresse(pfad: str) -> str | None: + """Uebersetzt eine alte Adresse -- oder ``None``, wenn es keine ist.""" + for alt, (neu, segmente) in WURZELN.items(): + if pfad != alt and not pfad.startswith(alt + "/"): + continue + rest = pfad[len(alt) :] + teile = rest.split("/") + if alt in STRENG: + erstes = next((teil for teil in teile if teil), "") + if erstes and erstes not in segmente: + return None + return neu + "/".join(segmente.get(teil, teil) for teil in teile) + return None + + +def neue_parameter(args: Mapping[str, str]) -> dict[str, str]: + return {PARAMETER.get(name, name): wert for name, wert in args.items()} diff --git a/packages/tesm-core/src/tesm_core/rbac/common.py b/packages/tesm-core/src/tesm_core/rbac/common.py index 47695e3..b56eb8a 100644 --- a/packages/tesm-core/src/tesm_core/rbac/common.py +++ b/packages/tesm-core/src/tesm_core/rbac/common.py @@ -77,7 +77,7 @@ SETTINGS_NETWORK = Resource( BACKUP = Resource( key="backup", - label="Sicherung und Uebertragung", + label="Sicherung und Übertragung", actions=(A.VIEW, A.EXECUTE, A.EXPORT, A.SECRETS), hint="Verschluesselter Export und Import", action_hints={ @@ -121,7 +121,7 @@ LOGS_HISTORY = Resource( LOGS_AUDIT = Resource( key="logs_audit", - label="Aenderungsprotokoll", + label="Änderungsprotokoll", actions=(A.VIEW, A.EXPORT), hint="Wer hat wann was geaendert", ) diff --git a/packages/tesm-licensing-server/pyproject.toml b/packages/tesm-licensing-server/pyproject.toml index 80aef6c..f81138a 100644 --- a/packages/tesm-licensing-server/pyproject.toml +++ b/packages/tesm-licensing-server/pyproject.toml @@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta" [project] name = "tesm-licensing-server" -version = "2.5.0" +version = "2.5.1" description = "Serverhaelfte des TESM-Lizenzprotokolls: ausstellen, erneuern, antworten" requires-python = ">=3.11" -dependencies = ["tesm-licensing==2.5.0", "cryptography>=41"] +dependencies = ["tesm-licensing==2.5.1", "cryptography>=41"] [tool.setuptools.packages.find] where = ["src"] diff --git a/packages/tesm-licensing/pyproject.toml b/packages/tesm-licensing/pyproject.toml index 1b9416c..cdc6fd4 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.5.0" +version = "2.5.1" description = "Ed25519-basiertes Lizenz- und Aktivierungsprotokoll fuer TESM und TESM-Lizenzserver" requires-python = ">=3.11" dependencies = ["cryptography>=42"]