Version 2.8.0: Rechtekonzept -- eine Zeile je Funktion, Administrator heisst Verwaltung
This commit is contained in:
1 parent
c3c6eae72d
commit
f6b0bbaad1
47 files changed
+1366
-304
No files matched your search
@@ -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 = [
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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("/<int:customer_id>")
|
||||
@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("/<int:customer_id>")
|
||||
@permission_required("customers.edit")
|
||||
@permission_required("customers.write")
|
||||
def update(customer_id: int):
|
||||
try:
|
||||
with transaction() as conn:
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
@@ -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("/<license_id>")
|
||||
@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/<ticket_id>")
|
||||
@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("/<license_id>/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("/<license_id>/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/<license_id>")
|
||||
@permission_required("reports.view")
|
||||
@permission_required("reports.read")
|
||||
def report_detail(license_id: str):
|
||||
conn = db()
|
||||
row = issuance.get_license(conn, license_id)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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("/<ticket_id>")
|
||||
@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("/<ticket_id>")
|
||||
@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("/<ticket_id>/lock")
|
||||
@permission_required("tickets.edit")
|
||||
@permission_required("tickets.write")
|
||||
def toggle_block(ticket_id: str):
|
||||
conn = db()
|
||||
ticket = issuance.get_ticket(conn, ticket_id)
|
||||
|
||||
@@ -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",),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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"]
|
||||
@@ -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),
|
||||
)
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -171,22 +171,22 @@
|
||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||
<div class="card__body stack">
|
||||
{{ 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')) }}
|
||||
<div class="form-grid">
|
||||
{{ 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')) }}
|
||||
</div>
|
||||
{{ textarea_field('notes', 'Notizen', value=customer.notes, rows=3,
|
||||
extra_class='field--full') }}
|
||||
@@ -200,7 +200,7 @@
|
||||
{{ icon('copy', size=13) }}</button></dd>
|
||||
</dl>
|
||||
</div>
|
||||
{% if current_user.has('customers.edit') %}
|
||||
{% if current_user.has('customers.write') %}
|
||||
<div class="card__footer modal__footer--split">
|
||||
{% if current_user.has('customers.delete') %}
|
||||
<button class="btn btn--sm btn--ghost danger-text" type="submit"
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% if current_user.has('licenses.edit') and license.status != 'revoked' %}
|
||||
{% if current_user.has('license_renew.execute') and license.status != 'revoked' %}
|
||||
<section class="card">
|
||||
<div class="card__header">
|
||||
<div class="flex-1">
|
||||
@@ -204,13 +204,13 @@
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.has('licenses.edit') or current_user.has('licenses.delete') %}
|
||||
{% if current_user.has('license_revoke.execute') or current_user.has('licenses.delete') %}
|
||||
<section class="card">
|
||||
<div class="card__header">
|
||||
<div class="flex-1"><div class="card__title">{{ icon('alert', size=17) }} Lebenszyklus</div></div>
|
||||
</div>
|
||||
<div class="card__body stack stack--tight">
|
||||
{% if current_user.has('licenses.edit') and license.status != 'revoked' %}
|
||||
{% if current_user.has('license_revoke.execute') and license.status != 'revoked' %}
|
||||
<button class="btn btn--sm btn--danger btn--block" type="button"
|
||||
data-dialog-open="revoke-dialog">Widerrufen</button>
|
||||
<p class="fs-xs faint">
|
||||
@@ -239,7 +239,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block dialogs %}
|
||||
{% if current_user.has('licenses.edit') and license.status != 'revoked' %}
|
||||
{% if current_user.has('license_revoke.execute') and license.status != 'revoked' %}
|
||||
{% call modal('revoke-dialog', 'Lizenz widerrufen', license.customer_name, 'narrow') %}
|
||||
<form method="post" action="{{ url_for('licenses.revoke', license_id=license.license_id) }}">
|
||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
{% block topbar_extra %}
|
||||
{{ search_box('licenses-table', 'Kunde, Lizenz-ID, Host ...') }}
|
||||
{% if current_user.has('licenses.execute') %}
|
||||
{% if current_user.has('license_activation.execute') %}
|
||||
<a class="btn btn--sm" href="{{ url_for('licenses.manual_code') }}">
|
||||
{{ icon('transfer', size=15) }} Offline-Code
|
||||
</a>
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
{% if current_user.has('settings_license.edit') %}
|
||||
{% if current_user.has('settings_license.write') %}
|
||||
<div class="card__footer">
|
||||
{% if not state.activated %}
|
||||
<form method="post" action="{{ url_for('own_license.activate') }}" data-no-guard>
|
||||
@@ -151,7 +151,7 @@
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.has('settings_license.edit') %}
|
||||
{% if current_user.has('settings_license.write') %}
|
||||
<section class="card">
|
||||
<div class="card__header">
|
||||
<div class="flex-1">
|
||||
|
||||
@@ -33,16 +33,16 @@ bewusst keine echte Nachricht.', 'info') }}
|
||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||
<div class="card__body stack">
|
||||
{{ checkbox('graph_enabled', 'E-Mail-Versand aktiv', checked=values.graph_enabled,
|
||||
disabled=not current_user.has('settings_mail.edit')) }}
|
||||
disabled=not current_user.has('settings_mail.write')) }}
|
||||
<div class="form-grid">
|
||||
{{ field('graph_tenant_id', 'Verzeichnis-ID (Tenant)', value=values.graph_tenant_id,
|
||||
disabled=not current_user.has('settings_mail.edit')) }}
|
||||
disabled=not current_user.has('settings_mail.write')) }}
|
||||
{{ field('graph_client_id', 'Anwendungs-ID (Client)', value=values.graph_client_id,
|
||||
disabled=not current_user.has('settings_mail.edit')) }}
|
||||
disabled=not current_user.has('settings_mail.write')) }}
|
||||
{{ field('graph_sender', 'Absender-Postfach', value=values.graph_sender, type='email',
|
||||
disabled=not current_user.has('settings_mail.edit')) }}
|
||||
disabled=not current_user.has('settings_mail.write')) }}
|
||||
{{ field('graph_reply_to', 'Antwortadresse', value=values.graph_reply_to, type='email',
|
||||
disabled=not current_user.has('settings_mail.edit')) }}
|
||||
disabled=not current_user.has('settings_mail.write')) }}
|
||||
</div>
|
||||
<fieldset class="fieldset">
|
||||
<legend>Client-Secret {% if has_secret %}{{ pill('gesetzt', 'online') }}{% else %}{{ pill('fehlt', 'warning') }}{% endif %}</legend>
|
||||
@@ -55,7 +55,7 @@ bewusst keine echte Nachricht.', 'info') }}
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
</div>
|
||||
{% if current_user.has('settings_mail.edit') %}
|
||||
{% if current_user.has('settings_mail.write') %}
|
||||
<div class="card__footer"><button class="btn btn--primary" type="submit">Speichern</button></div>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
@@ -19,16 +19,16 @@ welche Basis-URL in kundengerichteten Links steht.', 'info') }}
|
||||
<div class="card__body stack">
|
||||
<div class="form-grid">
|
||||
{{ field('vendor_name', 'Anbietername', value=values.vendor_name,
|
||||
disabled=not current_user.has('settings_vendor.edit')) }}
|
||||
disabled=not current_user.has('settings_vendor.write')) }}
|
||||
{{ field('vendor_email', 'E-Mail', value=values.vendor_email, type='email',
|
||||
disabled=not current_user.has('settings_vendor.edit')) }}
|
||||
disabled=not current_user.has('settings_vendor.write')) }}
|
||||
{{ field('vendor_phone', 'Telefon', value=values.vendor_phone,
|
||||
disabled=not current_user.has('settings_vendor.edit')) }}
|
||||
disabled=not current_user.has('settings_vendor.write')) }}
|
||||
</div>
|
||||
{{ textarea_field('vendor_address', 'Adresse', value=values.vendor_address, rows=3) }}
|
||||
{{ field('master_endpoint', 'Öffentlicher Endpunkt', value=values.master_endpoint,
|
||||
placeholder='https://lizenz.firma.de',
|
||||
disabled=not current_user.has('settings_vendor.edit'),
|
||||
disabled=not current_user.has('settings_vendor.write'),
|
||||
hint_text='Basis-URL, unter der Kundeninstanzen diesen Server erreichen. Wird in
|
||||
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.',
|
||||
@@ -43,14 +43,14 @@ welche Basis-URL in kundengerichteten Links steht.', 'info') }}
|
||||
<p class="faint fs-sm">Kein Logo hinterlegt.</p>
|
||||
{% endif %}
|
||||
<input class="input" type="file" name="vendor_logo" accept="image/png,image/jpeg,image/svg+xml"
|
||||
{% if not current_user.has('settings_vendor.edit') %}disabled{% endif %}>
|
||||
{% if not current_user.has('settings_vendor.write') %}disabled{% endif %}>
|
||||
<span class="field__hint">
|
||||
Ein zu großes Logo wird abgelehnt, die übrigen Angaben werden trotzdem gespeichert.
|
||||
</span>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
{% if current_user.has('settings_vendor.edit') %}
|
||||
{% if current_user.has('settings_vendor.write') %}
|
||||
<div class="card__footer modal__footer--split">
|
||||
{% if values.vendor_logo_base64 %}
|
||||
<button class="btn btn--sm btn--ghost" type="submit"
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="card__body row row--tight">
|
||||
{% if current_user.has('licenses.create') %}
|
||||
{% if current_user.has('license_issue.execute') %}
|
||||
<form method="post" action="{{ url_for('licenses.issue', ticket_id=ticket.id) }}"
|
||||
data-no-guard>
|
||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||
@@ -163,7 +163,7 @@
|
||||
<label class="field">
|
||||
<span class="field__label">Lizenztyp</span>
|
||||
<select class="select" name="type" id="ticket-type"
|
||||
{% if not current_user.has('tickets.edit') %}disabled{% endif %}>
|
||||
{% if not current_user.has('tickets.write') %}disabled{% endif %}>
|
||||
{% for value in types %}
|
||||
<option value="{{ value }}" {% if value == ticket.type %}selected{% endif %}>
|
||||
{{ type_labels.get(value, value) }}
|
||||
@@ -178,15 +178,15 @@
|
||||
{% for module in modules %}
|
||||
{{ checkbox('modules', module_labels[module], value=module,
|
||||
checked=module in ticket_modules,
|
||||
disabled=not current_user.has('tickets.edit')) }}
|
||||
disabled=not current_user.has('tickets.write')) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
{{ field('valid_days', 'Laufzeit (Tage)', value=ticket.valid_days, type='number',
|
||||
disabled=not current_user.has('tickets.edit')) }}
|
||||
disabled=not current_user.has('tickets.write')) }}
|
||||
{{ checkbox('lifetime', 'Unbefristet', checked=ticket.lifetime,
|
||||
disabled=not current_user.has('tickets.edit')) }}
|
||||
disabled=not current_user.has('tickets.write')) }}
|
||||
{{ textarea_field('notes', 'Interne Notiz', value=ticket.notes, rows=2) }}
|
||||
<div class="divider"></div>
|
||||
<dl class="kv fs-xs">
|
||||
@@ -195,7 +195,7 @@
|
||||
<dt>Angelegt</dt><dd>{{ ticket.created_at | dt }} von {{ ticket.created_by or '-' }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
{% if current_user.has('tickets.edit') %}
|
||||
{% if current_user.has('tickets.write') %}
|
||||
<div class="card__footer">
|
||||
<button class="btn btn--primary" type="submit">Speichern</button>
|
||||
</div>
|
||||
@@ -203,13 +203,13 @@
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{% if current_user.has('tickets.edit') or current_user.has('tickets.delete') %}
|
||||
{% if current_user.has('tickets.write') or current_user.has('tickets.delete') %}
|
||||
<section class="card">
|
||||
<div class="card__header">
|
||||
<div class="flex-1"><div class="card__title">{{ icon('lock', size=17) }} Sperre und Löschung</div></div>
|
||||
</div>
|
||||
<div class="card__body stack stack--tight">
|
||||
{% if current_user.has('tickets.edit') %}
|
||||
{% if current_user.has('tickets.write') %}
|
||||
<form method="post" action="{{ url_for('tickets.toggle_block', ticket_id=ticket.id) }}"
|
||||
data-no-guard>
|
||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||
|
||||
Reference in new issue
Block a user