Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48ce56fe7d |
No files matched your search
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm-license"
|
name = "tesm-license"
|
||||||
version = "2.8.2"
|
version = "2.8.3"
|
||||||
description = "TESM-Lizenzserver -- Kunden, Tickets, Lizenzen, Aktivierung"
|
description = "TESM-Lizenzserver -- Kunden, Tickets, Lizenzen, Aktivierung"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -68,7 +68,11 @@ def system():
|
|||||||
else ""
|
else ""
|
||||||
),
|
),
|
||||||
timezones=_timezone_choices(),
|
timezones=_timezone_choices(),
|
||||||
nav_items=extension.nav.items,
|
# Nicht ``nav_items``: dieser Name gehoert dem Seitenrahmen. Wer ihn hier
|
||||||
|
# belegt, ueberschreibt die gefilterte Navigation aus dem Kontext --
|
||||||
|
# und die Seitenleiste zeigte dann jeden Punkt, auch den, den dieser
|
||||||
|
# Benutzer nicht darf und den die Lizenz nicht freischaltet.
|
||||||
|
nav_baum=extension.nav.items,
|
||||||
nav_order=extension.settings.get(conn, "nav_order") or extension.nav.default_order(),
|
nav_order=extension.settings.get(conn, "nav_order") or extension.nav.default_order(),
|
||||||
nav_child_order=extension.settings.get(conn, "nav_child_order") or {},
|
nav_child_order=extension.settings.get(conn, "nav_child_order") or {},
|
||||||
sysops_available=bool(sysops and sysops.available),
|
sysops_available=bool(sysops and sysops.available),
|
||||||
|
|||||||
@@ -183,6 +183,14 @@ FUNKTIONEN_VOLLZUGRIFF = RolePreset(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#: Wie in TESM: drei Gruppen ab Werk, nicht sechs.
|
||||||
|
#:
|
||||||
|
#: Auch "Vertrieb" ist hier entfallen, obwohl es die Rolle ist, die den
|
||||||
|
#: Lizenzserver taeglich benutzt -- gerade deshalb: wer Lizenzen ausstellt,
|
||||||
|
#: schneidet die Gruppe ohnehin auf sein Haus zu (darf Vertrieb widerrufen?
|
||||||
|
#: Tickets loeschen? in die Protokolle sehen?). Eine mitgelieferte Antwort auf
|
||||||
|
#: diese Frage wird entweder uebernommen, ohne sie zu pruefen, oder sie wird
|
||||||
|
#: umgebaut. "Funktionen (Vollzugriff)" ist der ehrlichere Startpunkt.
|
||||||
ROLE_PRESETS: tuple[RolePreset, ...] = (
|
ROLE_PRESETS: tuple[RolePreset, ...] = (
|
||||||
RolePreset(
|
RolePreset(
|
||||||
key="viewer",
|
key="viewer",
|
||||||
@@ -191,52 +199,6 @@ ROLE_PRESETS: tuple[RolePreset, ...] = (
|
|||||||
grants=(("logs_live", (A.READ,)),),
|
grants=(("logs_live", (A.READ,)),),
|
||||||
is_default=True,
|
is_default=True,
|
||||||
),
|
),
|
||||||
RolePreset(
|
|
||||||
key="sales",
|
|
||||||
name="Vertrieb",
|
|
||||||
description="Kunden und Tickets pflegen, Lizenzen ausstellen und versenden.",
|
|
||||||
grants=(
|
|
||||||
("customers", "*"),
|
|
||||||
("tickets", "*"),
|
|
||||||
("licenses", (A.READ, A.EXPORT)),
|
|
||||||
("license_issue", "*"),
|
|
||||||
("license_renew", "*"),
|
|
||||||
("license_revoke", "*"),
|
|
||||||
("portal", "*"),
|
|
||||||
("logs_live", (A.READ,)),
|
|
||||||
("logs_audit", (A.READ,)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
RolePreset(
|
|
||||||
key="support",
|
|
||||||
name="Support",
|
|
||||||
description="Lesen und Offline-Aktivierungen verarbeiten.",
|
|
||||||
grants=(
|
|
||||||
("customers", (A.READ,)),
|
|
||||||
("tickets", (A.READ,)),
|
|
||||||
("licenses", (A.READ,)),
|
|
||||||
("license_activation", "*"),
|
|
||||||
("reports", (A.READ,)),
|
|
||||||
("logs_live", (A.READ,)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
RolePreset(
|
|
||||||
key="auditor",
|
|
||||||
name="Revision",
|
|
||||||
description="Nur lesen – inklusive Änderungsprotokoll und Diagnose.",
|
|
||||||
grants=(
|
|
||||||
("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(
|
RolePreset(
|
||||||
key="administration",
|
key="administration",
|
||||||
name="Administration",
|
name="Administration",
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ Aenderungen gegenueber dem Vorgaenger:
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from tesm_core.migrations import Migration
|
from tesm_core.migrations import Migration
|
||||||
|
|
||||||
V1 = """
|
V1 = """
|
||||||
@@ -223,8 +225,79 @@ def uebergang_funktionsgruppe(conn) -> None: # type: ignore[no-untyped-def]
|
|||||||
rbac.funktionsgruppe_fuer_administratoren(conn, PERMISSIONS, FUNKTIONEN_VOLLZUGRIFF)
|
rbac.funktionsgruppe_fuer_administratoren(conn, PERMISSIONS, FUNKTIONEN_VOLLZUGRIFF)
|
||||||
|
|
||||||
|
|
||||||
|
#: Die drei Vorlagen, die es bis 2.8.2 gab -- eingefrorene Kopie, siehe TESM.
|
||||||
|
ABGELEGTE_VORLAGEN: tuple[tuple[str, str, tuple[tuple[str, Any], ...]], ...] = (
|
||||||
|
(
|
||||||
|
"sales",
|
||||||
|
"Vertrieb",
|
||||||
|
(
|
||||||
|
("customers", "*"),
|
||||||
|
("tickets", "*"),
|
||||||
|
("licenses", ("read", "export")),
|
||||||
|
("license_issue", "*"),
|
||||||
|
("license_renew", "*"),
|
||||||
|
("license_revoke", "*"),
|
||||||
|
("portal", "*"),
|
||||||
|
("logs_live", ("read",)),
|
||||||
|
("logs_audit", ("read",)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"support",
|
||||||
|
"Support",
|
||||||
|
(
|
||||||
|
("customers", ("read",)),
|
||||||
|
("tickets", ("read",)),
|
||||||
|
("licenses", ("read",)),
|
||||||
|
("license_activation", "*"),
|
||||||
|
("reports", ("read",)),
|
||||||
|
("logs_live", ("read",)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"auditor",
|
||||||
|
"Revision",
|
||||||
|
(
|
||||||
|
("customers", ("read",)),
|
||||||
|
("tickets", ("read",)),
|
||||||
|
("licenses", ("read",)),
|
||||||
|
("reports", ("read",)),
|
||||||
|
("users", ("read",)),
|
||||||
|
("groups", ("read",)),
|
||||||
|
("logs_live", ("read",)),
|
||||||
|
("logs_history", ("read", "export")),
|
||||||
|
("logs_audit", ("read", "export")),
|
||||||
|
("diagnostics", ("read",)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def uebergang_standardgruppen(conn) -> None: # type: ignore[no-untyped-def]
|
||||||
|
"""Wie in TESM: unberuehrte Vorlagen in den Papierkorb, benutzte bleiben."""
|
||||||
|
from tesm_core.rbac import service as rbac
|
||||||
|
from tesm_core.rbac.model import Action, RolePreset
|
||||||
|
|
||||||
|
from .permissions import PERMISSIONS
|
||||||
|
|
||||||
|
vorlagen = [
|
||||||
|
RolePreset(
|
||||||
|
key=key,
|
||||||
|
name=name,
|
||||||
|
description="",
|
||||||
|
grants=tuple(
|
||||||
|
(ressource, a if a == "*" else tuple(Action(x) for x in a))
|
||||||
|
for ressource, a in grants
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for key, name, grants in ABGELEGTE_VORLAGEN
|
||||||
|
]
|
||||||
|
rbac.unberuehrte_vorlagen_entfernen(conn, PERMISSIONS, vorlagen)
|
||||||
|
|
||||||
|
|
||||||
LICENSE_MIGRATIONS: tuple[Migration, ...] = (
|
LICENSE_MIGRATIONS: tuple[Migration, ...] = (
|
||||||
Migration(version=1, name="baseline", sql=V1, allow_integrity_error=True),
|
Migration(version=1, name="baseline", sql=V1, allow_integrity_error=True),
|
||||||
Migration(version=2, name="revision-timestamp", sql=V2),
|
Migration(version=2, name="revision-timestamp", sql=V2),
|
||||||
Migration(version=3, name="funktionsgruppe-fuer-admins", fn=uebergang_funktionsgruppe),
|
Migration(version=3, name="funktionsgruppe-fuer-admins", fn=uebergang_funktionsgruppe),
|
||||||
|
Migration(version=4, name="standardgruppen-aufraeumen", fn=uebergang_standardgruppen),
|
||||||
)
|
)
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{% if current_user.is_admin %}
|
{% if current_user.is_admin %}
|
||||||
{{ nav_order_card(nav_items, nav_order, nav_child_order,
|
{{ nav_order_card(nav_baum, nav_order, nav_child_order,
|
||||||
url_for('admin.nav_order'), csrf_field_name, csrf_token(),
|
url_for('admin.nav_order'), csrf_field_name, csrf_token(),
|
||||||
sub='Gilt für alle Benutzer') }}
|
sub='Gilt für alle Benutzer') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm-core"
|
name = "tesm-core"
|
||||||
version = "2.8.2"
|
version = "2.8.3"
|
||||||
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
|
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -102,6 +102,14 @@ CORE_SETTINGS: tuple[SettingSpec, ...] = (
|
|||||||
),
|
),
|
||||||
# -- Webserver/TLS (Host-Ebene, in beiden Anwendungen gleich) ----------- #
|
# -- Webserver/TLS (Host-Ebene, in beiden Anwendungen gleich) ----------- #
|
||||||
# Lizenzserver: eigenes Zertifikat statt abgeschalteter Pruefung.
|
# Lizenzserver: eigenes Zertifikat statt abgeschalteter Pruefung.
|
||||||
|
SettingSpec(
|
||||||
|
"modules_disabled",
|
||||||
|
[],
|
||||||
|
"json",
|
||||||
|
"Vor Ort abgeschaltete Module",
|
||||||
|
hint="Die Lizenz sagt, was erlaubt ist; das hier sagt, was benutzt wird.",
|
||||||
|
section="system",
|
||||||
|
),
|
||||||
SettingSpec(
|
SettingSpec(
|
||||||
"license_server_ca",
|
"license_server_ca",
|
||||||
"",
|
"",
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import urllib.request
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable, Iterable
|
||||||
|
|
||||||
import tesm_licensing as lic
|
import tesm_licensing as lic
|
||||||
|
|
||||||
@@ -54,6 +54,21 @@ SETTING_HEARTBEAT_ERROR = "license_heartbeat_error"
|
|||||||
EXPIRY_HINT_DAYS = 90
|
EXPIRY_HINT_DAYS = 90
|
||||||
SETTING_EXPORT_GRACE = "license_export_grace"
|
SETTING_EXPORT_GRACE = "license_export_grace"
|
||||||
|
|
||||||
|
#: Module, die der Betreiber vor Ort abgeschaltet hat (Liste von Namen).
|
||||||
|
#:
|
||||||
|
#: Die Lizenz sagt, *was erlaubt ist*; sie sagt nicht, was ein Haus auch
|
||||||
|
#: benutzen will. Wer kein DHCP ueber TESM fahren will, weil ein anderer Server
|
||||||
|
#: das tut, soll den Punkt nicht wegdruecken muessen -- und vor allem soll
|
||||||
|
#: niemand versehentlich eine fremde Kea-Konfiguration ueberschreiben koennen.
|
||||||
|
#: Abschalten ist deshalb mehr als Aufraeumen der Navigation: die Sperre sitzt
|
||||||
|
#: an derselben Stelle wie die Lizenzpruefung, also vor jeder Route.
|
||||||
|
#: Bewusst **ohne** das Praefix ``license_``: Sicherungen lassen alles mit
|
||||||
|
#: diesem Praefix draussen, weil Lizenz- und Aktivierungszustand zur Maschine
|
||||||
|
#: gehoeren und nicht in ein Backup. Der Schalter ist aber keine Lizenzangabe,
|
||||||
|
#: sondern eine Entscheidung des Hauses -- die soll eine Wiederherstellung
|
||||||
|
#: mitbringen.
|
||||||
|
SETTING_MODULES_OFF = "modules_disabled"
|
||||||
|
|
||||||
STATUS_INACTIVE = "inactive"
|
STATUS_INACTIVE = "inactive"
|
||||||
STATUS_PENDING = "pending"
|
STATUS_PENDING = "pending"
|
||||||
STATUS_ACTIVE = "active"
|
STATUS_ACTIVE = "active"
|
||||||
@@ -121,6 +136,8 @@ class LicenseState:
|
|||||||
last_heartbeat_at: str = ""
|
last_heartbeat_at: str = ""
|
||||||
heartbeat_error: str = ""
|
heartbeat_error: str = ""
|
||||||
pending_action: str = ""
|
pending_action: str = ""
|
||||||
|
#: Vor Ort abgeschaltet -- unabhaengig davon, was die Lizenz erlaubt.
|
||||||
|
modules_off: frozenset[str] = frozenset()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def role(self) -> str:
|
def role(self) -> str:
|
||||||
@@ -128,8 +145,14 @@ class LicenseState:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def modules(self) -> frozenset[str]:
|
def modules(self) -> frozenset[str]:
|
||||||
|
"""Was die Lizenz freischaltet -- ohne Ruecksicht auf den Schalter."""
|
||||||
return self.status.modules if self.status else frozenset()
|
return self.status.modules if self.status else frozenset()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def modules_in_use(self) -> frozenset[str]:
|
||||||
|
"""Was tatsaechlich laeuft: lizenziert **und** nicht abgeschaltet."""
|
||||||
|
return self.modules - self.modules_off
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def customer_name(self) -> str:
|
def customer_name(self) -> str:
|
||||||
return str((self.status.customer if self.status else {}).get("name") or "")
|
return str((self.status.customer if self.status else {}).get("name") or "")
|
||||||
@@ -179,6 +202,7 @@ class LicenseManager:
|
|||||||
"""Liest Lizenzdatei und Status neu ein."""
|
"""Liest Lizenzdatei und Status neu ein."""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
state = LicenseState()
|
state = LicenseState()
|
||||||
|
state.modules_off = self.abgeschaltete_module(conn)
|
||||||
state.last_heartbeat_at = str(self.settings.get(conn, SETTING_LAST_HEARTBEAT) or "")
|
state.last_heartbeat_at = str(self.settings.get(conn, SETTING_LAST_HEARTBEAT) or "")
|
||||||
state.heartbeat_error = str(self.settings.get(conn, SETTING_HEARTBEAT_ERROR) or "")
|
state.heartbeat_error = str(self.settings.get(conn, SETTING_HEARTBEAT_ERROR) or "")
|
||||||
pending = self._pending(conn)
|
pending = self._pending(conn)
|
||||||
@@ -239,9 +263,51 @@ class LicenseManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def module_active(self, name: str) -> bool:
|
def module_active(self, name: str) -> bool:
|
||||||
"""Ist ein Vollmodul (dhcp/fileshare/maintenance) freigeschaltet?"""
|
"""Ist ein Vollmodul (dhcp/fileshare/maintenance) in Betrieb?
|
||||||
|
|
||||||
|
Zwei Bedingungen, und beide muessen gelten: die Lizenz muss es
|
||||||
|
freischalten, und der Betreiber darf es nicht abgeschaltet haben. Diese
|
||||||
|
eine Funktion fragen die Navigation, jede Route und die Hintergrund-
|
||||||
|
aufgaben -- darum sitzt der Schalter hier und nicht in einer Vorlage.
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
self.active()
|
||||||
|
and name in self._state.modules
|
||||||
|
and name not in self._state.modules_off
|
||||||
|
)
|
||||||
|
|
||||||
|
def module_licensed(self, name: str) -> bool:
|
||||||
|
"""Schaltet die Lizenz das Modul frei? (Ohne den Schalter vor Ort.)"""
|
||||||
return self.active() and name in self._state.modules
|
return self.active() and name in self._state.modules
|
||||||
|
|
||||||
|
def abgeschaltete_module(self, conn: sqlite3.Connection) -> frozenset[str]:
|
||||||
|
"""Liest die Liste aus den Einstellungen."""
|
||||||
|
gespeichert = self.settings.get(conn, SETTING_MODULES_OFF) or []
|
||||||
|
if isinstance(gespeichert, str): # Altbestand: ein einzelner Name
|
||||||
|
gespeichert = [gespeichert]
|
||||||
|
return frozenset(str(name) for name in gespeichert)
|
||||||
|
|
||||||
|
def module_abschalten(
|
||||||
|
self, conn: sqlite3.Connection, namen: Iterable[str], *, actor: str = ""
|
||||||
|
) -> tuple[frozenset[str], frozenset[str]]:
|
||||||
|
"""Setzt die Liste der abgeschalteten Module neu.
|
||||||
|
|
||||||
|
Gibt ``(eingeschaltet, abgeschaltet)`` zurueck -- also nur das, was sich
|
||||||
|
wirklich geaendert hat; der Aufrufer schreibt daraus seinen
|
||||||
|
Protokolleintrag.
|
||||||
|
|
||||||
|
Der Zustand wird hier nicht neu eingelesen, sondern nur an dieser einen
|
||||||
|
Stelle nachgezogen: ein voller Neuaufbau wuerde die Lizenzdatei erneut
|
||||||
|
pruefen, obwohl sich an ihr nichts geaendert hat -- und der Schalter ist
|
||||||
|
das Einzige, was jetzt anders ist.
|
||||||
|
"""
|
||||||
|
vorher = self.abgeschaltete_module(conn)
|
||||||
|
nachher = frozenset(str(name) for name in namen)
|
||||||
|
self.settings.set(conn, SETTING_MODULES_OFF, sorted(nachher), actor=actor)
|
||||||
|
with self._lock:
|
||||||
|
self._state.modules_off = nachher
|
||||||
|
return vorher - nachher, nachher - vorher
|
||||||
|
|
||||||
def feature_allowed(self, feature: str) -> bool:
|
def feature_allowed(self, feature: str) -> bool:
|
||||||
"""Lizenzpflichtige Einzelfunktion.
|
"""Lizenzpflichtige Einzelfunktion.
|
||||||
|
|
||||||
|
|||||||
@@ -328,6 +328,83 @@ def ensure_presets(
|
|||||||
return created
|
return created
|
||||||
|
|
||||||
|
|
||||||
|
def unberuehrte_vorlagen_entfernen(
|
||||||
|
conn: sqlite3.Connection, tree: PermissionTree, presets: Sequence[RolePreset]
|
||||||
|
) -> list[str]:
|
||||||
|
"""Legt Vorlagengruppen in den Papierkorb -- aber nur unberuehrte.
|
||||||
|
|
||||||
|
Wird gebraucht, wenn eine Vorlage aus der Auslieferung verschwindet. Auf
|
||||||
|
einer frischen Installation entsteht sie dann gar nicht erst; auf einer
|
||||||
|
bestehenden steht sie in der Datenbank, und eine Migration, die sie einfach
|
||||||
|
loescht, nimmt einem Kunden womoeglich eine Gruppe weg, mit der er
|
||||||
|
arbeitet.
|
||||||
|
|
||||||
|
Darum diese fuenf Bedingungen, und alle fuenf muessen zutreffen:
|
||||||
|
|
||||||
|
* Die Gruppe stammt **aus der Auslieferung**: ihr ``preset_key`` ist der der
|
||||||
|
Vorlage. Diese Kennung setzt nur :func:`ensure_presets`; eine selbst
|
||||||
|
angelegte Gruppe hat sie nicht, auch wenn sie zufaellig denselben Namen
|
||||||
|
traegt. Ueber den Namen wird bewusst *nicht* gesucht -- sonst raeumte
|
||||||
|
dieser Schritt jemandem seine eigene Gruppe "Betrieb" weg.
|
||||||
|
* Sie heisst noch so, wie sie ausgeliefert wurde. Ein anderer Name heisst:
|
||||||
|
jemand hat sie uebernommen.
|
||||||
|
* Die Gruppe hat **keine Mitglieder**. Wer Mitglieder hat, wird benutzt.
|
||||||
|
* Ihre Rechte sind **unveraendert** die der Vorlage. Jede Abweichung heisst:
|
||||||
|
jemand hat sie angepasst, also ist sie seine, nicht unsere.
|
||||||
|
* Sie ist keine Systemgruppe.
|
||||||
|
|
||||||
|
Geloescht wird weich: die Gruppe liegt im Papierkorb und kommt mit einem
|
||||||
|
Klick zurueck. Gibt die Namen der einsortierten Gruppen zurueck.
|
||||||
|
"""
|
||||||
|
entfernt: list[str] = []
|
||||||
|
gruppen = list_groups(conn)
|
||||||
|
for preset in presets:
|
||||||
|
gruppe = next(
|
||||||
|
(
|
||||||
|
g
|
||||||
|
for g in gruppen
|
||||||
|
if g.preset_key
|
||||||
|
and g.preset_key == preset.key
|
||||||
|
and g.name.lower() == preset.name.lower()
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if gruppe is None or gruppe.is_system or gruppe.member_count:
|
||||||
|
continue
|
||||||
|
if set(gruppe.permissions) != set(tree.expand_preset(preset)):
|
||||||
|
continue
|
||||||
|
conn.execute("UPDATE groups SET deleted_at=? WHERE id=?", (_now(), gruppe.id))
|
||||||
|
entfernt.append(gruppe.name)
|
||||||
|
return entfernt
|
||||||
|
|
||||||
|
|
||||||
|
def funktionsgruppe_anlegen(
|
||||||
|
conn: sqlite3.Connection, tree: PermissionTree, preset: RolePreset
|
||||||
|
) -> int:
|
||||||
|
"""Legt die Vollzugriffsgruppe an, falls sie fehlt, und gibt ihre Nummer.
|
||||||
|
|
||||||
|
Getrennt von :func:`funktionsgruppe_fuer_administratoren`, weil das zwei
|
||||||
|
verschiedene Dinge sind: hier entsteht nur die Gruppe, dort bekommt sie
|
||||||
|
**jeder** Administrator. Wer ein einzelnes Konto aufnehmen will -- etwa
|
||||||
|
das, das an der Kommandozeile entsteht --, braucht das erste ohne das
|
||||||
|
zweite; sonst faengt sich ein Administrator, der die Gruppe bewusst
|
||||||
|
abgelegt hat, sie beim naechsten Aufruf wieder ein.
|
||||||
|
"""
|
||||||
|
zeile = Database.one(
|
||||||
|
conn, "SELECT id FROM groups WHERE name=? COLLATE NOCASE", (preset.name,)
|
||||||
|
)
|
||||||
|
if zeile is not None:
|
||||||
|
return int(zeile["id"])
|
||||||
|
cursor = conn.execute(
|
||||||
|
"INSERT INTO groups (name, description, is_system, is_default, preset_key, "
|
||||||
|
"created_at, updated_at) VALUES (?,?,0,0,?,?,?)",
|
||||||
|
(preset.name, preset.description, preset.key, _now(), _now()),
|
||||||
|
)
|
||||||
|
gruppe = int(cursor.lastrowid or 0)
|
||||||
|
_write_permissions(conn, gruppe, tree.expand_preset(preset))
|
||||||
|
return gruppe
|
||||||
|
|
||||||
|
|
||||||
def funktionsgruppe_fuer_administratoren(
|
def funktionsgruppe_fuer_administratoren(
|
||||||
conn: sqlite3.Connection, tree: PermissionTree, preset: RolePreset
|
conn: sqlite3.Connection, tree: PermissionTree, preset: RolePreset
|
||||||
) -> int:
|
) -> int:
|
||||||
@@ -345,20 +422,7 @@ def funktionsgruppe_fuer_administratoren(
|
|||||||
|
|
||||||
Gibt die Nummer der Gruppe zurueck.
|
Gibt die Nummer der Gruppe zurueck.
|
||||||
"""
|
"""
|
||||||
zeile = Database.one(
|
gruppe = funktionsgruppe_anlegen(conn, tree, preset)
|
||||||
conn, "SELECT id FROM groups WHERE name=? COLLATE NOCASE", (preset.name,)
|
|
||||||
)
|
|
||||||
if zeile is None:
|
|
||||||
cursor = conn.execute(
|
|
||||||
"INSERT INTO groups (name, description, is_system, is_default, preset_key, "
|
|
||||||
"created_at, updated_at) VALUES (?,?,0,0,?,?,?)",
|
|
||||||
(preset.name, preset.description, preset.key, _now(), _now()),
|
|
||||||
)
|
|
||||||
gruppe = int(cursor.lastrowid or 0)
|
|
||||||
_write_permissions(conn, gruppe, tree.expand_preset(preset))
|
|
||||||
else:
|
|
||||||
gruppe = int(zeile["id"])
|
|
||||||
|
|
||||||
conn.executemany(
|
conn.executemany(
|
||||||
"INSERT OR IGNORE INTO user_groups (user_id, group_id) VALUES (?,?)",
|
"INSERT OR IGNORE INTO user_groups (user_id, group_id) VALUES (?,?)",
|
||||||
[
|
[
|
||||||
|
|||||||
Reference in new issue
Block a user