diff --git a/apps/tesm-license/pyproject.toml b/apps/tesm-license/pyproject.toml index 6b68629..93b2ed1 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.7.7" +version = "2.8.0" 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 8ade82f..3dd4cd3 100644 --- a/apps/tesm-license/src/tesm_license/__init__.py +++ b/apps/tesm-license/src/tesm_license/__init__.py @@ -180,7 +180,7 @@ def _reencrypt_secrets(conn: Any, keystore: Any) -> int: def _status_pills(user: Any) -> list[dict[str, str]]: - if not user.is_authenticated or not user.has_any(("licenses.view",)): + if not user.is_authenticated or not user.has_any(("licenses.read",)): return [] from tesm_core.extension import db diff --git a/apps/tesm-license/src/tesm_license/blueprints/backup.py b/apps/tesm-license/src/tesm_license/blueprints/backup.py index 045b844..083b8b4 100644 --- a/apps/tesm-license/src/tesm_license/blueprints/backup.py +++ b/apps/tesm-license/src/tesm_license/blueprints/backup.py @@ -24,14 +24,14 @@ def _allowed_categories() -> list[transfer.Category]: for category in transfer.CATEGORIES: if category.admin_only and not current_user.is_admin: continue - if category.sensitive and not current_user.has("backup.secrets"): + if category.sensitive and not current_user.has("backup_export.secrets"): continue allowed.append(category) return allowed @bp.get("/") -@permission_required("backup.view", "backup.export", "backup.execute", mode="any") +@permission_required("backup_export.read", "backup_import.read", mode="any") def overview(): conn = db() preview = None @@ -50,13 +50,13 @@ def overview(): preview=preview, min_passphrase=transfer.MIN_PASSPHRASE_LENGTH, preview_ttl=transfer.PREVIEW_TTL_MINUTES, - can_secrets=current_user.has("backup.secrets"), + can_secrets=current_user.has("backup_export.secrets"), signing_key_path=str(core().registry["master_key_path"]), ) @bp.post("/export") -@permission_required("backup.export") +@permission_required("backup_export.export") # Die frische Bestaetigung verlangt das Recht selbst (SENSITIVE_ACTIONS). def export(): conn = db() @@ -95,7 +95,7 @@ def export(): @bp.post("/import/preview") -@permission_required("backup.execute") +@permission_required("backup_import.execute") @reauth_required def import_preview(): conn = db() @@ -132,7 +132,7 @@ def import_preview(): @bp.post("/import/apply") -@permission_required("backup.execute") +@permission_required("backup_import.execute") @reauth_required def import_apply(): extension = core() @@ -191,7 +191,7 @@ def import_apply(): @bp.post("/import/discard") -@permission_required("backup.execute") +@permission_required("backup_import.execute") def import_discard(): token = session.pop(PREVIEW_SESSION_KEY, "") if token: diff --git a/apps/tesm-license/src/tesm_license/blueprints/customers.py b/apps/tesm-license/src/tesm_license/blueprints/customers.py index 902affc..89a6c31 100644 --- a/apps/tesm-license/src/tesm_license/blueprints/customers.py +++ b/apps/tesm-license/src/tesm_license/blueprints/customers.py @@ -29,13 +29,13 @@ def _form() -> dict[str, str]: @bp.get("/") -@permission_required("customers.view") +@permission_required("customers.read") def overview(): return render_template("license/customers.html", customers=issuance.list_customers(db())) @bp.get("/") -@permission_required("customers.view") +@permission_required("customers.read") def detail(customer_id: int): conn = db() customer = issuance.get_customer(conn, customer_id) @@ -48,7 +48,7 @@ def detail(customer_id: int): ] reported = ( reporting.reported_for_customer(conn, customer_id) - if current_user.has("reports.view") + if current_user.has("reports.read") else {"customers": [], "source_license_id": ""} ) return render_template( @@ -83,7 +83,7 @@ def create(): @bp.post("/") -@permission_required("customers.edit") +@permission_required("customers.write") def update(customer_id: int): try: with transaction() as conn: diff --git a/apps/tesm-license/src/tesm_license/blueprints/dashboard.py b/apps/tesm-license/src/tesm_license/blueprints/dashboard.py index 81599fc..872d750 100644 --- a/apps/tesm-license/src/tesm_license/blueprints/dashboard.py +++ b/apps/tesm-license/src/tesm_license/blueprints/dashboard.py @@ -21,7 +21,7 @@ bp = Blueprint("dashboard", __name__) def index(): conn = db() extension = core() - if not current_user.has("licenses.view"): + if not current_user.has("licenses.read"): # Leere Huelle statt 403: die Startseite soll fuer jeden erreichbar # bleiben, auch fuer ein Konto ohne Lizenzrechte. return render_template("license/dashboard.html", summary=None, sources=[]) @@ -30,7 +30,7 @@ def index(): return render_template( "license/dashboard.html", summary=summary, - sources=reporting.reporting_sources(conn) if current_user.has("reports.view") else [], + sources=reporting.reporting_sources(conn) if current_user.has("reports.read") else [], own_role=(extension.license.state.role if extension.license else "master"), type_labels=dict(), ) diff --git a/apps/tesm-license/src/tesm_license/blueprints/licenses.py b/apps/tesm-license/src/tesm_license/blueprints/licenses.py index dec8850..d81fad3 100644 --- a/apps/tesm-license/src/tesm_license/blueprints/licenses.py +++ b/apps/tesm-license/src/tesm_license/blueprints/licenses.py @@ -42,7 +42,7 @@ bp = Blueprint("licenses", __name__, url_prefix="/licenses") @bp.get("/") -@permission_required("licenses.view") +@permission_required("licenses.read") def overview(): conn = db() rows = [ @@ -65,7 +65,7 @@ def overview(): @bp.get("/") -@permission_required("licenses.view") +@permission_required("licenses.read") def detail(license_id: str): conn = db() row = issuance.get_license(conn, license_id) @@ -100,7 +100,7 @@ def detail(license_id: str): @bp.post("/from-ticket/") -@permission_required("licenses.create") +@permission_required("license_issue.execute") def issue(ticket_id: str): conn = db() as_trial = request.form.get("as_trial") == "1" @@ -182,7 +182,7 @@ def download(license_id: str): @bp.post("//resign") -@permission_required("licenses.edit") +@permission_required("license_renew.execute") @reauth_required def resign(license_id: str): """Signiert eine neue Revision -- z. B. nach einer Modulerweiterung. @@ -241,7 +241,7 @@ def resign(license_id: str): @bp.post("//revoke") -@permission_required("licenses.edit") +@permission_required("license_revoke.execute") def revoke(license_id: str): conn = db() try: @@ -369,7 +369,7 @@ def send_mail(license_id: str): @bp.route("/offline-code", methods=["GET", "POST"]) -@permission_required("licenses.execute") +@permission_required("license_activation.execute") def manual_code(): """Verarbeitet einen Offline-Code des Kunden. @@ -429,7 +429,7 @@ def manual_code(): @bp.get("/sub-servers") -@permission_required("reports.view") +@permission_required("reports.read") def reports(): conn = db() sources = reporting.reporting_sources(conn) @@ -441,7 +441,7 @@ def reports(): @bp.get("/sub-servers/") -@permission_required("reports.view") +@permission_required("reports.read") def report_detail(license_id: str): conn = db() row = issuance.get_license(conn, license_id) diff --git a/apps/tesm-license/src/tesm_license/blueprints/own_license.py b/apps/tesm-license/src/tesm_license/blueprints/own_license.py index 461253e..30a866c 100644 --- a/apps/tesm-license/src/tesm_license/blueprints/own_license.py +++ b/apps/tesm-license/src/tesm_license/blueprints/own_license.py @@ -42,7 +42,7 @@ MAX_UPLOAD_BYTES = 256 * 1024 @bp.get("/") -@permission_required("settings_license.view") +@permission_required("settings_license.read") def overview(): extension = core() conn = db() @@ -69,7 +69,7 @@ def overview(): @bp.post("/install") -@permission_required("settings_license.edit") +@permission_required("settings_license.write") def upload(): extension = core() manager = extension.license @@ -130,26 +130,26 @@ def _handshake(action: str): @bp.post("/activate") -@permission_required("settings_license.edit") +@permission_required("settings_license.write") def activate(): return _handshake("activate") @bp.post("/deactivate") -@permission_required("settings_license.edit") +@permission_required("settings_license.write") @reauth_required def deactivate(): return _handshake("deactivate") @bp.post("/heartbeat") -@permission_required("settings_license.edit") +@permission_required("settings_license.write") def heartbeat(): return _handshake("heartbeat") @bp.post("/offline-response") -@permission_required("settings_license.edit") +@permission_required("settings_license.write") def offline_response(): manager = core().license if manager is None: @@ -167,7 +167,7 @@ def offline_response(): @bp.get("/signing-key") -@permission_required("settings_license.view") +@permission_required("settings_license.read") def signing_key_info(): """Zeigt den oeffentlichen Signaturschluessel -- zum Pinnen beim Kunden. diff --git a/apps/tesm-license/src/tesm_license/blueprints/settings.py b/apps/tesm-license/src/tesm_license/blueprints/settings.py index 1f2f169..274cd58 100644 --- a/apps/tesm-license/src/tesm_license/blueprints/settings.py +++ b/apps/tesm-license/src/tesm_license/blueprints/settings.py @@ -35,7 +35,7 @@ LOGO_MAGIC = { @bp.get("/system") -@permission_required("settings_system.view") +@permission_required("settings_system.read") def system(): extension = core() conn = db() @@ -85,7 +85,7 @@ def _timezone_choices() -> list[str]: @bp.post("/system") -@permission_required("settings_system.edit") +@permission_required("settings_system.write") def system_save(): extension = core() conn = db() @@ -179,7 +179,7 @@ def set_timezone(): @bp.get("/vendor") -@permission_required("settings_vendor.view") +@permission_required("settings_vendor.read") def vendor(): extension = core() conn = db() @@ -201,7 +201,7 @@ def vendor(): @bp.post("/vendor") -@permission_required("settings_vendor.edit") +@permission_required("settings_vendor.write") def vendor_save(): extension = core() conn = db() @@ -251,7 +251,7 @@ def vendor_save(): @bp.post("/vendor/remove-logo") -@permission_required("settings_vendor.edit") +@permission_required("settings_vendor.write") def vendor_logo_remove(): with transaction() as conn: core().settings.set(conn, "vendor_logo_base64", "", actor=current_user.username) @@ -265,7 +265,7 @@ def vendor_logo_remove(): @bp.get("/email") -@permission_required("settings_mail.view") +@permission_required("settings_mail.read") def mail_settings(): extension = core() conn = db() @@ -280,7 +280,7 @@ def mail_settings(): @bp.post("/email") -@permission_required("settings_mail.edit") +@permission_required("settings_mail.write") def mail_save(): extension = core() conn = db() diff --git a/apps/tesm-license/src/tesm_license/blueprints/tickets.py b/apps/tesm-license/src/tesm_license/blueprints/tickets.py index ef23f17..f001da1 100644 --- a/apps/tesm-license/src/tesm_license/blueprints/tickets.py +++ b/apps/tesm-license/src/tesm_license/blueprints/tickets.py @@ -25,7 +25,7 @@ bp = Blueprint("tickets", __name__, url_prefix="/tickets") @bp.get("/") -@permission_required("tickets.view") +@permission_required("tickets.read") def overview(): conn = db() tickets = issuance.list_tickets(conn) @@ -113,7 +113,7 @@ def _endpoint_missing() -> str: @bp.get("/") -@permission_required("tickets.view") +@permission_required("tickets.read") def detail(ticket_id: str): conn = db() ticket = issuance.get_ticket(conn, ticket_id) @@ -150,7 +150,7 @@ def detail(ticket_id: str): @bp.post("/") -@permission_required("tickets.edit") +@permission_required("tickets.write") def update(ticket_id: str): conn = db() if issuance.get_ticket(conn, ticket_id) is None: @@ -187,7 +187,7 @@ def update(ticket_id: str): @bp.post("//lock") -@permission_required("tickets.edit") +@permission_required("tickets.write") def toggle_block(ticket_id: str): conn = db() ticket = issuance.get_ticket(conn, ticket_id) diff --git a/apps/tesm-license/src/tesm_license/navigation.py b/apps/tesm-license/src/tesm_license/navigation.py index 5c26a9c..dfe0a9b 100644 --- a/apps/tesm-license/src/tesm_license/navigation.py +++ b/apps/tesm-license/src/tesm_license/navigation.py @@ -34,35 +34,35 @@ NAVIGATION = NavTree( label="Kunden", icon="building", endpoint="customers.overview", - permissions=("customers.view",), + permissions=("customers.read",), ), NavItem( key="tickets", label="Tickets", icon="ticket", endpoint="tickets.overview", - permissions=("tickets.view",), + permissions=("tickets.read",), ), NavItem( key="licenses", label="Lizenzen", icon="shield", endpoint="licenses.overview", - permissions=("licenses.view",), + permissions=("licenses.read",), ), NavItem( key="offline_code", label="Offline-Aktivierung", icon="transfer", endpoint="licenses.manual_code", - permissions=("licenses.execute",), + permissions=("license_activation.execute",), ), NavItem( key="reports", label="Sub-Lizenzserver", icon="share", endpoint="licenses.reports", - permissions=("reports.view",), + permissions=("reports.read",), condition=_is_master, ), NavItem( @@ -75,21 +75,21 @@ NAVIGATION = NavTree( label="Laufend", icon="terminal", endpoint="logs.live", - permissions=("logs_live.view",), + permissions=("logs_live.read",), ), NavItem( key="logs_history", label="Verlauf", icon="clock", endpoint="logs.history", - permissions=("logs_history.view", "logs_history.export"), + permissions=("logs_history.read", "logs_history.export"), ), NavItem( key="logs_audit", label="Audit", icon="history", endpoint="logs.audit_view", - permissions=("logs_audit.view", "logs_audit.export"), + permissions=("logs_audit.read", "logs_audit.export"), ), ), ), @@ -103,84 +103,84 @@ NAVIGATION = NavTree( label="Benutzer", icon="users", endpoint="admin.users", - permissions=("users.view",), + permissions=("users.read",), ), NavItem( key="groups", label="Gruppen und Rechte", icon="shield-users", endpoint="admin.groups", - permissions=("groups.view",), + permissions=("groups.read",), ), NavItem( key="settings_vendor", label="Anbieterdaten", icon="building", endpoint="settings.vendor", - permissions=("settings_vendor.view",), + permissions=("settings_vendor.read",), ), NavItem( key="settings_mail", label="E-Mail-Versand", icon="mail", endpoint="settings.mail_settings", - permissions=("settings_mail.view",), + permissions=("settings_mail.read",), ), NavItem( key="settings_system", label="System", icon="sliders", endpoint="settings.system", - permissions=("settings_system.view",), + permissions=("settings_system.read",), ), NavItem( key="settings_network", label="Netzwerk", icon="network", endpoint="hostsettings.network", - permissions=("settings_network.view",), + permissions=("settings_network.read",), ), NavItem( key="settings_web", label="Webserver und TLS", icon="server", endpoint="hostsettings.web", - permissions=("settings_web.view",), + permissions=("settings_web.read",), ), NavItem( key="settings_directory", label="Verzeichnisdienst", icon="users", endpoint="hostsettings.directory_settings", - permissions=("settings_directory.view",), + permissions=("settings_directory.read",), ), NavItem( key="settings_license", label="Eigene Lizenz", icon="key", endpoint="own_license.overview", - permissions=("settings_license.view",), + permissions=("settings_license.read",), ), NavItem( key="backup", label="Sicherung", icon="transfer", endpoint="backup.overview", - permissions=("backup.view", "backup.export", "backup.execute"), + permissions=("backup_export.read", "backup_import.read"), ), NavItem( key="trash", label="Papierkorb", icon="trash", endpoint="trash.overview", - permissions=("trash.view",), + permissions=("trash.read",), ), NavItem( key="diagnostics", label="Diagnose", icon="info", endpoint="diagnostics.overview", - permissions=("diagnostics.view",), + permissions=("diagnostics.read",), ), ), ), diff --git a/apps/tesm-license/src/tesm_license/permissions.py b/apps/tesm-license/src/tesm_license/permissions.py index c774221..81bab0d 100644 --- a/apps/tesm-license/src/tesm_license/permissions.py +++ b/apps/tesm-license/src/tesm_license/permissions.py @@ -28,7 +28,7 @@ A = Action CUSTOMERS = Resource( key="customers", label="Kunden", - actions=(A.VIEW, A.CREATE, A.EDIT, A.DELETE), + actions=(A.READ, A.CREATE, A.WRITE, A.DELETE), hint="Stammdaten der Kundeninstanzen", action_hints={A.DELETE: "Nur möglich, solange kein Ticket am Kunden hängt."}, ) @@ -36,32 +36,71 @@ CUSTOMERS = Resource( TICKETS = Resource( key="tickets", label="Tickets", - actions=(A.VIEW, A.CREATE, A.EDIT, A.DELETE), + actions=(A.READ, A.CREATE, A.WRITE, A.DELETE), hint="Dauerhafte Ansprüche auf eine Lizenz", action_hints={ - A.EDIT: "Parameter ändern und Ticket sperren/entsperren. Wirkt erst auf die " + A.WRITE: "Parameter ändern und Ticket sperren/entsperren. Wirkt erst auf die " "nächste Ausstellung – bereits ausgestellte Lizenzen bleiben unverändert.", A.DELETE: "Nur möglich, wenn keine offene Lizenz mehr existiert.", }, ) +#: Aus einer Zeile wurden fuenf. "Aendern" hiess hier **widerrufen oder neu +#: signieren** -- zwei Eingriffe mit entgegengesetzter Wirkung unter einem +#: Haken: der eine verlaengert einem Kunden die Lizenz, der andere nimmt sie +#: ihm. Wer eine Laufzeit verlaengern durfte, konnte damit jede Lizenz +#: abschalten. Genau das soll die Matrix trennen koennen. LICENSES = Resource( key="licenses", label="Lizenzen", - actions=(A.VIEW, A.CREATE, A.EDIT, A.DELETE, A.EXECUTE, A.EXPORT), - hint="Konkrete, signierte Lizenzdateien", + actions=(A.READ, A.DELETE, A.EXPORT), + hint="Ausgestellte Lizenzen: Laufzeit, Zustand, Datei", action_hints={ - A.CREATE: "Aus einem Ticket eine Lizenz ausstellen.", - A.EDIT: "Lizenz widerrufen oder eine neue Revision signieren.", - A.EXECUTE: "Offline-Aktivierungscodes verarbeiten.", + A.DELETE: "Eintrag entfernen (nur ohne offene Aktivierung).", A.EXPORT: "Lizenzdatei herunterladen oder per E-Mail versenden.", }, ) +LICENSE_ISSUE = Resource( + key="license_issue", + parent="licenses", + label="Lizenz ausstellen", + actions=(A.READ, A.EXECUTE), + hint="Aus einem Ticket eine neue Lizenz signieren", + action_hints={A.EXECUTE: "Ausstellen und signieren."}, +) + +LICENSE_RENEW = Resource( + key="license_renew", + parent="licenses", + label="Lizenz erneuern", + actions=(A.READ, A.EXECUTE), + hint="Neue Revision einer bestehenden Lizenz signieren", + action_hints={A.EXECUTE: "Erneuern – die alte Revision verliert ihre Gültigkeit."}, +) + +LICENSE_REVOKE = Resource( + key="license_revoke", + parent="licenses", + label="Lizenz widerrufen", + actions=(A.READ, A.EXECUTE), + hint="Eine ausgestellte Lizenz ungültig machen", + action_hints={A.EXECUTE: "Widerrufen. Der Kunde verliert den Zugang."}, +) + +LICENSE_ACTIVATION = Resource( + key="license_activation", + parent="licenses", + label="Aktivierung von Hand", + actions=(A.READ, A.EXECUTE), + hint="Offline-Codes für Kunden ohne Internetzugang", + action_hints={A.EXECUTE: "Code prüfen und Antwortcode erzeugen."}, +) + PORTAL = Resource( key="portal", label="Kundenportal", - actions=(A.VIEW, A.EXECUTE), + actions=(A.READ, A.EXECUTE), hint="Zugangslinks für Kunden", action_hints={A.EXECUTE: "Zugangslink erzeugen und versenden."}, ) @@ -69,14 +108,14 @@ PORTAL = Resource( REPORTS = Resource( key="reports", label="Berichte der Sub-Lizenzserver", - actions=(A.VIEW,), + actions=(A.READ,), hint="Von untergeordneten Lizenzservern gemeldete Kunden, Tickets und Lizenzen", ) MAIL = Resource( key="settings_mail", label="E-Mail-Versand", - actions=(A.VIEW, A.EDIT, A.EXECUTE, A.SECRETS), + actions=(A.READ, A.WRITE, A.EXECUTE, A.SECRETS), hint="Microsoft Graph (App-only)", action_hints={ A.EXECUTE: "Verbindungstest ausführen.", @@ -87,7 +126,7 @@ MAIL = Resource( VENDOR = Resource( key="settings_vendor", label="Anbieterdaten", - actions=(A.VIEW, A.EDIT), + actions=(A.READ, A.WRITE), hint="Name, Kontakt, Logo und öffentlicher Endpunkt – reisen in jeder Lizenz mit", ) @@ -96,7 +135,17 @@ LICENSING_AREA = Area( label="Lizenzierung", icon="key", hint="Kunden, Tickets, Lizenzen. Ohne diesen Zugang bleibt der gesamte Bereich verborgen.", - resources=(CUSTOMERS, TICKETS, LICENSES, PORTAL, REPORTS), + resources=( + CUSTOMERS, + TICKETS, + LICENSES, + LICENSE_ISSUE, + LICENSE_RENEW, + LICENSE_REVOKE, + LICENSE_ACTIVATION, + PORTAL, + REPORTS, + ), ) PERMISSIONS = PermissionTree( @@ -108,12 +157,38 @@ PERMISSIONS = PermissionTree( ) +#: Alle Funktionsrechte ausserhalb der Verwaltung, in einer Gruppe. +#: +#: Seit 2.8.0 heisst das Administratorkennzeichen "Verwaltung" und nicht +#: "alles": Geraete, Neustarts, DHCP und Freigaben kommen ueber Gruppen. Diese +#: Vorlage ist die Gruppe, die man sich dafuer zuweist -- und die Migration +#: gibt sie bestehenden Administratoren, damit nach dem Update nichts fehlt, +#: was vorher ging. Entfernen kann man sie mit einem Klick. +#: +#: Die Liste entsteht aus dem Baum und nicht aus Tippen: eine neue Zeile in der +#: Matrix ist sonst genau die, die hier vergessen wird. +FUNKTIONEN_VOLLZUGRIFF = RolePreset( + key="functions", + name="Funktionen (Vollzugriff)", + description=( + "Alle Rechte ausserhalb der Verwaltung. Gedacht fuer Administratoren, " + "die nicht nur einstellen, sondern auch bedienen." + ), + grants=tuple( + (resource.key, "*") + for area in PERMISSIONS.areas + if area.key != PermissionTree.VERWALTUNGSBEREICH + for resource in area.resources + ), +) + + ROLE_PRESETS: tuple[RolePreset, ...] = ( RolePreset( key="viewer", name="Benutzer", description="Standardgruppe: nur das laufende Protokoll lesen.", - grants=(("logs_live", (A.VIEW,)),), + grants=(("logs_live", (A.READ,)),), is_default=True, ), RolePreset( @@ -123,10 +198,13 @@ ROLE_PRESETS: tuple[RolePreset, ...] = ( grants=( ("customers", "*"), ("tickets", "*"), - ("licenses", (A.VIEW, A.CREATE, A.EDIT, A.EXPORT)), + ("licenses", (A.READ, A.EXPORT)), + ("license_issue", "*"), + ("license_renew", "*"), + ("license_revoke", "*"), ("portal", "*"), - ("logs_live", (A.VIEW,)), - ("logs_audit", (A.VIEW,)), + ("logs_live", (A.READ,)), + ("logs_audit", (A.READ,)), ), ), RolePreset( @@ -134,11 +212,12 @@ ROLE_PRESETS: tuple[RolePreset, ...] = ( name="Support", description="Lesen und Offline-Aktivierungen verarbeiten.", grants=( - ("customers", (A.VIEW,)), - ("tickets", (A.VIEW,)), - ("licenses", (A.VIEW, A.EXECUTE)), - ("reports", (A.VIEW,)), - ("logs_live", (A.VIEW,)), + ("customers", (A.READ,)), + ("tickets", (A.READ,)), + ("licenses", (A.READ,)), + ("license_activation", "*"), + ("reports", (A.READ,)), + ("logs_live", (A.READ,)), ), ), RolePreset( @@ -146,16 +225,16 @@ ROLE_PRESETS: tuple[RolePreset, ...] = ( name="Revision", description="Nur lesen – inklusive Änderungsprotokoll und Diagnose.", grants=( - ("customers", (A.VIEW,)), - ("tickets", (A.VIEW,)), - ("licenses", (A.VIEW,)), - ("reports", (A.VIEW,)), - ("users", (A.VIEW,)), - ("groups", (A.VIEW,)), - ("logs_live", (A.VIEW,)), - ("logs_history", (A.VIEW, A.EXPORT)), - ("logs_audit", (A.VIEW, A.EXPORT)), - ("diagnostics", (A.VIEW,)), + ("customers", (A.READ,)), + ("tickets", (A.READ,)), + ("licenses", (A.READ,)), + ("reports", (A.READ,)), + ("users", (A.READ,)), + ("groups", (A.READ,)), + ("logs_live", (A.READ,)), + ("logs_history", (A.READ, A.EXPORT)), + ("logs_audit", (A.READ, A.EXPORT)), + ("diagnostics", (A.READ,)), ), ), RolePreset( @@ -166,18 +245,20 @@ ROLE_PRESETS: tuple[RolePreset, ...] = ( ("users", "*"), ("groups", "*"), ("settings_system", "*"), + ("settings_nav", "*"), ("settings_license", "*"), - ("settings_directory", (A.VIEW, A.EDIT, A.EXECUTE)), + ("settings_directory", (A.READ, A.WRITE, A.EXECUTE)), ("settings_web", "*"), ("settings_network", "*"), - ("settings_mail", (A.VIEW, A.EDIT, A.EXECUTE)), + ("settings_mail", (A.READ, A.WRITE, A.EXECUTE)), ("settings_vendor", "*"), - ("backup", (A.VIEW, A.EXECUTE, A.EXPORT)), + ("backup", (A.READ, A.EXECUTE, A.EXPORT)), ("trash", "*"), - ("diagnostics", (A.VIEW,)), - ("logs_audit", (A.VIEW,)), + ("diagnostics", (A.READ,)), + ("logs_audit", (A.READ,)), ), ), + FUNKTIONEN_VOLLZUGRIFF, ) __all__ = ["PERMISSIONS", "ROLE_PRESETS"] diff --git a/apps/tesm-license/src/tesm_license/schema.py b/apps/tesm-license/src/tesm_license/schema.py index 53c909b..74e2d8d 100644 --- a/apps/tesm-license/src/tesm_license/schema.py +++ b/apps/tesm-license/src/tesm_license/schema.py @@ -210,7 +210,21 @@ CREATE INDEX IF NOT EXISTS idx_licenses_revision ON licenses(revision_at); """ +def uebergang_funktionsgruppe(conn) -> None: # type: ignore[no-untyped-def] + """Wie in TESM: das Kennzeichen heisst ab 2.8.0 "Verwaltung". + + Ohne diesen Schritt koennte ein bestehender Administrator nach dem Update + keine Lizenz mehr ausstellen -- und wuerde das fuer einen Fehler halten. + """ + from tesm_core.rbac import service as rbac + + from .permissions import FUNKTIONEN_VOLLZUGRIFF, PERMISSIONS + + rbac.funktionsgruppe_fuer_administratoren(conn, PERMISSIONS, FUNKTIONEN_VOLLZUGRIFF) + + LICENSE_MIGRATIONS: tuple[Migration, ...] = ( Migration(version=1, name="baseline", sql=V1, allow_integrity_error=True), Migration(version=2, name="revision-timestamp", sql=V2), + Migration(version=3, name="funktionsgruppe-fuer-admins", fn=uebergang_funktionsgruppe), ) diff --git a/apps/tesm-license/src/tesm_license/services/transfer.py b/apps/tesm-license/src/tesm_license/services/transfer.py index e28c9b3..9b8eb44 100644 --- a/apps/tesm-license/src/tesm_license/services/transfer.py +++ b/apps/tesm-license/src/tesm_license/services/transfer.py @@ -24,6 +24,7 @@ import sqlite3 from typing import Any, Callable, Iterable from tesm_core.db import Database +from tesm_core.rbac.legacy_permissions import umgeschrieben from tesm_core.transfer import ( MIN_PASSPHRASE_LENGTH, PREVIEW_TTL_MINUTES, @@ -153,6 +154,10 @@ def apply_import( zusammengefuehrt; Tickets und Lizenzen ueber ihre ID, weil die zugleich kryptographisch relevant ist (Self-Service-Link bzw. signierte Lizenz-ID). """ + #: Merkt, ob das Paket alte Rechteschluessel mitbringt -- dann stammt es + #: von vor 2.8.0, und ein Administrator konnte dort noch alles. + alte_fassung: set[bool] = set() + wanted = {key for key in categories if key in CATEGORY_BY_KEY} report: dict[str, dict[str, int]] = {} stamp = _stamp() @@ -332,9 +337,16 @@ def apply_import( group_id = int(existing["id"]) bump("groups", "updated") conn.execute("DELETE FROM group_permissions WHERE group_id=?", (group_id,)) + # Wie in TESM: eine Sicherung aus 2.7.x nennt customers.view und + # tickets.edit. Unbekannte Schluessel wirft sanitize() still weg -- + # die wiederhergestellte Gruppe waere leer, ohne Meldung. + mitgebracht = [str(recht) for recht in entry.get("permissions", []) or []] + neue = umgeschrieben(mitgebracht) + if sorted(set(mitgebracht)) != neue: + alte_fassung.add(True) conn.executemany( "INSERT OR IGNORE INTO group_permissions (group_id, permission) VALUES (?,?)", - [(group_id, str(permission)) for permission in entry.get("permissions", [])], + [(group_id, permission) for permission in neue], ) if "users" in wanted: @@ -393,6 +405,18 @@ def apply_import( set_setting(str(key), value) bump("settings", "updated") + if alte_fassung and "users" in wanted: + # Wie die Migration: aus einer Sicherung von vor 2.8.0 kommen + # Administratoren, die alles konnten. Ohne die Gruppe koennten sie + # nach dem Einlesen keine Lizenz mehr ausstellen. + from tesm_core.rbac import service as rbac + + from ..permissions import FUNKTIONEN_VOLLZUGRIFF, PERMISSIONS + + rbac.funktionsgruppe_fuer_administratoren( + conn, PERMISSIONS, FUNKTIONEN_VOLLZUGRIFF + ) + return report 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 7d8ae7d..9f90ebe 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 @@ -171,22 +171,22 @@
{{ field('name', 'Firmenname', value=customer.name, required=True, - disabled=not current_user.has('customers.edit')) }} + disabled=not current_user.has('customers.write')) }} {{ field('contact_email', 'E-Mail', value=customer.contact_email, type='email', - disabled=not current_user.has('customers.edit')) }} + disabled=not current_user.has('customers.write')) }} {{ field('contact_person', 'Ansprechpartner', value=customer.contact_person, - disabled=not current_user.has('customers.edit')) }} + disabled=not current_user.has('customers.write')) }} {{ field('contact_phone', 'Telefon', value=customer.contact_phone, - disabled=not current_user.has('customers.edit')) }} + disabled=not current_user.has('customers.write')) }}
{{ field('street', 'Strasse', value=customer.street, - disabled=not current_user.has('customers.edit')) }} + disabled=not current_user.has('customers.write')) }} {{ field('house_number', 'Nr.', value=customer.house_number, - disabled=not current_user.has('customers.edit')) }} + disabled=not current_user.has('customers.write')) }} {{ field('postal_code', 'PLZ', value=customer.postal_code, - disabled=not current_user.has('customers.edit')) }} + disabled=not current_user.has('customers.write')) }} {{ field('city', 'Ort', value=customer.city, - disabled=not current_user.has('customers.edit')) }} + disabled=not current_user.has('customers.write')) }}
{{ textarea_field('notes', 'Notizen', value=customer.notes, rows=3, extra_class='field--full') }} @@ -200,7 +200,7 @@ {{ icon('copy', size=13) }}
- {% if current_user.has('customers.edit') %} + {% if current_user.has('customers.write') %} - {% if current_user.has('settings_license.edit') %} + {% if current_user.has('settings_license.write') %}