From ab5fda658e3f2fc762f6fbcd93e449a69034c547 Mon Sep 17 00:00:00 2001 From: Tim Eertmoed Date: Wed, 2 Sep 2026 23:54:35 +0200 Subject: [PATCH] Umlaute in der Oberflaeche, Sitzungskopplung, Statusanzeigen Echte Umlaute in allen Texten der Oberflaeche und in den Markdown-Dateien. Die Einbindung von Dateifreigaben haengt jetzt an der Sitzung: wird sie widerrufen oder laeuft sie ab, verschwinden die Freigaben mit ihr. Neu einstellbar ist die automatische Abmeldung bei Leerlauf (Vorgabe 30 Minuten). Ein Geraet, das nicht antwortet, bietet nur noch "Einschalten (PoE)" an -- Neustart ueber SSH oder RPC braucht ein erreichbares Geraet. Der Zeitgeber der naechsten Pruefung und die Restlaufzeit der Lizenz stehen jetzt oben in der Kopfzeile; der doppelte Knopf darunter ist weg. Millisekunden werden ab einer Sekunde als Sekunden und ab einer Minute als m:ss dargestellt. Ausserdem: Release-Tarball nicht mehr im Repository -- er haengt am Release. Co-Authored-By: Claude Opus 5 --- README.md | 56 ++++---- apps/tesm-license/pyproject.toml | 2 +- .../tesm-license/src/tesm_license/__init__.py | 2 +- .../src/tesm_license/navigation.py | 2 +- .../templates/license/backup.html | 42 +++--- .../templates/license/customer_detail.html | 12 +- .../templates/license/customers.html | 4 +- .../templates/license/dashboard.html | 6 +- .../templates/license/license_detail.html | 36 ++--- .../templates/license/manual_code.html | 10 +- .../templates/license/own_license.html | 36 ++--- .../templates/license/portal_me.html | 4 +- .../templates/license/portal_request.html | 4 +- .../templates/license/report_detail.html | 4 +- .../templates/license/reports.html | 4 +- .../templates/license/selfservice.html | 14 +- .../license/selfservice_offline.html | 10 +- .../templates/license/settings_mail.html | 16 +-- .../templates/license/settings_system.html | 17 ++- .../templates/license/settings_vendor.html | 14 +- .../templates/license/signing_key.html | 20 +-- .../templates/license/ticket_detail.html | 28 ++-- .../templates/license/ticket_new.html | 18 +-- .../templates/license/tickets.html | 8 +- deploy/systemd/tesm.service | 15 +- docs/BETRIEB.md | 130 +++++++++--------- docs/SICHERHEIT.md | 112 +++++++-------- packages/tesm-core/pyproject.toml | 2 +- packages/tesm-core/src/tesm_core/__init__.py | 2 +- .../tesm-core/src/tesm_core/app_factory.py | 34 ++++- .../tesm-core/src/tesm_core/auth/directory.py | 6 +- packages/tesm-core/src/tesm_core/config.py | 4 +- .../src/tesm_core/licensing_client.py | 52 +++++-- .../src/tesm_core/security/sessions.py | 17 ++- .../static/tesm_core/css/components.css | 66 +++++++++ .../src/tesm_core/static/tesm_core/js/app.js | 14 +- .../templates/tesm_core/_group_picker.html | 14 +- .../templates/tesm_core/account.html | 4 +- .../templates/tesm_core/account_security.html | 20 +-- .../tesm_core/templates/tesm_core/base.html | 36 +++-- .../templates/tesm_core/diagnostics.html | 40 +++--- .../tesm_core/templates/tesm_core/error.html | 4 +- .../templates/tesm_core/group_detail.html | 10 +- .../tesm_core/templates/tesm_core/groups.html | 16 +-- .../tesm_core/templates/tesm_core/login.html | 2 +- .../templates/tesm_core/logs_archive.html | 2 +- .../templates/tesm_core/logs_audit.html | 29 ++-- .../templates/tesm_core/logs_history.html | 12 +- .../tesm_core/templates/tesm_core/macros.html | 23 +++- .../tesm_core/templates/tesm_core/reauth.html | 8 +- .../templates/tesm_core/second_factor.html | 6 +- .../templates/tesm_core/sessions.html | 6 +- .../tesm_core/settings_directory.html | 26 ++-- .../templates/tesm_core/settings_network.html | 32 ++--- .../templates/tesm_core/settings_web.html | 44 +++--- .../templates/tesm_core/standalone.html | 18 +-- .../tesm_core/templates/tesm_core/trash.html | 18 +-- .../tesm_core/templates/tesm_core/users.html | 20 +-- packages/tesm-core/src/tesm_core/ui.py | 32 +++++ packages/tesm-licensing-server/pyproject.toml | 4 +- packages/tesm-licensing/pyproject.toml | 2 +- .../src/tesm_licensing/__init__.py | 2 +- tesm-license-v2.0.1.tar.gz | Bin 574342 -> 0 bytes 63 files changed, 746 insertions(+), 507 deletions(-) delete mode 100644 tesm-license-v2.0.1.tar.gz diff --git a/README.md b/README.md index 5998478..28924d1 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@ # TESM-Lizenzserver -Ausgabe und Verwaltung der Lizenzen fuer TESM: Kunde -> Ticket -> Lizenz, -Aktivierung online und offline, Kundenportal, Selbstbedienung, Versand ueber +Ausgabe und Verwaltung der Lizenzen für TESM: Kunde -> Ticket -> Lizenz, +Aktivierung online und offline, Kundenportal, Selbstbedienung, Versand über MS Graph und eine Hierarchie aus Unter-Lizenzservern. -**Dieses Repository enthaelt genau das, was zum Installieren und Betreiben des -Lizenzservers gebraucht wird** -- nichts darueber hinaus: +**Dieses Repository enthält genau das, was zum Installieren und Betreiben des +Lizenzservers gebraucht wird** -- nichts darüber hinaus: ``` apps/tesm-license die Anwendung packages/tesm-core gemeinsamer Kern (Anmeldung, Rechte, Sicherung, nginx, ...) -packages/tesm-licensing Lizenzprotokoll, gemeinsame Haelfte -packages/tesm-licensing-server Lizenzprotokoll, Serverhaelfte: ausstellen, erneuern, antworten +packages/tesm-licensing Lizenzprotokoll, gemeinsame Hälfte +packages/tesm-licensing-server Lizenzprotokoll, Serverhälfte: ausstellen, erneuern, antworten deploy/ install.sh, update.sh, release.sh, systemd, privilegierter Helfer docs/ Betrieb und Sicherheit ``` @@ -28,7 +28,7 @@ Die verwaltete Anwendung liegt hier **nicht** mit drin. | Zweig | Inhalt | |---|---| | `main` | dieser Neubau (Version 2.0.0) | -| `SONNET5` | der Stand davor, unveraendert aufbewahrt | +| `SONNET5` | der Stand davor, unverändert aufbewahrt | --- @@ -41,18 +41,18 @@ sudo -u tesm-license tesm-license-admin bootstrap sudo -u tesm-license tesm-license-admin create-admin ``` -**`init-key` erzeugt den Signaturschluessel dieses Servers. Sichern Sie ihn -sofort.** Ohne ihn laesst sich keine ausgestellte Lizenz mehr erneuern, und -Kunden muessten einen Ausstellerwechsel bestaetigen. +**`init-key` erzeugt den Signaturschlüssel dieses Servers. Sichern Sie ihn +sofort.** Ohne ihn lässt sich keine ausgestellte Lizenz mehr erneuern, und +Kunden müssten einen Ausstellerwechsel bestätigen. Zielzustand: **Port 443 mit TLS, Port 80 leitet dorthin um** -- ausser dem ACME-Pfad, der erreichbar bleiben muss, sonst kann Let's Encrypt nie erneuern. -Sitzt ein eigener Reverse Proxy davor, der TLS beendet, laeuft der Dienst ohne +Sitzt ein eigener Reverse Proxy davor, der TLS beendet, läuft der Dienst ohne `--https` schlicht auf Port 80. Der Lizenzserver darf neben TESM auf demselben Host laufen; nginx unterscheidet die beiden am Servernamen, und jede Installation hat ihren eigenen `SITE_KEY` -fuer nginx-Site, logrotate, Zertifikate und Sitzungscookie. +für nginx-Site, logrotate, Zertifikate und Sitzungscookie. ### Aktualisieren @@ -60,7 +60,7 @@ fuer nginx-Site, logrotate, Zertifikate und Sitzungscookie. sudo bash deploy/update.sh --app tesm-license ``` -Vor jedem Update entsteht ein vollstaendiges Backup, danach laeuft ein +Vor jedem Update entsteht ein vollständiges Backup, danach läuft ein Health-Check mit automatischem Rollback. --- @@ -69,16 +69,16 @@ Health-Check mit automatischem Rollback. ```bash sudo -u tesm-license tesm-license-admin status -sudo -u tesm-license tesm-license-admin init-key # Signaturschluessel +sudo -u tesm-license tesm-license-admin init-key # Signaturschlüssel sudo -u tesm-license tesm-license-admin bootstrap # Grunddaten sudo -u tesm-license tesm-license-admin create-admin -sudo -u tesm-license tesm-license-admin migrate-legacy # Altbestand uebernehmen +sudo -u tesm-license tesm-license-admin migrate-legacy # Altbestand übernehmen ``` `migrate-legacy` liest die SQLite-Datei eines alten Lizenzservers **nur -lesend**, uebernimmt dessen Signaturschluessel und stellt die Lizenzen mit ihrer -restlichen Laufzeit neu aus. Wird der Schluessel nicht uebernommen, muessen -Kunden den Ausstellerwechsel einmal bestaetigen. +lesend**, übernimmt dessen Signaturschlüssel und stellt die Lizenzen mit ihrer +restlichen Laufzeit neu aus. Wird der Schlüssel nicht übernommen, müssen +Kunden den Ausstellerwechsel einmal bestätigen. --- @@ -101,14 +101,14 @@ genau einmal. (`tesm/license-file/v2\0`) -- eine Signatur aus einem Kontext ist in einem anderen nicht verwendbar. * Kanonisches JSON, damit dieselbe Aussage genau eine Signatur hat. -* Nonce gegen Wiedereinspielung, `seq` gegen das Zurueckrollen auf eine aeltere +* Nonce gegen Wiedereinspielung, `seq` gegen das Zurückrollen auf eine ältere Lizenz. * Aussteller wird beim ersten Kontakt gemerkt (TOFU); ein Wechsel muss - ausdruecklich bestaetigt werden. -* Der Fingerabdruck der Maschine enthaelt **keinen** Hostnamen -- Umbenennen - brach im Vorgaenger die Bindung. -* Offline-Aktivierung ueber einen kurzen Code in beide Richtungen; die Lizenz - bleibt auf dem Server als "Aktivierung offen" markiert, bis sie zurueckkommt. + ausdrücklich bestätigt werden. +* Der Fingerabdruck der Maschine enthält **keinen** Hostnamen -- Umbenennen + brach im Vorgänger die Bindung. +* Offline-Aktivierung über einen kurzen Code in beide Richtungen; die Lizenz + bleibt auf dem Server als "Aktivierung offen" markiert, bis sie zurückkommt. * Keine Lizenz ohne `master_endpoint`: eine Ausstellung ohne Endpunkt wird abgelehnt statt eine Lizenz zu erzeugen, die sich nie aktivieren kann. @@ -130,8 +130,8 @@ Entwicklungsrepository, ebenso die Tests. * **Keine doppelte `licensing.py`.** Ein Paket, das beide Anwendungen installieren -- statt zweier Dateien, die byte-identisch bleiben sollen. -* **Kein root fuer die Anwendung.** Ein einziger allowlisted Helfer mit - Argumentpruefung. -* **CSRF-Schutz ueberhaupt.** Der Vorgaenger hatte keinen. -* **Rechte werden serverseitig geprueft**, nicht nur im Template ausgeblendet. +* **Kein root für die Anwendung.** Ein einziger allowlisted Helfer mit + Argumentprüfung. +* **CSRF-Schutz überhaupt.** Der Vorgänger hatte keinen. +* **Rechte werden serverseitig geprüft**, nicht nur im Template ausgeblendet. * **Verschachtelte Datenbankverbindungen sind ein Fehler**, kein Deadlock. diff --git a/apps/tesm-license/pyproject.toml b/apps/tesm-license/pyproject.toml index bb86d7e..b25a166 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.0.0" +version = "2.0.2" description = "TESM-Lizenzserver -- Kunden, Tickets, Lizenzen, Aktivierung" requires-python = ">=3.11" dependencies = [ diff --git a/apps/tesm-license/src/tesm_license/__init__.py b/apps/tesm-license/src/tesm_license/__init__.py index a8262ec..0879abd 100644 --- a/apps/tesm-license/src/tesm_license/__init__.py +++ b/apps/tesm-license/src/tesm_license/__init__.py @@ -23,7 +23,7 @@ from .permissions import PERMISSIONS, ROLE_PRESETS from .schema import LICENSE_MIGRATIONS from .settings_spec import LICENSE_SERVER_SETTINGS -__version__ = "2.0.0" +__version__ = "2.0.2" APP_KEY = "tesm-license" APP_NAME = "TESM-Lizenzserver" diff --git a/apps/tesm-license/src/tesm_license/navigation.py b/apps/tesm-license/src/tesm_license/navigation.py index 8960fba..13ddebb 100644 --- a/apps/tesm-license/src/tesm_license/navigation.py +++ b/apps/tesm-license/src/tesm_license/navigation.py @@ -86,7 +86,7 @@ NAVIGATION = NavTree( ), NavItem( key="logs_audit", - label="Aenderungen", + label="Audit", icon="history", endpoint="logs.audit_view", permissions=("logs_audit.view", "logs_audit.export"), diff --git a/apps/tesm-license/src/tesm_license/templates/license/backup.html b/apps/tesm-license/src/tesm_license/templates/license/backup.html index 237ed10..32eaa7f 100644 --- a/apps/tesm-license/src/tesm_license/templates/license/backup.html +++ b/apps/tesm-license/src/tesm_license/templates/license/backup.html @@ -1,26 +1,26 @@ {% extends "tesm_core/base.html" %} {% from "tesm_core/macros.html" import field, alert, pill %} -{% block page_title %}Sicherung und Uebertragung{% endblock %} -{% block page_sub %}Verschluesselter Export · zweistufiger Import{% endblock %} +{% block page_title %}Sicherung und Übertragung{% endblock %} +{% block page_sub %}Verschlüsselter Export · zweistufiger Import{% endblock %} {% block content %} -{{ alert('Der Export wird mit Argon2id aus Ihrer Passphrase verschluesselt (AES-256-GCM). Ohne -diese Passphrase ist die Datei nicht wiederherstellbar -- es gibt keine Hintertuer.', 'warning', +{{ alert('Der Export wird mit Argon2id aus Ihrer Passphrase verschlüsselt (AES-256-GCM). Ohne +diese Passphrase ist die Datei nicht wiederherstellbar -- es gibt keine Hintertür.', 'warning', 'Passphrase') }} -{{ alert('Der Signaturschluessel (' ~ signing_key_path ~ ') ist bewusst -nicht Teil dieser Sicherung. Er ist der Kern des Vertrauens und gehoert in eine +{{ alert('Der Signaturschlüssel (' ~ signing_key_path ~ ') ist bewusst +nicht Teil dieser Sicherung. Er ist der Kern des Vertrauens und gehört in eine getrennte, bewusst verwahrte Sicherung -- nicht in dieselbe Datei wie die Kundendaten. Ohne ihn -ist nach einer Wiederherstellung keine ausgestellte Lizenz mehr pruefbar.', 'danger', -'Wichtig: Schluessel getrennt sichern') }} +ist nach einer Wiederherstellung keine ausgestellte Lizenz mehr prüfbar.', 'danger', +'Wichtig: Schlüssel getrennt sichern') }}
{{ icon('download', size=17) }} Sicherung erstellen
-
Verlangt eine frische Passwortbestaetigung
+
Verlangt eine frische Passwortbestätigung
@@ -40,7 +40,7 @@ ist nach einer Wiederherstellung keine ausgestellte Lizenz mehr pruefbar.', 'dan {% if category.admin_only %}{{ pill('nur Admin', 'accent') }}{% endif %}
{{ category.hint }}
{% if not allowed %} -
Dafuer fehlt Ihnen das Recht.
+
Dafür fehlt Ihnen das Recht.
{% endif %} @@ -66,8 +66,8 @@ ist nach einer Wiederherstellung keine ausgestellte Lizenz mehr pruefbar.', 'dan
-
{{ icon('upload', size=17) }} Geoeffnete Sicherung
-
Gueltig fuer {{ preview_ttl }} Minuten
+
{{ icon('upload', size=17) }} Geöffnete Sicherung
+
Gültig für {{ preview_ttl }} Minuten
@@ -75,7 +75,7 @@ ist nach einer Wiederherstellung keine ausgestellte Lizenz mehr pruefbar.', 'dan
- + {% for entry in preview.summary %} @@ -89,17 +89,17 @@ ist nach einer Wiederherstellung keine ausgestellte Lizenz mehr pruefbar.', 'dan
KategorieEintraege
KategorieEinträge
- {{ alert('Bereits vorhandene Lizenzen werden nie ueberschrieben -- ihr Inhalt ist - kryptographisch festgelegt. Kunden werden ueber ihren Namen zusammengefuehrt.', + {{ alert('Bereits vorhandene Lizenzen werden nie überschrieben -- ihr Inhalt ist + kryptographisch festgelegt. Kunden werden über ihren Namen zusammengeführt.', 'info') }}
@@ -108,7 +108,7 @@ ist nach einer Wiederherstellung keine ausgestellte Lizenz mehr pruefbar.', 'dan
{{ icon('upload', size=17) }} Sicherung einspielen
-
Erst oeffnen und pruefen, dann uebernehmen
+
Erst öffnen und prüfen, dann übernehmen
diff --git a/apps/tesm-license/src/tesm_license/templates/license/customer_detail.html b/apps/tesm-license/src/tesm_license/templates/license/customer_detail.html index c2d2261..9672e14 100644 --- a/apps/tesm-license/src/tesm_license/templates/license/customer_detail.html +++ b/apps/tesm-license/src/tesm_license/templates/license/customer_detail.html @@ -70,7 +70,7 @@ {% if licenses %}
- + {% for entry in licenses %} @@ -119,7 +119,7 @@
{{ alert('Diese Daten verwaltet der Sub-Lizenzserver selbst. Sie sind hier rein - informativ und werden bei jedem Bericht vollstaendig ersetzt.', 'info') }} + informativ und werden bei jedem Bericht vollständig ersetzt.', 'info') }} {% for entry in reported.customers %}
@@ -193,7 +193,7 @@
Angelegt
{{ customer.created_at | dt }} von {{ customer.created_by or '-' }}
-
Geaendert
{{ customer.updated_at | dt }}
+
Geändert
{{ customer.updated_at | dt }}
Portal-Zugang
{{ portal_url }} {% else %}{% endif %} diff --git a/apps/tesm-license/src/tesm_license/templates/license/customers.html b/apps/tesm-license/src/tesm_license/templates/license/customers.html index 734a508..a2dee5e 100644 --- a/apps/tesm-license/src/tesm_license/templates/license/customers.html +++ b/apps/tesm-license/src/tesm_license/templates/license/customers.html @@ -7,7 +7,7 @@ {{ field('name', 'Firmenname', value=customer.name if customer else '', required=True) }} {{ field('contact_email', 'E-Mail', value=customer.contact_email if customer else '', type='email', required=not customer, - hint_text='Pflicht bei der Neuanlage -- ueber diese Adresse laeuft das Kundenportal.') }} + hint_text='Pflicht bei der Neuanlage -- über diese Adresse läuft das Kundenportal.') }} {{ field('contact_person', 'Ansprechpartner', value=customer.contact_person if customer else '') }} {{ field('contact_phone', 'Telefon', value=customer.contact_phone if customer else '') }} {{ field('street', 'Strasse', value=customer.street if customer else '') }} @@ -72,7 +72,7 @@ {% block dialogs %} {% if current_user.has('customers.create') %} - {% call modal('customer-new', 'Kunde anlegen', 'Adresse in Einzelfeldern -- fuer Rechnungen weiterverwendbar', 'wide') %} + {% call modal('customer-new', 'Kunde anlegen', 'Adresse in Einzelfeldern -- für Rechnungen weiterverwendbar', 'wide') %}
diff --git a/apps/tesm-license/src/tesm_license/templates/license/dashboard.html b/apps/tesm-license/src/tesm_license/templates/license/dashboard.html index 05ae415..f3afea0 100644 --- a/apps/tesm-license/src/tesm_license/templates/license/dashboard.html +++ b/apps/tesm-license/src/tesm_license/templates/license/dashboard.html @@ -1,7 +1,7 @@ {% extends "tesm_core/base.html" %} {% from "tesm_core/macros.html" import pill, empty_state, alert, stat %} -{% block page_title %}Uebersicht{% endblock %} +{% block page_title %}Übersicht{% endblock %} {% block page_sub %} {% if summary %} {{ summary.counts.customers }} Kunden · {{ summary.counts.licenses_active }} aktive Lizenzen @@ -24,13 +24,13 @@
{% if summary.counts.tickets_blocked %} - {{ alert(summary.counts.tickets_blocked ~ ' Ticket(s) sind gesperrt -- fuer diese Kunden + {{ alert(summary.counts.tickets_blocked ~ ' Ticket(s) sind gesperrt -- für diese Kunden lassen sich keine neuen Lizenzen ausstellen.', 'warning') }} {% endif %} {% for key, label, kind, hint in [ ('red', 'Kritisch', 'danger', 'Weniger als 30 Tage Restlaufzeit oder bereits abgelaufen'), - ('orange', 'Bald faellig', 'warning', '30 bis 90 Tage Restlaufzeit'), + ('orange', 'Bald fällig', 'warning', '30 bis 90 Tage Restlaufzeit'), ('green', 'In Ordnung', 'ok', 'Mehr als 90 Tage oder unbefristet')] %} {% set entries = summary.buckets[key] %} {% if entries %} diff --git a/apps/tesm-license/src/tesm_license/templates/license/license_detail.html b/apps/tesm-license/src/tesm_license/templates/license/license_detail.html index aa1088d..92e2cd3 100644 --- a/apps/tesm-license/src/tesm_license/templates/license/license_detail.html +++ b/apps/tesm-license/src/tesm_license/templates/license/license_detail.html @@ -26,13 +26,13 @@ {% block content %} {% if state.revision_pending %} {{ alert('Es wurde eine neue Revision signiert, die der Kunde noch nicht abgeholt hat. - Eine erreichbare Installation holt sie beim naechsten Heartbeat; eine Installation ohne - Verbindung braucht dafuer den Offline-Weg -- der Kunde findet ihn in seinem Portal.', + Eine erreichbare Installation holt sie beim nächsten Heartbeat; eine Installation ohne + Verbindung braucht dafür den Offline-Weg -- der Kunde findet ihn in seinem Portal.', 'warning', 'Revision ausstehend') }} {% endif %} {% if license.status == 'revoked' %} - {{ alert('Diese Lizenz ist widerrufen. Der Client erfaehrt das beim naechsten Heartbeat und + {{ alert('Diese Lizenz ist widerrufen. Der Client erfährt das beim nächsten Heartbeat und schaltet seine lizenzpflichtigen Funktionen ab.' ~ ((' Grund: ' ~ license.revoke_reason) if license.revoke_reason else ''), 'danger', 'Widerrufen') }} {% endif %} @@ -72,7 +72,7 @@ {% else %}keine{% endif %}
Ausgestellt
{{ license.issued_at | dt }} von {{ license.created_by or '-' }}
-
Laeuft ab
+
Läuft ab
{% if license.lifetime %}{{ pill('unbefristet', 'online') }} {% else %} @@ -105,9 +105,9 @@
-
{{ icon('refresh', size=17) }} Nachtraeglich anpassen
+
{{ icon('refresh', size=17) }} Nachträglich anpassen
- Signiert eine neue Revision. Der Client erhaelt sie beim naechsten Heartbeat -- + Signiert eine neue Revision. Der Client erhält sie beim nächsten Heartbeat -- ohne erneute Aktivierung.
@@ -126,9 +126,9 @@
{% endif %} - {{ field('extend_days', 'Laufzeit verlaengern (Tage)', type='number', + {{ field('extend_days', 'Laufzeit verlängern (Tage)', type='number', placeholder='0', - hint_text='Wird auf das aktuelle Ablaufdatum aufgeschlagen.') }} + hint_text='Wird auf das aktülle Ablaufdatum aufgeschlagen.') }}
LizenzTypZustandLaeuft abHost
LizenzTypZustandLäuft abHost
- + {% for entry in deliveries %} @@ -170,7 +170,7 @@
{{ icon('file-text', size=17) }} Signiertes Dokument
-
Genau so, wie es signiert wurde -- jede Aenderung bricht die Signatur
+
Genau so, wie es signiert wurde -- jede Änderung bricht die Signatur
{{ icon('chevron-down', size=16) }}
@@ -185,7 +185,7 @@
{{ icon('mail', size=17) }} Per E-Mail versenden
- {% if mail_ready %}Ueber Microsoft Graph{% else %}Nicht konfiguriert{% endif %} + {% if mail_ready %}Über Microsoft Graph{% else %}Nicht konfiguriert{% endif %}
@@ -193,7 +193,7 @@ data-no-guard>
- {{ field('recipient', 'Empfaenger', value=license.contact_email, type='email', + {{ field('recipient', 'Empfänger', value=license.contact_email, type='email', disabled=not mail_ready) }}
diff --git a/apps/tesm-license/src/tesm_license/templates/license/selfservice_offline.html b/apps/tesm-license/src/tesm_license/templates/license/selfservice_offline.html index f2fa05b..1f85c1c 100644 --- a/apps/tesm-license/src/tesm_license/templates/license/selfservice_offline.html +++ b/apps/tesm-license/src/tesm_license/templates/license/selfservice_offline.html @@ -9,9 +9,9 @@
{{ alert('In Ihrer TESM-Installation unter Verwaltung -- Lizenz auf "Lizenz aktivieren" - klicken. Erscheint dort ein Anfragecode, fuegen Sie ihn hier ein. Sie bekommen einen - Antwortcode zurueck, den Sie in TESM eintragen. Beide Codes sind signiert und lassen - sich unterwegs nicht veraendern.', 'info', 'So geht es') }} + klicken. Erscheint dort ein Anfragecode, fügen Sie ihn hier ein. Sie bekommen einen + Antwortcode zurück, den Sie in TESM eintragen. Beide Codes sind signiert und lassen + sich unterwegs nicht verändern.', 'info', 'So geht es') }} {% if response_code %}
@@ -35,7 +35,7 @@
@@ -44,7 +44,7 @@ {% endif %}

- Zurueck zur Uebersicht + Zurück zur Übersicht

diff --git a/apps/tesm-license/src/tesm_license/templates/license/settings_mail.html b/apps/tesm-license/src/tesm_license/templates/license/settings_mail.html index 30e5860..87db40c 100644 --- a/apps/tesm-license/src/tesm_license/templates/license/settings_mail.html +++ b/apps/tesm-license/src/tesm_license/templates/license/settings_mail.html @@ -4,7 +4,7 @@ {% block page_title %}E-Mail-Versand{% endblock %} {% block page_sub %} Microsoft Graph (App-only) · - {% if ready %}einsatzbereit{% else %}nicht vollstaendig konfiguriert{% endif %} + {% if ready %}einsatzbereit{% else %}nicht vollständig konfiguriert{% endif %} {% endblock %} {% block topbar_extra %} @@ -19,8 +19,8 @@ {% endblock %} {% block content %} -{{ alert('Der Versand laeuft ueber den Client-Credentials-Flow von Microsoft Graph -- kein SMTP, -kein zusaetzliches Paket. Die Anwendung braucht in Azure die Anwendungsberechtigung +{{ alert('Der Versand läuft über den Client-Credentials-Flow von Microsoft Graph -- kein SMTP, +kein zusätzliches Paket. Die Anwendung braucht in Azure die Anwendungsberechtigung Mail.Send. Der Verbindungstest holt nur ein Token und verschickt bewusst keine echte Nachricht.', 'info') }} @@ -46,7 +46,7 @@ bewusst keine echte Nachricht.', 'info') }}
Client-Secret {% if has_secret %}{{ pill('gesetzt', 'online') }}{% else %}{{ pill('fehlt', 'warning') }}{% endif %} - {{ field('client_secret', 'Neues Secret (leer = unveraendert)', type='password', + {{ field('client_secret', 'Neues Secret (leer = unverändert)', type='password', autocomplete='new-password', disabled=not can_secrets) }} {% if not can_secrets %}

@@ -63,17 +63,17 @@ bewusst keine echte Nachricht.', 'info') }}

-
{{ icon('info', size=17) }} Wofuer der Versand genutzt wird
+
{{ icon('info', size=17) }} Wofür der Versand genutzt wird
  • Lizenzdatei an den Kunden senden (mit Anhang).
  • -
  • Zugangslink fuer das Kundenportal.
  • +
  • Zugangslink für das Kundenportal.

- Das Secret liegt AES-256-GCM-verschluesselt in der Datenbank, gebunden an seinen - Verwendungszweck. Ohne die Schluesseldatei dieser Installation ist es nicht lesbar. + Das Secret liegt AES-256-GCM-verschlüsselt in der Datenbank, gebunden an seinen + Verwendungszweck. Ohne die Schlüsseldatei dieser Installation ist es nicht lesbar.

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 b26887d..1511b55 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 @@ -6,8 +6,8 @@ {% block content %} {% if not sysops_available %} - {{ alert('Systemhelfer nicht verfuegbar -- Hostname und Zeitzone koennen hier nur angezeigt, - nicht geaendert werden.', 'info') }} + {{ alert('Systemhelfer nicht verfügbar -- Hostname und Zeitzone können hier nur angezeigt, + nicht geändert werden.', 'info') }} {% endif %}
@@ -16,7 +16,7 @@
{{ icon('users', size=17) }} Kundenportal und Self-Service
-
Zwei unabhaengige Zeitfenster: Link und Sitzung
+
Zwei unabhängige Zeitfenster: Link und Sitzung
@@ -24,10 +24,10 @@
{{ checkbox('portal_enabled', 'Kundenportal aktiv', checked=values.portal_enabled) }}
- {{ field('portal_link_minutes', 'Zugangslink gueltig (Minuten)', + {{ field('portal_link_minutes', 'Zugangslink gültig (Minuten)', value=values.portal_link_minutes, type='number', hint_text=specs['portal_link_minutes'].hint) }} - {{ field('portal_session_minutes', 'Portalsitzung gueltig (Minuten)', + {{ field('portal_session_minutes', 'Portalsitzung gültig (Minuten)', value=values.portal_session_minutes, type='number', hint_text=specs['portal_session_minutes'].hint) }}
@@ -35,7 +35,7 @@ {{ checkbox('self_service_enabled', 'Self-Service-Link je Ticket aktiv', checked=values.self_service_enabled, hint_text=specs['self_service_enabled'].hint) }} - {{ checkbox('allow_customer_revoke', 'Kunden duerfen ihre Lizenz selbst freigeben', + {{ checkbox('allow_customer_revoke', 'Kunden dürfen ihre Lizenz selbst freigeben', checked=values.allow_customer_revoke, hint_text=specs['allow_customer_revoke'].hint) }}
@@ -60,6 +60,9 @@ 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') }}
@@ -105,7 +108,7 @@
{{ icon('sliders', size=17) }} Navigationsreihenfolge
-
Gilt fuer alle Benutzer
+
Gilt für alle Benutzer
diff --git a/apps/tesm-license/src/tesm_license/templates/license/settings_vendor.html b/apps/tesm-license/src/tesm_license/templates/license/settings_vendor.html index c2e5e46..9a9deb2 100644 --- a/apps/tesm-license/src/tesm_license/templates/license/settings_vendor.html +++ b/apps/tesm-license/src/tesm_license/templates/license/settings_vendor.html @@ -6,7 +6,7 @@ {% block content %} {{ alert('Diese Angaben werden in jede Lizenzdatei eingebettet und beim Kunden auf der -Lizenzseite angezeigt -- inklusive Logo. Der oeffentliche Endpunkt entscheidet zusaetzlich, +Lizenzseite angezeigt -- inklusive Logo. Der öffentliche Endpunkt entscheidet zusätzlich, welche Basis-URL in kundengerichteten Links steht.', 'info') }}
@@ -26,18 +26,18 @@ welche Basis-URL in kundengerichteten Links steht.', 'info') }} disabled=not current_user.has('settings_vendor.edit')) }}
{{ textarea_field('vendor_address', 'Adresse', value=values.vendor_address, rows=3) }} - {{ field('master_endpoint', 'Oeffentlicher Endpunkt', value=values.master_endpoint, + {{ field('master_endpoint', 'Öffentlicher Endpunkt', value=values.master_endpoint, placeholder='https://lizenz.firma.de', disabled=not current_user.has('settings_vendor.edit'), hint_text='Basis-URL, unter der Kundeninstanzen diesen Server erreichen. Wird in - jede Lizenz eingebettet und fuer alle kundengerichteten Links verwendet -- statt - des Host-Headers der aktuellen Anfrage, der bei internen Aufrufen falsch waere.', + jede Lizenz eingebettet und für alle kundengerichteten Links verwendet -- statt + des Host-Headers der aktüllen Anfrage, der bei internen Aufrufen falsch wäre.', extra_class='field--full') }}
Logo (maximal {{ max_logo_kb }} KB)
{% if values.vendor_logo_base64 %} - Aktuelles Logo {% else %}

Kein Logo hinterlegt.

@@ -45,7 +45,7 @@ welche Basis-URL in kundengerichteten Links steht.', 'info') }} - Ein zu grosses Logo wird abgelehnt, die uebrigen Angaben werden trotzdem gespeichert. + Ein zu grosses Logo wird abgelehnt, die übrigen Angaben werden trotzdem gespeichert.
@@ -75,7 +75,7 @@ welche Basis-URL in kundengerichteten Links steht.', 'info') }}

Die Angaben werden zum Zeitpunkt der Ausstellung in die Lizenz kopiert und signiert. Eine - Aenderung hier wirkt auf neue Lizenzen -- bestehende behalten ihren Stand, bis eine neue + Änderung hier wirkt auf neue Lizenzen -- bestehende behalten ihren Stand, bis eine neue Revision signiert wird.

diff --git a/apps/tesm-license/src/tesm_license/templates/license/signing_key.html b/apps/tesm-license/src/tesm_license/templates/license/signing_key.html index 0baf53f..e2d8d90 100644 --- a/apps/tesm-license/src/tesm_license/templates/license/signing_key.html +++ b/apps/tesm-license/src/tesm_license/templates/license/signing_key.html @@ -1,34 +1,34 @@ {% extends "tesm_core/base.html" %} {% from "tesm_core/macros.html" import alert %} -{% block page_title %}Signaturschluessel{% endblock %} -{% block page_sub %}Schluessel-ID {{ key_id }}{% endblock %} +{% block page_title %}Signaturschlüssel{% endblock %} +{% block page_sub %}Schlüssel-ID {{ key_id }}{% endblock %} {% block breadcrumb %} {% endblock %} {% block content %} -{{ alert('Der oeffentliche Teil ist kein Geheimnis -- er reist in jeder ausgestellten Lizenz mit. -Ihn hier anzuzeigen erlaubt es, ihn unabhaengig zu vergleichen, bevor ein Kunde ihn als -vertrauenswuerdig hinterlegt.', 'info') }} +{{ alert('Der öffentliche Teil ist kein Geheimnis -- er reist in jeder ausgestellten Lizenz mit. +Ihn hier anzuzeigen erlaubt es, ihn unabhängig zu vergleichen, bevor ein Kunde ihn als +vertrauenswürdig hinterlegt.', 'info') }} {% if health.problems %} {% for problem in health.problems %}{{ alert(problem, 'danger') }}{% endfor %} {% endif %}
-
{{ icon('key', size=17) }} Oeffentlicher Schluessel
+
{{ icon('key', size=17) }} Öffentlicher Schlüssel
Erzeugt {{ created_at | dt }}
-
Schluessel-ID
{{ key_id }}
+
Schlüssel-ID
{{ key_id }}
Verfahren
Ed25519 (EdDSA)
{{ public_key }}
{{ alert('Der private Teil liegt ausschliesslich auf diesem Host (Modus 0600) und ist aus dem - Deployment-Abgleich ausgeschlossen. Ginge er verloren, waere jede - ausgestellte Lizenz nicht mehr pruefbar -- bitte getrennt sichern.', 'warning') }} + Deployment-Abgleich ausgeschlossen. Ginge er verloren, wäre jede + ausgestellte Lizenz nicht mehr prüfbar -- bitte getrennt sichern.', 'warning') }}
{% endblock %} diff --git a/apps/tesm-license/src/tesm_license/templates/license/ticket_detail.html b/apps/tesm-license/src/tesm_license/templates/license/ticket_detail.html index 1d75ca1..04cfec8 100644 --- a/apps/tesm-license/src/tesm_license/templates/license/ticket_detail.html +++ b/apps/tesm-license/src/tesm_license/templates/license/ticket_detail.html @@ -17,11 +17,11 @@ {% block content %} {% if endpoint_missing %} - {{ alert(endpoint_missing, 'danger', 'Keine Ausstellung moeglich') }} + {{ alert(endpoint_missing, 'danger', 'Keine Ausstellung möglich') }} {% endif %} {% if ticket.status == 'blocked' %} {{ alert('Dieses Ticket ist gesperrt. Es lassen sich keine neuen Lizenzen ausstellen -- weder - hier noch ueber den Self-Service des Kunden. Bereits ausgestellte Lizenzen bleiben gueltig.', + hier noch über den Self-Service des Kunden. Bereits ausgestellte Lizenzen bleiben gültig.', 'danger', 'Gesperrt') }} {% endif %} @@ -58,7 +58,7 @@
ZeitWegEmpfaengerDurchDetail
ZeitWegEmpfängerDurchDetail
{{ entry.issued_at | date }} {% if entry.status == 'revoked' %}freigegeben - {% elif entry.status == 'deactivated' %}abgeloest + {% elif entry.status == 'deactivated' %}abgelöst {% else %}{{ entry.status }}{% endif %} {{ entry.hostname or '–' | safe }}
- + {% for entry in licenses %} @@ -115,7 +115,7 @@
-
{{ icon('link', size=17) }} Self-Service fuer den Kunden
+
{{ icon('link', size=17) }} Self-Service für den Kunden
Ohne Anmeldung nutzbar – die Berechtigung ist der Besitz dieses Links
@@ -128,7 +128,7 @@

Damit kann der Kunde seine Lizenz herunterladen, bei einem Systemwechsel freigeben und - neu ausstellen. Der Link enthaelt 192 Bit Zufall und ist nicht ratbar -- behandeln Sie + neu ausstellen. Der Link enthält 192 Bit Zufall und ist nicht ratbar -- behandeln Sie ihn wie ein Passwort.

@@ -143,7 +143,7 @@
{% elif ticket.role != 'customer' %} - {{ alert('Fuer Lizenzserver-Tickets ist der Self-Service vollstaendig gesperrt -- diese + {{ alert('Für Lizenzserver-Tickets ist der Self-Service vollständig gesperrt -- diese Lizenzen werden ausschliesslich hier verwaltet.', 'info') }} {% endif %} @@ -158,8 +158,8 @@
- {{ alert('Aenderungen wirken auf die naechste Ausstellung. Kunde und Rolle sind - unveraenderlich.', 'info') }} + {{ alert('Änderungen wirken auf die nächste Ausstellung. Kunde und Rolle sind + unveränderlich.', 'info') }}
- Module (nur bei „Individuell“) + Module (nur bei „Individüll“)
{% for module in modules %} {{ checkbox('modules', module_labels[module], value=module) }} @@ -65,7 +65,7 @@ {{ field('valid_days', 'Laufzeit (Tage)', value=default_valid_days, type='number') }}
{{ checkbox('lifetime', 'Unbefristet', - hint_text='Nicht kombinierbar mit einer Testlizenz -- das waere eine + hint_text='Nicht kombinierbar mit einer Testlizenz -- das wäre eine permanente Lizenz ohne Module.') }} {{ textarea_field('notes', 'Interne Notiz', rows=2) }}
@@ -77,15 +77,15 @@
{{ icon('share', size=17) }} Lizenzserver-Lizenz
- Erzeugt eine neue Vertrauensgrenze -- der Empfaenger verwaltet danach eigene Kunden + Erzeugt eine neue Vertrauensgrenze -- der Empfänger verwaltet danach eigene Kunden
- {{ alert('Ein so lizenzierter Lizenzserver darf alles, was ein Lizenzserver koennen + {{ alert('Ein so lizenzierter Lizenzserver darf alles, was ein Lizenzserver können soll -- ausser selbst weitere Lizenzserver-Lizenzen auszustellen. Er meldet seinen - Bestand automatisch hierher zurueck. Module sind fuer diese Rolle bedeutungslos und - werden ignoriert. Self-Service und Kundenportal sind fuer solche Tickets gesperrt.', + Bestand automatisch hierher zurück. Module sind für diese Rolle bedeutungslos und + werden ignoriert. Self-Service und Kundenportal sind für solche Tickets gesperrt.', 'warning') }}
LizenzAusgestelltZustandLaeuft abHost
LizenzAusgestelltZustandLäuft abHost
{% if not tickets %} - {{ empty_state('Keine Tickets', 'Legen Sie das erste Ticket fuer einen Kunden an.', 'ticket') }} + {{ empty_state('Keine Tickets', 'Legen Sie das erste Ticket für einen Kunden an.', 'ticket') }} {% endif %}
{% endblock %} diff --git a/deploy/systemd/tesm.service b/deploy/systemd/tesm.service index 51cb663..a3b557e 100644 --- a/deploy/systemd/tesm.service +++ b/deploy/systemd/tesm.service @@ -59,7 +59,20 @@ RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_NETLINK LockPersonality=yes MemoryDenyWriteExecute=no SystemCallArchitectures=native -SystemCallFilter=@system-service +# "@mount" zusaetzlich zu @system-service: die Dateifreigaben werden ueber +# mount.cifs eingebunden, und @system-service enthaelt mount/umount2 nicht. +# Ohne diese Ergaenzung scheitert jede Einbindung mit "mount error(1): +# Operation not permitted" -- gemessen, und zwar unabhaengig von Rechten, +# Zugangsdaten und Mount-Optionen: aus einer normalen Root-Shell gelingt +# derselbe Aufruf sofort. Der Umweg ueber nsenter in den Namensraum des Wirts +# ist keine Alternative, weil "RestrictNamespaces=yes" setns verbietet. +# +# Eingebunden wird damit im Mount-Namensraum *dieses* Dienstes. Das ist genau +# richtig: nur er liest die Dateien, andere Prozesse haben dort nichts zu +# suchen, und mit dem Dienst verschwinden die Einbindungen wieder. Die +# Aufraeumaufgabe fuer abgelaufene Einbindungen laeuft deshalb im Webprozess -- +# der Ueberwachungsdienst koennte sie nicht einmal sehen. +SystemCallFilter=@system-service @mount SystemCallErrorNumber=EPERM # Bewusst **kein** einschraenkendes CapabilityBoundingSet: sudo braucht # CAP_SETUID/CAP_SETGID, und der Helfer dahinter braucht Root. Was hier fehlt, diff --git a/docs/BETRIEB.md b/docs/BETRIEB.md index 6cdf02f..a5b3a99 100644 --- a/docs/BETRIEB.md +++ b/docs/BETRIEB.md @@ -2,7 +2,7 @@ Dieses Dokument richtet sich an die Person, die die Anlage installiert, aktualisiert und im Fehlerfall wieder zum Laufen bringt. Es beschreibt, was auf -dem Host tatsaechlich passiert -- nicht, wie die Oberflaeche bedient wird. +dem Host tatsächlich passiert -- nicht, wie die Oberfläche bedient wird. --- @@ -11,7 +11,7 @@ dem Host tatsaechlich passiert -- nicht, wie die Oberflaeche bedient wird. | Zweck | Pfad | |---|---| | Anwendung (Code, venv, statische Dateien) | `/srv//` | -| Instanzdaten (Datenbank, Schluessel, Lizenz) | `/srv//data/` | +| Instanzdaten (Datenbank, Schlüssel, Lizenz) | `/srv//data/` | | Protokolle | `/var/log//` | | Umgebungsdatei | `/etc/tesm/.env` | | Zertifikate (selbstsigniert) | `/etc/tesm/certs//` | @@ -20,13 +20,13 @@ dem Host tatsaechlich passiert -- nicht, wie die Oberflaeche bedient wird. | Privilegierter Helfer | `/usr/local/lib/tesm/tesm-helper` | | Verwaltungswerkzeug | `/usr/local/bin/-admin` | -`` ist `tesm` bzw. `tesm-license` -- oder, bei einer zusaetzlichen +`` ist `tesm` bzw. `tesm-license` -- oder, bei einer zusätzlichen Instanz, `tesm-`. Dieser Name (der **SITE_KEY**) benennt alles, was -ausserhalb von `/srv/` liegt. Deshalb koennen zwei Installationen +ausserhalb von `/srv/` liegt. Deshalb können zwei Installationen derselben Anwendung nebeneinander laufen, ohne sich gegenseitig die -nginx-Site, die netplan-Datei oder das Sitzungscookie zu ueberschreiben. +nginx-Site, die netplan-Datei oder das Sitzungscookie zu überschreiben. -Alles, was ein Mensch verlieren wuerde, liegt unter `data/`. Das Verzeichnis +Alles, was ein Mensch verlieren würde, liegt unter `data/`. Das Verzeichnis wird von `install.sh` nie angefasst. --- @@ -43,10 +43,10 @@ Wichtige Optionen: | Option | Wirkung | |---|---| | `--https` | Port 443 mit TLS, Port 80 leitet dorthin um. Ohne `--domain` entsteht ein selbstsigniertes Zertifikat. | -| `--domain ` | Oeffentlicher Name. Setzt `server_name`; die Site ist dann **nicht** mehr die Vorgabe-Site. | -| `--acme-email ` | Erst zusammen mit `--domain` wird ein Let's-Encrypt-Zertifikat angefordert (und HSTS gesetzt). Ohne die Adresse bleibt es selbstsigniert -- interne Namen wie `lizenz.firma.local` kann keine CA bestaetigen. | -| `--instance ` | Zusaetzliche Instanz neben der Hauptinstallation. | -| `--port` / `--http-port` / `--https-port` | Abweichende Ports (nur fuer Testinstanzen sinnvoll). | +| `--domain ` | Öffentlicher Name. Setzt `server_name`; die Site ist dann **nicht** mehr die Vorgabe-Site. | +| `--acme-email ` | Erst zusammen mit `--domain` wird ein Let's-Encrypt-Zertifikat angefordert (und HSTS gesetzt). Ohne die Adresse bleibt es selbstsigniert -- interne Namen wie `lizenz.firma.local` kann keine CA bestätigen. | +| `--instance ` | Zusätzliche Instanz neben der Hauptinstallation. | +| `--port` / `--http-port` / `--https-port` | Abweichende Ports (nur für Testinstanzen sinnvoll). | | `--skip-packages` | Keine Systempakete installieren. | Danach: @@ -55,17 +55,17 @@ Danach: sudo -u tesm tesm-admin create-admin ``` -Beim Lizenzserver zusaetzlich, **in dieser Reihenfolge**: +Beim Lizenzserver zusätzlich, **in dieser Reihenfolge**: ```bash -sudo -u tesm-license tesm-license-admin init-key # Signaturschluessel -- sofort sichern! +sudo -u tesm-license tesm-license-admin init-key # Signaturschlüssel -- sofort sichern! sudo -u tesm-license tesm-license-admin bootstrap # eigene Master-Lizenz sudo -u tesm-license tesm-license-admin create-admin ``` -> **Der Signaturschluessel ist nicht wiederherstellbar.** Geht +> **Der Signaturschlüssel ist nicht wiederherstellbar.** Geht > `data/master_signing_key.json` verloren, ist keine ausgestellte Lizenz mehr -> pruefbar. Sichern Sie ihn getrennt vom Server. +> prüfbar. Sichern Sie ihn getrennt vom Server. ### Zwei Anwendungen auf einem Host @@ -78,7 +78,7 @@ sudo bash deploy/install.sh --app tesm-license --https --domain lizenz.firma.loc Die Installation **ohne** Domain wird zur Vorgabe-Site (`default_server`) und beantwortet alles, was zu keinem Namen passt. Zwei namenlose Sites auf -demselben Port weist nginx ab -- richtig so, und beim Uebernehmen sofort +demselben Port weist nginx ab -- richtig so, und beim Übernehmen sofort sichtbar. --- @@ -90,14 +90,14 @@ sudo bash deploy/install.sh --app tesm ``` Derselbe Befehl. Das Skript erkennt an der vorhandenen Datenbank, dass es eine -Aktualisierung ist, und tut dann zusaetzlich: +Aktualisierung ist, und tut dann zusätzlich: -1. **Vollstaendige Sicherung** nach `/srv/-backup-` -- - unabhaengig davon, ob sich das Schema geaendert hat. +1. **Vollständige Sicherung** nach `/srv/-backup-` -- + unabhängig davon, ob sich das Schema geändert hat. 2. Dateiabgleich; `data/` bleibt unangetastet. -3. Schema-Migrationen beim Start (versioniert, mit Pruefsumme). -4. **Gesundheitspruefung.** Antwortet die Anwendung nicht, wird der alte Stand - automatisch zurueckgeholt; die fehlgeschlagene Version bleibt unter +3. Schema-Migrationen beim Start (versioniert, mit Prüfsumme). +4. **Gesundheitsprüfung.** Antwortet die Anwendung nicht, wird der alte Stand + automatisch zurückgeholt; die fehlgeschlagene Version bleibt unter `/srv/-failed-` zur Analyse liegen. Eine bereits eingerichtete HTTPS-Konfiguration bleibt erhalten, auch ohne @@ -114,7 +114,7 @@ Drei Wege dorthin: **a) Bei der Installation** -- siehe oben, `--https`. -**b) Nachtraeglich ueber die Kommandozeile:** +**b) Nachträglich über die Kommandozeile:** ```bash sudo -u tesm tesm-admin web-setup --https --self-signed --apply @@ -124,42 +124,42 @@ sudo -u tesm tesm-admin web-setup --https --domain tesm.firma.de \ `--print-config` zeigt die Datei, ohne etwas zu schreiben. -**c) In der Oberflaeche** unter *Einstellungen -> Webserver und TLS*. +**c) In der Oberfläche** unter *Einstellungen -> Webserver und TLS*. Danach muss `COOKIE_SECURE` in `/etc/tesm/.env` auf `1` stehen (alle drei Wege erledigen das selbst) und der Dienst neu gestartet werden. ### Die Site-Datei wird immer neu erzeugt -Sie ist **kein** Ort fuer Handarbeit. Jede Uebernahme schreibt sie vollstaendig -aus der Vorlage neu. Was frueher von Hand hineingeschrieben wurde -- Domain, +Sie ist **kein** Ort für Handarbeit. Jede Übernahme schreibt sie vollständig +aus der Vorlage neu. Was früher von Hand hineingeschrieben wurde -- Domain, Ports, Zertifikatspfade, HSTS, Upload-Grenze -- ist heute Einstellung in der Anwendung. -Der Grund steht im Vorgaengerprojekt: dort wurde die Datei nur angelegt, wenn -sie fehlte. Ein einmal falscher `alias`-Pfad ueberlebte dadurch jahrelang jedes -Update, und die Oberflaeche kam die ganze Zeit ohne Design. +Der Grund steht im Vorgängerprojekt: dort wurde die Datei nur angelegt, wenn +sie fehlte. Ein einmal falscher `alias`-Pfad überlebte dadurch jahrelang jedes +Update, und die Oberfläche kam die ganze Zeit ohne Design. ### Selbstsigniert oder Let's Encrypt? -* **Oeffentlich erreichbar, echter DNS-Name:** Let's Encrypt. Nur dann ist HSTS +* **Öffentlich erreichbar, echter DNS-Name:** Let's Encrypt. Nur dann ist HSTS sinnvoll. * **Geschlossenes Netz:** selbstsigniert. Browser zeigen eine Warnung, die - Verbindung ist trotzdem verschluesselt und das Sitzungscookie darf `Secure` + Verbindung ist trotzdem verschlüsselt und das Sitzungscookie darf `Secure` tragen. **HSTS hier nicht einschalten** -- der Browser merkt sich die Vorgabe - monatelang und die Warnung laesst sich dann nicht mehr wegklicken. + monatelang und die Warnung lässt sich dann nicht mehr wegklicken. -Die Erneuerung von Let's Encrypt laeuft ueber `certbot certonly --webroot` -(bewusst nicht ueber das nginx-Plugin, das die erzeugte Site umschreiben -wuerde). Der ACME-Pfad bleibt auch bei aktiver Umleitung erreichbar. +Die Erneuerung von Let's Encrypt läuft über `certbot certonly --webroot` +(bewusst nicht über das nginx-Plugin, das die erzeugte Site umschreiben +würde). Der ACME-Pfad bleibt auch bei aktiver Umleitung erreichbar. --- ## 5. Dienste ```bash -systemctl status tesm.service # Weboberflaeche (gunicorn) -systemctl status tesm-monitor.service # Ueberwachungsschleife (nur TESM) +systemctl status tesm.service # Weboberfläche (gunicorn) +systemctl status tesm-monitor.service # Überwachungsschleife (nur TESM) systemctl status tesm-license.service journalctl -u tesm -n 100 -f ``` @@ -179,8 +179,8 @@ Die Anwendung hat keine. Alles, was Root braucht, geht durch **einen** Pfad: /usr/local/lib/tesm/tesm-helper [argumente] ``` -Der Helfer gehoert `root`, ist per sudoers ausschliesslich fuer genau diesen -Pfad mit `NOPASSWD` freigegeben und prueft jedes Argument gegen eine +Der Helfer gehört `root`, ist per sudoers ausschliesslich für genau diesen +Pfad mit `NOPASSWD` freigegeben und prüft jedes Argument gegen eine Positivliste. Er reicht nie etwas an eine Shell weiter (kein `eval`, kein `sh -c`). @@ -191,16 +191,16 @@ sudo /usr/local/lib/tesm/tesm-helper grep -n ')$' /usr/local/lib/tesm/tesm-helper ``` -Regel fuer Aenderungen: **Jedes neue Verb braucht eine Argumentpruefung.** +Regel für Änderungen: **Jedes neue Verb braucht eine Argumentprüfung.** `tests/test_repo_hygiene.py` erzwingt, dass jedes der Anwendung bekannte Verb -im Helfer vorkommt und dass jede Pfadpruefung `..` abweist. +im Helfer vorkommt und dass jede Pfadprüfung `..` abweist. --- ## 7. Sicherung und Wiederherstellung -In der Oberflaeche unter *Sicherung* -- verschluesselte Ausfuhr (Argon2id + -AES-256-GCM) mit einem selbst gewaehlten Kennwort. Der Import zeigt zuerst eine +In der Oberfläche unter *Sicherung* -- verschlüsselte Ausfuhr (Argon2id + +AES-256-GCM) mit einem selbst gewählten Kennwort. Der Import zeigt zuerst eine Vorschau, bevor irgendetwas geschrieben wird. Auf dem Host reicht: @@ -214,10 +214,10 @@ sudo systemctl start tesm Unverzichtbar: * `data/app.db` -- alle Daten -* `data/secret.key` -- ohne sie sind alle Sitzungen ungueltig +* `data/secret.key` -- ohne sie sind alle Sitzungen ungültig * `data/data.keys` -- ohne sie sind **alle gespeicherten Geheimnisse verloren** * `data/license.json` und `data/license_key.json` -* Lizenzserver zusaetzlich: `data/master_signing_key.json` +* Lizenzserver zusätzlich: `data/master_signing_key.json` --- @@ -230,17 +230,17 @@ curl -s http://127.0.0.1:5000/gesundheit ``` Antwortet gunicorn, aber der Browser nicht, liegt es an nginx. -**nginx laesst sich nicht neu laden.** +**nginx lässt sich nicht neu laden.** ```bash sudo nginx -t ``` -Die Uebernahme aus der Anwendung nimmt eine abgelehnte Konfiguration selbst -zurueck. Eine von Hand bearbeitete Datei tut das nicht -- in dem Fall hilft +Die Übernahme aus der Anwendung nimmt eine abgelehnte Konfiguration selbst +zurück. Eine von Hand bearbeitete Datei tut das nicht -- in dem Fall hilft `install.sh`, das eine unbrauchbare Site durch die Vorlage ersetzt. -**Anmeldung schlaegt ohne Fehlermeldung fehl.** +**Anmeldung schlägt ohne Fehlermeldung fehl.** Fast immer `COOKIE_SECURE=1` ohne HTTPS. Der Browser bekommt dann ein Cookie, -das er ueber HTTP nie zuruecksendet. +das er über HTTP nie zurücksendet. ```bash grep COOKIE_SECURE /etc/tesm/tesm.env ``` @@ -250,38 +250,38 @@ Der Reverse Proxy reicht den Host falsch durch. In der Site-Datei muss `proxy_set_header Host $http_host;` stehen -- **nicht** `$host`, das verwirft den Port. -**Statische Dateien fehlen, die Oberflaeche ist unformatiert.** +**Statische Dateien fehlen, die Oberfläche ist unformatiert.** ```bash grep alias /etc/nginx/sites-available/tesm ls /srv/tesm/static /srv/tesm/static-core ``` -Beide `alias`-Pfade muessen in das Installationsverzeichnis zeigen. +Beide `alias`-Pfade müssen in das Installationsverzeichnis zeigen. -**Zwei Instanzen stoeren sich.** +**Zwei Instanzen stören sich.** `SITE_KEY` in beiden `/etc/tesm/*.env` vergleichen -- er muss sich unterscheiden. Sonst teilen sich beide nginx-Site und Sitzungscookie. -**Die Wartung meldet "Der SSH-Host-Schluessel ist unbekannt".** +**Die Wartung meldet "Der SSH-Host-Schlüssel ist unbekannt".** Kein Fehler, sondern die Absicht: ein Hintergrundauftrag darf keinen fremden -Schluessel akzeptieren. Die Freigabe erfolgt von Hand, in zwei Schritten, auf -der Detailseite des Geraets (*Clients -> Geraet -> SSH-Host-Schluessel*): -erst **Auslesen**, dann den Fingerprint mit einer unabhaengigen Quelle -vergleichen, dann **Freigeben**. Die Wartungsuebersicht zeigt in einer eigenen -Spalte, fuer welche Geraete das noch offen ist. Fuer Switche liegt dieselbe +Schlüssel akzeptieren. Die Freigabe erfolgt von Hand, in zwei Schritten, auf +der Detailseite des Geräts (*Clients -> Gerät -> SSH-Host-Schlüssel*): +erst **Auslesen**, dann den Fingerprint mit einer unabhängigen Quelle +vergleichen, dann **Freigeben**. Die Wartungsübersicht zeigt in einer eigenen +Spalte, für welche Geräte das noch offen ist. Für Switche liegt dieselbe Freigabe auf der Switch-Detailseite. -Der Schluessel wird nach **IP und Port** gespeichert, nicht nach Name -- TESM -verbindet sich immer ueber die hinterlegte IP-Adresse. Aendert sich die IP -eines Geraets, ist der Schluessel erneut freizugeben. +Der Schlüssel wird nach **IP und Port** gespeichert, nicht nach Name -- TESM +verbindet sich immer über die hinterlegte IP-Adresse. Ändert sich die IP +eines Geräts, ist der Schlüssel erneut freizugeben. -**Die Pruefkette des Aenderungsprotokolls ist gebrochen.** +**Die Prüfkette des Änderungsprotokolls ist gebrochen.** Unter *Diagnose* sichtbar. Das Protokoll ist verkettet gehasht; ein Bruch heisst, dass jemand direkt in die Datenbank geschrieben hat. Der Eintrag, ab dem es nicht mehr stimmt, wird genannt. --- -## 9. Zurueck auf eine aeltere Version +## 9. Zurück auf eine ältere Version ```bash sudo systemctl stop tesm @@ -290,6 +290,6 @@ sudo cp -a /srv/tesm-backup- /srv/tesm sudo systemctl start tesm ``` -Schema-Migrationen laufen nur vorwaerts. Ein Ruecksprung ueber eine -Schema-Aenderung hinweg braucht deshalb auch die Datenbank aus der Sicherung -- +Schema-Migrationen laufen nur vorwärts. Ein Rücksprung über eine +Schema-Änderung hinweg braucht deshalb auch die Datenbank aus der Sicherung -- genau das liegt im Backup-Verzeichnis. diff --git a/docs/SICHERHEIT.md b/docs/SICHERHEIT.md index feed727..68104f0 100644 --- a/docs/SICHERHEIT.md +++ b/docs/SICHERHEIT.md @@ -1,6 +1,6 @@ # Sicherheit -Was geschuetzt wird, womit, und wo bewusst Grenzen gezogen sind. +Was geschützt wird, womit, und wo bewusst Grenzen gezogen sind. --- @@ -9,35 +9,35 @@ Was geschuetzt wird, womit, und wo bewusst Grenzen gezogen sind. | | | |---|---| | Speicherung | Argon2id (argon2-cffi) | -| Altbestand | bcrypt wird noch geprueft und bei der naechsten erfolgreichen Anmeldung stillschweigend auf Argon2id umgestellt | -| Zweiter Faktor | TOTP, optional erzwingbar fuer Administratoren | +| Altbestand | bcrypt wird noch geprüft und bei der nächsten erfolgreichen Anmeldung stillschweigend auf Argon2id umgestellt | +| Zweiter Faktor | TOTP, optional erzwingbar für Administratoren | | Sperre je Konto | nach `LOGIN_MAX_ATTEMPTS` Versuchen | -| Sperre je Adresse | zusaetzlich `LOGIN_MAX_ATTEMPTS_PER_IP` -- sonst laesst sich die Kontosperre umgehen, indem man viele Konten je einmal probiert | -| Verzeichnisdienst | LDAP nur ueber TLS, Zertifikat wird geprueft. Ohne gueltiges Zertifikat keine Anmeldung -- kein Schalter, der das abschaltet | +| Sperre je Adresse | zusätzlich `LOGIN_MAX_ATTEMPTS_PER_IP` -- sonst lässt sich die Kontosperre umgehen, indem man viele Konten je einmal probiert | +| Verzeichnisdienst | LDAP nur über TLS, Zertifikat wird geprüft. Ohne gültiges Zertifikat keine Anmeldung -- kein Schalter, der das abschaltet | Nach einer Anmeldung wird die Sitzungskennung neu vergeben (gegen Session Fixation) und das CSRF-Token gewechselt. -### Erneute Bestaetigung ("sudo-Modus") +### Erneute Bestätigung ("sudo-Modus") -Alles, was Geheimnisse offenlegt oder Daten aus dem System heraustraegt, +Alles, was Geheimnisse offenlegt oder Daten aus dem System herausträgt, verlangt eine frische Passworteingabe: Klartextanzeige von Zugangsdaten, -Ausfuhr, Herunterladen eines Lizenzbundles, Schluesselrotation, -2FA-Aenderungen, Deaktivierung einer Lizenz. +Ausfuhr, Herunterladen eines Lizenzbundles, Schlüsselrotation, +2FA-Änderungen, Deaktivierung einer Lizenz. -Ein uebernommenes Sitzungscookie allein reicht dafuer nicht. +Ein übernommenes Sitzungscookie allein reicht dafür nicht. --- ## 2. Sitzungen -Serverseitig in SQLite, nicht im Cookie. Das Cookie traegt nur eine +Serverseitig in SQLite, nicht im Cookie. Das Cookie trägt nur eine Zufallskennung. * `HttpOnly`, `SameSite=Lax`, `Secure` sobald HTTPS eingerichtet ist -* Leerlaufzeit **und** absolute Hoechstdauer -* Der Cookiename enthaelt den `SITE_KEY` -- zwei Installationen auf einem Host - koennen sich die Sitzung nicht gegenseitig ueberschreiben (Cookies +* Leerlaufzeit **und** absolute Höchstdauer +* Der Cookiename enthält den `SITE_KEY` -- zwei Installationen auf einem Host + können sich die Sitzung nicht gegenseitig überschreiben (Cookies unterscheiden keine Ports) * Aktive Sitzungen sind einsehbar und einzeln beendbar @@ -45,22 +45,22 @@ Zufallskennung. ## 3. CSRF -Drei voneinander unabhaengige Schichten: +Drei voneinander unabhängige Schichten: 1. `SameSite=Lax` am Sitzungscookie. 2. Ein Token je Sitzung, in jedem Formular, in konstanter Zeit verglichen. -3. Origin-/Referer-Pruefung gegen den Host der Anfrage. +3. Origin-/Referer-Prüfung gegen den Host der Anfrage. Die dritte Schicht akzeptiert den Host mit und ohne Port sowie die Standardports -- **nicht** aber beliebige Ports: ein anderer Dienst auf demselben Rechner ist ein anderer Ursprung. -Der Vorgaenger hatte gar keinen CSRF-Schutz. Eine praeparierte Seite konnte im -Namen eines angemeldeten Administrators Geraete loeschen oder Benutzer anlegen. +Der Vorgänger hatte gar keinen CSRF-Schutz. Eine präparierte Seite konnte im +Namen eines angemeldeten Administrators Geräte löschen oder Benutzer anlegen. -Ausnahmen (`@csrf.exempt`) gibt es nur fuer Maschinenschnittstellen, die sich -**nicht** ueber Cookies autorisieren. Eine cookie-autorisierte Route ohne -CSRF-Schutz waere eine Luecke. +Ausnahmen (`@csrf.exempt`) gibt es nur für Maschinenschnittstellen, die sich +**nicht** über Cookies autorisieren. Eine cookie-autorisierte Route ohne +CSRF-Schutz wäre eine Lücke. --- @@ -69,75 +69,75 @@ CSRF-Schutz waere eine Luecke. Bei jeder Antwort: * `Content-Security-Policy` mit einem Nonce je Anfrage. **Kein** - `unsafe-inline` fuer Skripte -- deshalb gibt es im ganzen Projekt kein - Inline-Script ohne Nonce, und JavaScript haengt ueber + `unsafe-inline` für Skripte -- deshalb gibt es im ganzen Projekt kein + Inline-Script ohne Nonce, und JavaScript hängt über `data-behavior`-Attribute am DOM. `style-src` erlaubt `unsafe-inline`, weil einzelne berechnete Breiten (Fortschrittsbalken) als Style-Attribut gesetzt - werden; Style-Attribute fuehren keinen Code aus. + werden; Style-Attribute führen keinen Code aus. * `X-Content-Type-Options: nosniff` * `X-Frame-Options: DENY`, `frame-ancestors 'none'` * `Referrer-Policy: same-origin` * `Permissions-Policy` -- Kamera, Mikrofon, Ort, Zahlung, USB abgeschaltet * `Cross-Origin-Opener-Policy`, `Cross-Origin-Resource-Policy` -* `Strict-Transport-Security` nur, wenn TLS tatsaechlich terminiert wird +* `Strict-Transport-Security` nur, wenn TLS tatsächlich terminiert wird -> HSTS gehoert **nicht** zu einem selbstsignierten Zertifikat. Der Browser -> merkt sich die Vorgabe monatelang; die Warnung laesst sich danach nicht mehr +> HSTS gehört **nicht** zu einem selbstsignierten Zertifikat. Der Browser +> merkt sich die Vorgabe monatelang; die Warnung lässt sich danach nicht mehr > wegklicken. `install.sh` setzt HSTS deshalb nur zusammen mit Let's Encrypt. --- ## 5. Geheimnisse im Ruhezustand -Ein Schluesselspeicher mit AES-256-GCM (`data/data.keys`, Modus 0600): +Ein Schlüsselspeicher mit AES-256-GCM (`data/data.keys`, Modus 0600): -* Versionierte Schluessel, rotierbar. Chiffrate tragen ihre Schluesselkennung +* Versionierte Schlüssel, rotierbar. Chiffrate tragen ihre Schlüsselkennung (`v1...`) und bleiben nach einer Rotation lesbar. -* Der Verwendungszweck geht als AAD in die Verschluesselung ein. Ein Chiffrat - aus einem Kontext laesst sich in einem anderen nicht entschluesseln. -* Beim Start wird geprueft, ob die Rechte auf Schluessel- und Lizenzdateien +* Der Verwendungszweck geht als AAD in die Verschlüsselung ein. Ein Chiffrat + aus einem Kontext lässt sich in einem anderen nicht entschlüsseln. +* Beim Start wird geprüft, ob die Rechte auf Schlüssel- und Lizenzdateien noch stimmen; Abweichungen erscheinen unter *Diagnose*. -Passwoerter fuer Mounts gehen ueber `stdin` an den privilegierten Helfer, der +Passwörter für Mounts gehen über `stdin` an den privilegierten Helfer, der sie in eine 0600-Datei schreibt -- nie als Kommandozeilenargument (Prozessliste) -und nie ueber die Umgebung (`/proc//environ`). +und nie über die Umgebung (`/proc//environ`). --- ## 6. Rechtetrennung auf dem Host -Der Webprozess laeuft als `tesm` bzw. `tesm-license`. Root-Aktionen gehen durch +Der Webprozess läuft als `tesm` bzw. `tesm-license`. Root-Aktionen gehen durch genau einen allowlisted Helfer (`/usr/local/lib/tesm/tesm-helper`), der per -sudoers ausschliesslich fuer diesen Pfad freigegeben ist. +sudoers ausschliesslich für diesen Pfad freigegeben ist. Regeln, die im Repository durch Tests erzwungen werden: * Jedes Verb, das die Anwendung kennt, existiert im Helfer. * Kein `eval`, kein `bash -c`, kein `sh -c`. -* Jede Pfadpruefung weist `..` ab -- sonst laesst sich eine Positivliste ueber +* Jede Pfadprüfung weist `..` ab -- sonst lässt sich eine Positivliste über den Basisnamen unterlaufen (`/etc/nginx/sites-available/../../../root/x` hat - den zulaessigen Basisnamen `x`). + den zulässigen Basisnamen `x`). * Der Instanzname ist auf `tesm[-]` begrenzt; er landet in Pfaden. * Keine sudoers-Regel ausser der einen. `NOPASSWD: ALL` gibt es nicht. Die systemd-Units haben `NoNewPrivileges=yes`, `ProtectSystem=strict`, `PrivateTmp=yes` und eine enge `RestrictAddressFamilies`-Liste. -Der Vorgaenger liess die komplette Flask-Anwendung als root laufen. Eine -einzige Luecke in irgendeinem Pfad haette den Host bedeutet. +Der Vorgänger liess die komplette Flask-Anwendung als root laufen. Eine +einzige Lücke in irgendeinem Pfad hätte den Host bedeutet. --- -## 7. Aenderungsprotokoll +## 7. Änderungsprotokoll -Verkettet gehasht: jeder Eintrag enthaelt den Hash seines Vorgaengers. Ein -nachtraegliches Aendern oder Loeschen faellt auf und wird unter *Diagnose* +Verkettet gehasht: jeder Eintrag enthält den Hash seines Vorgängers. Ein +nachträgliches Ändern oder Löschen fällt auf und wird unter *Diagnose* angezeigt, mitsamt dem Eintrag, ab dem die Kette bricht. -Protokolliert wird, wer was wann von welcher Adresse getan hat -- ausdruecklich +Protokolliert wird, wer was wann von welcher Adresse getan hat -- ausdrücklich auch Lesezugriffe auf Geheimnisse (`credential.secret_revealed`). -Aeltere Tage wandern in JSONL-Archive und lassen sich als ZIP herunterladen. +Ältere Tage wandern in JSONL-Archive und lassen sich als ZIP herunterladen. --- @@ -145,20 +145,20 @@ Aeltere Tage wandern in JSONL-Archive und lassen sich als ZIP herunterladen. Siehe [ARCHITEKTUR.md](ARCHITEKTUR.md), Abschnitt Lizenzierung. Kurz: getrennte Signaturkontexte, kanonisches JSON, Nonce gegen Wiedereinspielung, -Frischefenster, `seq` gegen Rueckstufung, Antwort an die Anfrage gebunden, +Frischefenster, `seq` gegen Rückstufung, Antwort an die Anfrage gebunden, Aussteller-Pinning. -Das Auslieferungsbundle enthaelt den privaten Clientschluessel und ist damit +Das Auslieferungsbundle enthält den privaten Clientschlüssel und ist damit selbst ein Geheimnis: eigenes Recht (`licenses.export`), frische -Passwortbestaetigung, `Cache-Control: no-store`, Eintrag im Zustell- und im -Aenderungsprotokoll. +Passwortbestätigung, `Cache-Control: no-store`, Eintrag im Zustell- und im +Änderungsprotokoll. --- ## 9. Ein- und Ausfuhr -Verschluesselte Umschlaege (Argon2id-Ableitung + AES-256-GCM) mit einem selbst -gewaehlten Kennwort. Der Import zeigt eine Vorschau, bevor etwas geschrieben +Verschlüsselte Umschläge (Argon2id-Ableitung + AES-256-GCM) mit einem selbst +gewählten Kennwort. Der Import zeigt eine Vorschau, bevor etwas geschrieben wird; die Vorschau liegt in der Datenbank, nicht im Prozessspeicher -- deshalb braucht der Dienst kein `--workers 1` mehr. @@ -166,11 +166,11 @@ braucht der Dienst kein `--workers 1` mehr. ## 10. Bewusst nicht getan -* **Kein Schalter, der die LDAP-Zertifikatspruefung abschaltet.** Wer ihn +* **Kein Schalter, der die LDAP-Zertifikatsprüfung abschaltet.** Wer ihn einmal setzt, setzt ihn dauerhaft. -* **Kein CSRF-Freibrief fuer cookie-autorisierte Routen.** -* **Kein `unsafe-inline` fuer Skripte**, auch nicht "voruebergehend". -* **Keine Rechteausweitung**, auch nicht fuer Administratoren untereinander. +* **Kein CSRF-Freibrief für cookie-autorisierte Routen.** +* **Kein `unsafe-inline` für Skripte**, auch nicht "vorübergehend". +* **Keine Rechteausweitung**, auch nicht für Administratoren untereinander. * **Kein Hostname im Lizenz-Fingerabdruck** -- Umbenennen darf die Bindung nicht brechen. * **Keine von Hand gepflegte nginx-Site.** Sie wird immer neu erzeugt. @@ -179,5 +179,5 @@ braucht der Dienst kein `--workers 1` mehr. ## 11. Wenn Sie eine Schwachstelle finden -Bitte nicht oeffentlich melden. Wenden Sie sich an die im Anbieterprofil des +Bitte nicht öffentlich melden. Wenden Sie sich an die im Anbieterprofil des Lizenzservers hinterlegte Adresse. diff --git a/packages/tesm-core/pyproject.toml b/packages/tesm-core/pyproject.toml index 1be1f58..7849ca5 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.0.0" +version = "2.0.2" 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/__init__.py b/packages/tesm-core/src/tesm_core/__init__.py index ed00273..e1d5516 100644 --- a/packages/tesm-core/src/tesm_core/__init__.py +++ b/packages/tesm-core/src/tesm_core/__init__.py @@ -8,7 +8,7 @@ Design-System. from __future__ import annotations -__version__ = "2.0.0" +__version__ = "2.0.2" from .app_factory import create_base_app from .config import CoreConfig, SecurityConfig, load_core_config diff --git a/packages/tesm-core/src/tesm_core/app_factory.py b/packages/tesm-core/src/tesm_core/app_factory.py index d6feb2b..91fd9ba 100644 --- a/packages/tesm-core/src/tesm_core/app_factory.py +++ b/packages/tesm-core/src/tesm_core/app_factory.py @@ -43,6 +43,11 @@ from .views import hostsettings as hostsettings_views from .views import logs as logs_views from .views import trash as trash_views +#: Rechteinhaber, in dieser Reihenfolge. Steht an einer Stelle, weil die +#: Fusszeile in zwei Vorlagen auftaucht (Anwendungsrahmen und Anmeldeseiten) +#: und zwei Texte fuer dieselbe Aussage auseinanderlaufen. +COPYRIGHT_NOTICE = "Tim Eertmoed · WiS IT-Solutions GmbH" + CORE_SETTINGS: tuple[SettingSpec, ...] = ( SettingSpec("nav_order", [], "json", "Reihenfolge der Navigation", section="intern"), SettingSpec("nav_child_order", {}, "json", "Reihenfolge der Unterpunkte", section="intern"), @@ -56,6 +61,18 @@ CORE_SETTINGS: tuple[SettingSpec, ...] = ( minimum=1, maximum=3650, ), + SettingSpec( + "session_idle_minutes", + 30, + "int", + "Automatische Abmeldung nach Leerlauf (Minuten)", + hint="Jede Anfrage setzt den Zaehler zurueck -- gezaehlt wird nur Untaetigkeit. " + "Gilt auch fuer die eingebundenen Dateifreigaben: mit der Sitzung endet die " + "Einbindung.", + section="system", + minimum=5, + maximum=1440, + ), SettingSpec("timezone", "", "str", "Zeitzone des Hosts", section="system"), SettingSpec("hostname", "", "str", "Hostname", section="system"), SettingSpec( @@ -117,7 +134,7 @@ def create_base_app( #: Seitenhintergrund. Der Lizenzserver setzt sie bewusst nicht: die #: Funktionsliste von TESM waere dort schlicht falsch. auth_tagline: str = "", - app_version: str = "2.0.0", + app_version: str = "2.0.2", built_in_trusted_keys: tuple[str, ...] = (), enable_license: bool = True, template_folder: str | None = None, @@ -192,11 +209,25 @@ def create_base_app( ) app.extensions["tesm"] = extension + def _leerlauf(conn: Any) -> int: + """Leerlaufgrenze aus den Einstellungen -- ueber die offene Verbindung. + + Bewusst mit der Verbindung, die die Sitzungsverwaltung schon haelt: eine + zweite zu oeffnen waere eine verschachtelte Datenbankverbindung, und die + gilt hier als Fehler. + """ + try: + wert = int(settings.get(conn, "session_idle_minutes") or 0) + except (TypeError, ValueError): + wert = 0 + return wert if wert > 0 else config.security.session_idle_minutes + app.session_interface = SqliteSessionInterface( database, cookie_name=app.config["SESSION_COOKIE_NAME"], idle_minutes=config.security.session_idle_minutes, absolute_hours=config.security.session_absolute_hours, + idle_lookup=_leerlauf, ) # -- Schema und Startzustand ------------------------------------------ # @@ -264,6 +295,7 @@ def create_base_app( "auth_tagline": auth_tagline, "app_version": app_version, "current_year": datetime.now(timezone.utc).year, + "copyright_notice": COPYRIGHT_NOTICE, "current_user": current_user, "nav_items": nav_items, "palette_entries": palette, diff --git a/packages/tesm-core/src/tesm_core/auth/directory.py b/packages/tesm-core/src/tesm_core/auth/directory.py index 77fb828..c4766e7 100644 --- a/packages/tesm-core/src/tesm_core/auth/directory.py +++ b/packages/tesm-core/src/tesm_core/auth/directory.py @@ -42,7 +42,7 @@ AAD_LDAP_BIND = "service_accounts.ldap_bind" #: userAccountControl-Bit fuer "Konto deaktiviert". ACCOUNTDISABLE = 0x0002 -#: OID fuer rekursive Gruppenauflösung in AD. +#: OID fuer rekursive Gruppenaufloesung in AD. MATCHING_RULE_IN_CHAIN = "1.2.840.113556.1.4.1941" Status = Literal["ok", "invalid", "unavailable", "disabled"] @@ -349,7 +349,7 @@ def _attr_int(entry: Any, name: str) -> int | None: def _resolve_groups(connection: Any, base: str, user_dn: str) -> tuple[str, ...]: - """Rekursive Gruppenauflösung ueber LDAP_MATCHING_RULE_IN_CHAIN.""" + """Rekursive Gruppenaufloesung ueber LDAP_MATCHING_RULE_IN_CHAIN.""" try: connection.search( search_base=base, @@ -359,7 +359,7 @@ def _resolve_groups(connection: Any, base: str, user_dn: str) -> tuple[str, ...] ) return tuple(str(entry.entry_dn) for entry in connection.entries) except Exception as exc: # noqa: BLE001 - logger.warning("Gruppenauflösung fehlgeschlagen: %s", exc) + logger.warning("Gruppenaufloesung fehlgeschlagen: %s", exc) return () diff --git a/packages/tesm-core/src/tesm_core/config.py b/packages/tesm-core/src/tesm_core/config.py index 0d6fde9..9f7856a 100644 --- a/packages/tesm-core/src/tesm_core/config.py +++ b/packages/tesm-core/src/tesm_core/config.py @@ -45,7 +45,7 @@ class SecurityConfig: cookie_secure: bool = True cookie_samesite: str = "Lax" #: Leerlauf-Zeitfenster einer Sitzung. - session_idle_minutes: int = 60 + session_idle_minutes: int = 30 #: Harte Obergrenze unabhaengig von Aktivitaet. session_absolute_hours: int = 12 #: Wie lange eine erneute Passwortabfrage ("sudo-Modus") gilt. @@ -147,7 +147,7 @@ def load_core_config( security = SecurityConfig( cookie_secure=_env_bool(env_prefix, "COOKIE_SECURE", True), cookie_samesite=_env(env_prefix, "COOKIE_SAMESITE") or "Lax", - session_idle_minutes=_env_int(env_prefix, "SESSION_IDLE_MINUTES", 60), + session_idle_minutes=_env_int(env_prefix, "SESSION_IDLE_MINUTES", 30), session_absolute_hours=_env_int(env_prefix, "SESSION_ABSOLUTE_HOURS", 12), reauth_valid_minutes=_env_int(env_prefix, "REAUTH_VALID_MINUTES", 15), login_max_attempts=_env_int(env_prefix, "LOGIN_MAX_ATTEMPTS", 8), diff --git a/packages/tesm-core/src/tesm_core/licensing_client.py b/packages/tesm-core/src/tesm_core/licensing_client.py index f54de86..1b93b88 100644 --- a/packages/tesm-core/src/tesm_core/licensing_client.py +++ b/packages/tesm-core/src/tesm_core/licensing_client.py @@ -48,6 +48,10 @@ SETTING_STATUS = "license_activation_status" SETTING_PENDING = "license_pending_request" SETTING_LAST_HEARTBEAT = "license_last_heartbeat_at" SETTING_HEARTBEAT_ERROR = "license_heartbeat_error" + +#: Ab wann die Restlaufzeit als *Hinweis* erscheint (gruen). Unterhalb von +#: ``EXPIRY_WARNING_DAYS`` uebernimmt die Warnung (orange), nach Ablauf Rot. +EXPIRY_HINT_DAYS = 90 SETTING_EXPORT_GRACE = "license_export_grace" STATUS_INACTIVE = "inactive" @@ -596,14 +600,22 @@ class LicenseManager: message = "Verbindung zum Lizenzserver bestaetigt." self.settings.set(conn, SETTING_PENDING, "", actor=actor) - audit.log( - conn, - actor=actor, - action=f"license.{action}", - target=str(request_payload["license_id"]), - detail=f"Transport: {transport}", - severity="notice", - ) + # Ein erfolgreicher Heartbeat laeuft alle paar Minuten und ist kein + # Ereignis, sondern der Normalzustand. Im Auditlog stand dadurch + # praktisch nur noch er, und die Eintraege, um die es dort geht -- + # Aktivierung, Deaktivierung, Widerruf --, gingen darin unter. Der + # Zeitpunkt des letzten Heartbeats steht ohnehin in den Einstellungen + # und auf der Lizenzseite. Ein *fehlgeschlagener* Heartbeat wird + # weiterhin protokolliert (siehe die Fehlerpfade oben). + if action != "heartbeat": + audit.log( + conn, + actor=actor, + action=f"license.{action}", + target=str(request_payload["license_id"]), + detail=f"Transport: {transport}", + severity="notice", + ) self.reload(conn) return {"ok": True, "transport": transport, "code": "", "message": message} @@ -704,6 +716,19 @@ class LicenseManager: except LicenseError as exc: with self.database.transaction(conn): self.settings.set(conn, SETTING_HEARTBEAT_ERROR, str(exc), actor="system") + # Der *erfolgreiche* Heartbeat wird nicht protokolliert -- er + # ist der Normalzustand und hat das Auditlog zugeschuettet. + # Ein Fehlschlag dagegen ist genau das, was dort stehen soll: + # er kann bedeuten, dass die Lizenz demnaechst in die + # Kulanzzeit laeuft. + audit.log( + conn, + actor="system", + action="license.heartbeat_failed", + target=self._state.license_id or "", + detail=str(exc)[:500], + severity="warning", + ) return {"ok": False, "message": str(exc)} # ------------------------------------------------------------------ # @@ -761,6 +786,17 @@ class LicenseManager: "dot_class": "status-pill__dot--pulse", "title": "Bitte rechtzeitig verlaengern.", } + # Zwischen der Warnschwelle und 90 Tagen: gruen, rein als Hinweis. Wer + # eine Verlaengerung im Voraus plant, will das Datum sehen, ohne dass es + # nach einem Problem aussieht. + if status.days_left is not None and 0 < status.days_left <= EXPIRY_HINT_DAYS: + return { + "label": f"Lizenz laeuft in {status.days_left} Tagen ab", + "css_class": "", + "dot": "1", + "dot_class": "", + "title": "Nur ein Hinweis -- es ist nichts eingeschraenkt.", + } if state.last_heartbeat_at: try: last = lic.parse_iso(state.last_heartbeat_at) diff --git a/packages/tesm-core/src/tesm_core/security/sessions.py b/packages/tesm-core/src/tesm_core/security/sessions.py index b9a01f1..7c1c111 100644 --- a/packages/tesm-core/src/tesm_core/security/sessions.py +++ b/packages/tesm-core/src/tesm_core/security/sessions.py @@ -88,11 +88,24 @@ class SqliteSessionInterface(SessionInterface): cookie_name: str, idle_minutes: int, absolute_hours: int, + idle_lookup: Any = None, ) -> None: self.database = database self.cookie_name = cookie_name self.idle_minutes = idle_minutes self.absolute_hours = absolute_hours + #: Optionale Nachschlagefunktion fuer die Leerlaufgrenze. Damit ist die + #: Zeit zur Laufzeit einstellbar, ohne den Dienst neu zu starten -- und + #: ohne eine zweite Datenbankverbindung, denn sie bekommt die offene. + self._idle_lookup = idle_lookup + + def _idle(self, conn: Any) -> int: + if self._idle_lookup is None: + return self.idle_minutes + try: + return int(self._idle_lookup(conn)) or self.idle_minutes + except Exception: # pragma: no cover - Einstellung unlesbar + return self.idle_minutes # -- Flask-Schnittstelle ------------------------------------------- # @@ -146,7 +159,7 @@ class SqliteSessionInterface(SessionInterface): with self.database.transaction(conn): conn.execute( "UPDATE user_sessions SET last_seen_at=?, idle_expires_at=? WHERE id=?", - (_iso(now), _iso(now + timedelta(minutes=self.idle_minutes)), sid), + (_iso(now), _iso(now + timedelta(minutes=self._idle(conn))), sid), ) return session @@ -190,7 +203,7 @@ class SqliteSessionInterface(SessionInterface): session.get("user_id"), _iso(now), _iso(now), - _iso(now + timedelta(minutes=self.idle_minutes)), + _iso(now + timedelta(minutes=self._idle(conn))), _iso(now + timedelta(hours=self.absolute_hours)), session.get("_ip", ""), session.get("_ua", ""), 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 b79c77d..da61bc6 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 @@ -408,6 +408,15 @@ background: var(--danger-soft); color: var(--danger); } + /* Gefuellt statt getoent: "kritisch" muss sich von "Warnung" unterscheiden, + und beide sind rot. */ + .pill--critical { + background: var(--danger); + color: var(--surface-0); + font-weight: 700; + } + .pill--critical::before { background: currentColor; } + .pill--warning, .pill--unknown, .pill--orange { background: var(--warning-soft); color: var(--warning); @@ -436,6 +445,22 @@ background: var(--success); flex-shrink: 0; } + /* Der Knopf sitzt *in* der Pille: kein Rahmen, keine Flaeche, nur das + Symbol in der Farbe der Pille. Ein Standardknopf wuerde sie sprengen. */ + .status-pill__action { display: inline-flex; } + .status-pill__action button { + display: inline-flex; + align-items: center; + padding: 0; + margin-left: 2px; + border: 0; + background: none; + color: inherit; + opacity: 0.65; + cursor: pointer; + } + .status-pill__action button:hover { opacity: 1; } + .status-pill__dot--pulse { animation: pulse-ring 2.4s var(--ease) infinite; } .status-pill--danger { border-color: color-mix(in srgb, var(--danger) 45%, var(--border)); color: var(--danger); } .status-pill--danger .status-pill__dot { background: var(--danger); } @@ -655,6 +680,37 @@ } .modal__title { font-size: var(--fs-md); font-weight: 640; flex: 1; } .modal__sub { font-size: var(--fs-xs); color: var(--text-faint); margin-top: 2px; } + /* Eine Datei-Vorschau soll die ganze Flaeche des Dialogs nutzen; der + uebliche Innenabstand des Dialogkoerpers wuerde sie einrahmen. */ + .modal__body--flush { padding: 0; max-height: none; } + .modal__caption { + padding: 0 var(--sp-5) var(--sp-2); + color: var(--text-muted); + font-family: var(--font-mono); + font-size: var(--fs-xs); + word-break: break-all; + } + .file-preview { + display: block; + width: 100%; + height: min(70dvh, 760px); + border: 0; + background: var(--surface-0); + } + /* Der Dateiname ist ein Knopf (er oeffnet die Vorschau), soll aber wie ein + Link aussehen -- ein Knopf mit Rahmen mitten in einer Tabellenzeile waere + unruhig. */ + .file-name { + background: none; + border: 0; + padding: 0; + color: var(--accent); + font: inherit; + text-align: left; + cursor: pointer; + } + .file-name:hover { text-decoration: underline; } + .modal__body { padding: var(--sp-5); overflow-y: auto; @@ -1035,6 +1091,16 @@ .warning-text { color: var(--warning); } .accent-text { color: var(--accent); } .mono { font-family: var(--font-mono); font-size: 0.94em; } + /* Ein Inline-SVG ist im Reset display:block (base.css). In einem Knopf + stoert das nicht -- .btn ist inline-flex. In einem einfachen Link reisst + es die Beschriftung in die naechste Zeile. Fuer solche Links deshalb + ausdruecklich eine Zeile. */ + .link-icon { + display: inline-flex; + align-items: center; + gap: 6px; + } + .nowrap { white-space: nowrap; } .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; } .break-all { overflow-wrap: anywhere; } diff --git a/packages/tesm-core/src/tesm_core/static/tesm_core/js/app.js b/packages/tesm-core/src/tesm_core/static/tesm_core/js/app.js index d329862..ca478bf 100644 --- a/packages/tesm-core/src/tesm_core/static/tesm_core/js/app.js +++ b/packages/tesm-core/src/tesm_core/static/tesm_core/js/app.js @@ -28,7 +28,7 @@ function store(key, value) { if (value === null) localStorage.removeItem(key); else localStorage.setItem(key, value); } catch { - /* Privatmodus o. Ä. -- Einstellungen sind dann nur fluechtig. */ + /* Privatmodus o. Ae. -- Einstellungen sind dann nur fluechtig. */ } } @@ -212,13 +212,21 @@ function initDialogs() { event.preventDefault(); const dialog = document.getElementById(opener.dataset.dialogOpen); if (dialog) { - // Felder aus data-fill-* des Auslösers in das Formular uebernehmen. + // Felder aus data-fill-* des Ausloesers in das Formular uebernehmen. Object.entries(opener.dataset).forEach(([key, value]) => { if (!key.startsWith("fill")) return; const name = key.slice(4); const target = dialog.querySelector(`[data-fill-target="${name}"]`); if (!target) return; - if ("value" in target) target.value = value; + // Wohin der Wert gehoert, haengt am Element: ein Rahmen will die + // Quelle, ein Link das Ziel, ein Feld einen Wert. Ohne diese + // Unterscheidung landete die URL als *Beschriftung* im + // Herunterladen-Knopf -- und weil das href dabei "#" blieb, lud ein + // Klick die Seite selbst als "Herunterladen.htm" herunter. + const als = target.dataset.fillAs; + if (als === "src") target.src = value; + else if (als === "href") target.href = value; + else if ("value" in target) target.value = value; else target.textContent = value; }); openDialog(dialog); diff --git a/packages/tesm-core/src/tesm_core/templates/tesm_core/_group_picker.html b/packages/tesm-core/src/tesm_core/templates/tesm_core/_group_picker.html index d396fe1..1ceb947 100644 --- a/packages/tesm-core/src/tesm_core/templates/tesm_core/_group_picker.html +++ b/packages/tesm-core/src/tesm_core/templates/tesm_core/_group_picker.html @@ -1,15 +1,15 @@ -{# Auswahlfeld fuer eine Verzeichnisgruppe. +{# Auswahlfeld für eine Verzeichnisgruppe. - Der DN einer AD-Gruppe ist nichts, was man abtippt: er ist lang, enthaelt - Kommas und Leerzeichen und ein Tippfehler faellt erst bei der naechsten + Der DN einer AD-Gruppe ist nichts, was man abtippt: er ist lang, enthält + Kommas und Leerzeichen und ein Tippfehler fällt erst bei der nächsten Anmeldung auf -- als stille Nicht-Zuordnung. Deshalb gesucht statt getippt. Das Suchergebnis kommt aus dem Verzeichnis selbst; die Felder werden dabei - beide gefuellt (Anzeigename und DN). Getippt werden darf trotzdem: bei einer - Umgebung ohne Dienstkonto ist die Suche nicht moeglich, und dann ist ein - eingefuegter DN besser als gar keine Zuordnung. + beide gefüllt (Anzeigename und DN). Getippt werden darf trotzdem: bei einer + Umgebung ohne Dienstkonto ist die Suche nicht möglich, und dann ist ein + eingefügter DN besser als gar keine Zuordnung. - Erwartet: name_field, dn_field (Feldnamen), scope (eindeutiges Kuerzel), + Erwartet: name_field, dn_field (Feldnamen), scope (eindeutiges Kürzel), optional name_value / dn_value und required. #} {% macro group_picker(name_field, dn_field, scope, name_value='', dn_value='', diff --git a/packages/tesm-core/src/tesm_core/templates/tesm_core/account.html b/packages/tesm-core/src/tesm_core/templates/tesm_core/account.html index c330720..b898060 100644 --- a/packages/tesm-core/src/tesm_core/templates/tesm_core/account.html +++ b/packages/tesm-core/src/tesm_core/templates/tesm_core/account.html @@ -20,7 +20,7 @@
{% if current_user.is_directory_account %} {{ alert('Ihre Stammdaten kommen aus dem Verzeichnisdienst und werden bei jeder - Anmeldung uebernommen.', 'info') }} + Anmeldung übernommen.', 'info') }} {% endif %}
{{ field('first_name', 'Vorname', value=profile.first_name, @@ -48,7 +48,7 @@
{% if current_user.is_admin %} - {{ alert('Sie sind Administrator und duerfen unabhaengig von Gruppen alles.', 'warning') }} + {{ alert('Sie sind Administrator und dürfen unabhängig von Gruppen alles.', 'warning') }} {% elif permissions %}
{% for permission in permissions %} diff --git a/packages/tesm-core/src/tesm_core/templates/tesm_core/account_security.html b/packages/tesm-core/src/tesm_core/templates/tesm_core/account_security.html index 74fbc0e..3992050 100644 --- a/packages/tesm-core/src/tesm_core/templates/tesm_core/account_security.html +++ b/packages/tesm-core/src/tesm_core/templates/tesm_core/account_security.html @@ -35,26 +35,26 @@
-
{{ icon('lock', size=17) }} Passwort aendern
+
{{ icon('lock', size=17) }} Passwort ändern
{% if current_user.is_directory_account %}
- {{ alert('Ihr Passwort wird im Verzeichnisdienst gepflegt und dort geaendert.', 'info') }} + {{ alert('Ihr Passwort wird im Verzeichnisdienst gepflegt und dort geändert.', 'info') }}
{% else %}
- {{ field('current_password', 'Aktuelles Passwort', type='password', required=True, + {{ field('current_password', 'Aktülles Passwort', type='password', required=True, autocomplete='current-password') }} {{ field('new_password', 'Neues Passwort', type='password', required=True, autocomplete='new-password', hint_text='Mindestens ' ~ policy.min_length ~ ' Zeichen und zwei verschiedene Zeichenarten.') }} {{ field('repeat_password', 'Neues Passwort wiederholen', type='password', required=True, autocomplete='new-password') }} - {{ alert('Nach der Aenderung werden alle anderen Sitzungen beendet.', 'info') }} + {{ alert('Nach der Änderung werden alle anderen Sitzungen beendet.', 'info') }}
- +
{% endif %}
@@ -70,7 +70,7 @@
{% if pending_secret %} - {{ alert('Richten Sie den Eintrag in Ihrer App ein und bestaetigen Sie mit einem aktuellen Code. Erst dann wird die Anmeldung umgestellt.', 'warning') }} + {{ alert('Richten Sie den Eintrag in Ihrer App ein und bestätigen Sie mit einem aktüllen Code. Erst dann wird die Anmeldung umgestellt.', 'warning') }}
Geheimnis
{{ pending_secret_grouped }} @@ -87,7 +87,7 @@ - + {% elif totp_enabled %}
@@ -100,7 +100,7 @@
@@ -114,10 +114,10 @@
{% else %} {% if require_totp_for_admins and current_user.is_admin %} - {{ alert('In dieser Installation ist ein zweiter Faktor fuer Administratoren verpflichtend. Bitte jetzt einrichten.', 'danger') }} + {{ alert('In dieser Installation ist ein zweiter Faktor für Administratoren verpflichtend. Bitte jetzt einrichten.', 'danger') }} {% endif %}

- Ein zweiter Faktor schuetzt Ihr Konto auch dann, wenn Ihr Passwort bekannt wird. + Ein zweiter Faktor schützt Ihr Konto auch dann, wenn Ihr Passwort bekannt wird.

diff --git a/packages/tesm-core/src/tesm_core/templates/tesm_core/base.html b/packages/tesm-core/src/tesm_core/templates/tesm_core/base.html index 9b98231..f1fd151 100644 --- a/packages/tesm-core/src/tesm_core/templates/tesm_core/base.html +++ b/packages/tesm-core/src/tesm_core/templates/tesm_core/base.html @@ -1,6 +1,6 @@ {# ========================================================================== - Grundgeruest aller angemeldeten Seiten. - Bloecke: extra_head, page_title, page_sub, breadcrumb, topbar_extra, + Grundgerüst aller angemeldeten Seiten. + Blöcke: extra_head, page_title, page_sub, breadcrumb, topbar_extra, content, dialogs, scripts ========================================================================== #} @@ -23,9 +23,9 @@
Datei vorhanden
{{ license_state.present | yes_no }}
-
Signatur gueltig
{{ license_state.valid | yes_no }}
+
Signatur gültig
{{ license_state.valid | yes_no }}
Aussteller gepinnt
{{ license_state.issuer_pinned | yes_no }}
Rolle
{{ license_state.role }}
Module
{{ license_state.modules | sort | join(', ') or '–' | safe }}
diff --git a/packages/tesm-core/src/tesm_core/templates/tesm_core/error.html b/packages/tesm-core/src/tesm_core/templates/tesm_core/error.html index ebca1bb..1d0b9f7 100644 --- a/packages/tesm-core/src/tesm_core/templates/tesm_core/error.html +++ b/packages/tesm-core/src/tesm_core/templates/tesm_core/error.html @@ -7,11 +7,11 @@
{{ description }}
{% if request_id %} -

Vorgangsnummer fuer die Fehlersuche: {{ request_id }}

+

Vorgangsnummer für die Fehlersuche: {{ request_id }}

{% endif %}
{% endblock %} diff --git a/packages/tesm-core/src/tesm_core/templates/tesm_core/group_detail.html b/packages/tesm-core/src/tesm_core/templates/tesm_core/group_detail.html index aacb4d3..2f1e572 100644 --- a/packages/tesm-core/src/tesm_core/templates/tesm_core/group_detail.html +++ b/packages/tesm-core/src/tesm_core/templates/tesm_core/group_detail.html @@ -20,8 +20,8 @@ {{ icon('lock', size=18) }}
Systemgruppe
- Name und Rechte sind gesperrt. Anders als im Vorgaenger werden die Rechte - nicht bei jedem Neustart auf die Vorlage zurueckgesetzt – + Name und Rechte sind gesperrt. Anders als im Vorgänger werden die Rechte + nicht bei jedem Neustart auf die Vorlage zurückgesetzt – Anpassungen bleiben erhalten. {% if current_user.has('groups.edit') %} @@ -49,8 +49,8 @@ {% if group.preset_key and current_user.is_admin %} {% endif %} @@ -58,7 +58,7 @@
{{ field('name', 'Name', value=group.name, required=True, disabled=group.is_system, - hint_text='Systemgruppen koennen nicht umbenannt werden.' if group.is_system else '') }} + hint_text='Systemgruppen können nicht umbenannt werden.' if group.is_system else '') }} {{ textarea_field('description', 'Beschreibung', value=group.description, rows=2, extra_class='field--full') }}
diff --git a/packages/tesm-core/src/tesm_core/templates/tesm_core/groups.html b/packages/tesm-core/src/tesm_core/templates/tesm_core/groups.html index e4204d9..0945367 100644 --- a/packages/tesm-core/src/tesm_core/templates/tesm_core/groups.html +++ b/packages/tesm-core/src/tesm_core/templates/tesm_core/groups.html @@ -2,7 +2,7 @@ {% from "tesm_core/macros.html" import field, textarea_field, pill, modal, alert, permission_matrix, empty_state %} {% block page_title %}Gruppen und Rechte{% endblock %} -{% block page_sub %}{{ groups | length }} Gruppen · Rechte addieren sich ueber alle Gruppen eines Benutzers{% endblock %} +{% block page_sub %}{{ groups | length }} Gruppen · Rechte addieren sich über alle Gruppen eines Benutzers{% endblock %} {% block topbar_extra %} {% if current_user.has('groups.create') %} @@ -13,9 +13,9 @@ {% endblock %} {% block content %} -{{ alert('Wer selbst ein Recht nicht besitzt, kann es auch nicht vergeben. Diese Regel gilt fuer -alle ausser echten Administratoren und verhindert, dass sich ein Gruppenverwalter ueber eine -neue Gruppe selbst hoeherstuft.', 'info', 'Eskalationsschutz') }} +{{ alert('Wer selbst ein Recht nicht besitzt, kann es auch nicht vergeben. Diese Regel gilt für +alle ausser echten Administratoren und verhindert, dass sich ein Gruppenverwalter über eine +neue Gruppe selbst höherstuft.', 'info', 'Eskalationsschutz') }}
{% for group in groups %} @@ -37,9 +37,9 @@ neue Gruppe selbst hoeherstuft.', 'info', 'Eskalationsschutz') }}
+ data-confirm="Gruppe {{ group.name }} löschen? Sie landet im Papierkorb; Mitglieder verlieren die zugehörigen Rechte." + data-confirm-title="Gruppe löschen" data-confirm-ok="Löschen" + aria-label="Löschen">{{ icon('trash', size=15) }}
{% endif %}
@@ -67,7 +67,7 @@ neue Gruppe selbst hoeherstuft.', 'info', 'Eskalationsschutz') }} {% block dialogs %} {% if current_user.has('groups.create') %} - {% call modal('group-new', 'Gruppe anlegen', 'Rechte koennen anschliessend feiner eingestellt werden', 'wide') %} + {% call modal('group-new', 'Gruppe anlegen', 'Rechte können anschliessend feiner eingestellt werden', 'wide') %}