Version 2.8.0: Rechtekonzept -- eine Zeile je Funktion, Administrator heisst Verwaltung
This commit is contained in:
1 parent
713ff12db2
commit
0d99760d89
59 files changed
+1612
-398
No files matched your search
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tesm"
|
||||
version = "2.7.8"
|
||||
version = "2.8.0"
|
||||
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
||||
@@ -19,7 +19,7 @@ from tesm_core.extension import core
|
||||
from tesm_core.views import trash as trash_views
|
||||
|
||||
from .navigation import NAVIGATION
|
||||
from .permissions import PERMISSIONS, ROLE_PRESETS
|
||||
from .permissions import DHCP_PAGE, PERMISSIONS, ROLE_PRESETS
|
||||
from .schema import TESM_MIGRATIONS
|
||||
from .settings_spec import TESM_SETTINGS
|
||||
|
||||
@@ -205,7 +205,7 @@ def _status_pills(user: Any) -> list[dict[str, str]]:
|
||||
|
||||
# Timer bis zum naechsten Durchlauf, mit Knopf fuer "sofort pruefen". Der
|
||||
# Knopf gehoert in die Pille, weil er genau die Angabe daneben zuruecksetzt.
|
||||
if user.has_any(("devices.view", "monitoring.view")):
|
||||
if user.has_any(("devices.read", "monitoring.read")):
|
||||
intervall = int(extension.settings.get(verbindung, "monitor_interval_seconds") or 300)
|
||||
faellig = monitor.next_check_at(verbindung, intervall)
|
||||
if faellig is not None:
|
||||
@@ -237,7 +237,7 @@ def _status_pills(user: Any) -> list[dict[str, str]]:
|
||||
}
|
||||
)
|
||||
|
||||
if user.has_any(("dhcp.view", "dhcp.edit")) and extension.license is not None:
|
||||
if user.has_any(DHCP_PAGE) and extension.license is not None:
|
||||
if extension.license.module_active("dhcp") and extension.sysops is not None:
|
||||
state = extension.sysops.service_state(kea.SERVICE_UNIT)
|
||||
if state["installed"] == "ja":
|
||||
|
||||
@@ -37,14 +37,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():
|
||||
extension = core()
|
||||
conn = db()
|
||||
@@ -72,12 +72,12 @@ def overview():
|
||||
min_passphrase=transfer.MIN_PASSPHRASE_LENGTH,
|
||||
preview_ttl=transfer.PREVIEW_TTL_MINUTES,
|
||||
license_hint=license_hint,
|
||||
can_secrets=current_user.has("backup.secrets"),
|
||||
can_secrets=current_user.has("backup_export.secrets"),
|
||||
)
|
||||
|
||||
|
||||
@bp.post("/export")
|
||||
@permission_required("backup.export")
|
||||
@permission_required("backup_export.export")
|
||||
# Die frische Bestaetigung verlangt das Recht selbst (SENSITIVE_ACTIONS).
|
||||
def export():
|
||||
extension = core()
|
||||
@@ -96,7 +96,7 @@ def export():
|
||||
return redirect(url_for("backup.overview"))
|
||||
|
||||
include_secrets = (
|
||||
request.form.get("include_secrets") == "1" and current_user.has("backup.secrets")
|
||||
request.form.get("include_secrets") == "1" and current_user.has("backup_export.secrets")
|
||||
)
|
||||
passphrase = request.form.get("passphrase") or ""
|
||||
repeat = request.form.get("passphrase_repeat") or ""
|
||||
@@ -130,7 +130,7 @@ def export():
|
||||
|
||||
|
||||
@bp.post("/import/preview")
|
||||
@permission_required("backup.execute")
|
||||
@permission_required("backup_import.execute")
|
||||
@reauth_required
|
||||
def import_preview():
|
||||
conn = db()
|
||||
@@ -162,7 +162,7 @@ def import_preview():
|
||||
|
||||
|
||||
@bp.post("/import/apply")
|
||||
@permission_required("backup.execute")
|
||||
@permission_required("backup_import.execute")
|
||||
@reauth_required
|
||||
def import_apply():
|
||||
extension = core()
|
||||
@@ -226,7 +226,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,7 +29,7 @@ def _audit(conn: Any, action: str, target: str, detail: str = "", severity: str
|
||||
|
||||
|
||||
@bp.get("/")
|
||||
@permission_required("credentials.view")
|
||||
@permission_required("credentials.read")
|
||||
def overview():
|
||||
conn = db()
|
||||
return render_template(
|
||||
@@ -62,7 +62,7 @@ def create():
|
||||
|
||||
|
||||
@bp.post("/<int:credential_id>")
|
||||
@permission_required("credentials.edit")
|
||||
@permission_required("credentials.write")
|
||||
def update(credential_id: int):
|
||||
try:
|
||||
with transaction() as conn:
|
||||
|
||||
@@ -43,7 +43,7 @@ def _tiles(*, authenticated: bool) -> dict[str, Any]:
|
||||
counts = monitor.dashboard_counts(conn)
|
||||
rows = inventory.list_devices(conn, only_active=not authenticated)
|
||||
|
||||
detailed = authenticated and current_user.has("devices.view")
|
||||
detailed = authenticated and current_user.has("devices.read")
|
||||
tiles: list[dict[str, Any]] = []
|
||||
for row in rows:
|
||||
entry: dict[str, Any] = {
|
||||
@@ -67,8 +67,12 @@ def _tiles(*, authenticated: bool) -> dict[str, Any]:
|
||||
"credential_category": str(row["credential_category"] or ""),
|
||||
# Nicht nur PoE: ein Geraet kann auch ueber SSH oder RPC
|
||||
# neu startbar sein. Die Kachel zeigt den bevorzugten Weg.
|
||||
# Mit Rechtefilter: die Kachel zeigt nur Wege, die
|
||||
# dieser Benutzer auch ausloesen darf.
|
||||
"restart_methods": restart.available(
|
||||
row, maintenance_active=_maintenance_active()
|
||||
row,
|
||||
maintenance_active=_maintenance_active(),
|
||||
darf=current_user.has,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -60,7 +60,7 @@ def _form_values() -> dict[str, Any]:
|
||||
|
||||
|
||||
@bp.get("/")
|
||||
@permission_required("devices.view")
|
||||
@permission_required("devices.read")
|
||||
def overview():
|
||||
conn = db()
|
||||
devices = inventory.list_devices(conn)
|
||||
@@ -138,7 +138,7 @@ def create():
|
||||
|
||||
|
||||
@bp.post("/<int:device_id>")
|
||||
@permission_required("devices.edit")
|
||||
@permission_required("devices.write")
|
||||
def update(device_id: int):
|
||||
try:
|
||||
with transaction() as conn:
|
||||
@@ -162,7 +162,7 @@ def update(device_id: int):
|
||||
|
||||
|
||||
@bp.post("/<int:device_id>/active")
|
||||
@permission_required("devices.edit")
|
||||
@permission_required("devices.write")
|
||||
def toggle_active(device_id: int):
|
||||
active = request.form.get("active") == "1"
|
||||
with transaction() as conn:
|
||||
@@ -194,7 +194,12 @@ def delete(device_id: int):
|
||||
|
||||
|
||||
@bp.post("/<int:device_id>/restart")
|
||||
@permission_required("devices.execute")
|
||||
# Die Seite darf sehen, wer Clients sehen darf; welcher Weg offensteht,
|
||||
# entscheidet das Recht des Weges -- geprueft in restart.resolve() ueber
|
||||
# "darf". Ein Weg, den der Benutzer nicht ausloesen darf, existiert fuer ihn
|
||||
# nicht, und die Antwort ist dieselbe wie bei einem technisch unmoeglichen:
|
||||
# "dieser Weg steht nicht zur Verfuegung".
|
||||
@permission_required("devices.read")
|
||||
def restart_device(device_id: int):
|
||||
"""Startet ein Geraet neu -- ueber den Weg, der dafuer gewaehlt wurde.
|
||||
|
||||
@@ -214,9 +219,12 @@ def restart_device(device_id: int):
|
||||
dict(device),
|
||||
(request.form.get("method") or "").strip(),
|
||||
maintenance_active=maintenance_active,
|
||||
darf=current_user.has,
|
||||
)
|
||||
if method is None:
|
||||
options = restart.available(dict(device), maintenance_active=maintenance_active)
|
||||
options = restart.available(
|
||||
dict(device), maintenance_active=maintenance_active, darf=current_user.has
|
||||
)
|
||||
if options:
|
||||
flash(
|
||||
"Der gewählte Weg steht für dieses Gerät nicht zur Verfügung. "
|
||||
@@ -326,7 +334,7 @@ def _restart_as_job(extension: Any, conn: Any, device: Any, method: Any): # typ
|
||||
|
||||
|
||||
@bp.get("/<int:device_id>")
|
||||
@permission_required("devices.view")
|
||||
@permission_required("devices.read")
|
||||
def detail(device_id: int):
|
||||
conn = db()
|
||||
device = inventory.get_device(conn, device_id)
|
||||
@@ -361,7 +369,7 @@ def detail(device_id: int):
|
||||
|
||||
|
||||
@bp.post("/<int:device_id>/host-key/probe")
|
||||
@permission_required("devices.edit")
|
||||
@permission_required("devices.write")
|
||||
def probe_host_key(device_id: int):
|
||||
"""Liest den Host-Schluessel des Clients, ohne sich anzumelden.
|
||||
|
||||
@@ -391,7 +399,7 @@ def probe_host_key(device_id: int):
|
||||
|
||||
|
||||
@bp.post("/<int:device_id>/host-key/trust")
|
||||
@permission_required("devices.edit")
|
||||
@permission_required("devices.write")
|
||||
@reauth_required
|
||||
def trust_host_key(device_id: int):
|
||||
conn = db()
|
||||
@@ -425,7 +433,7 @@ def trust_host_key(device_id: int):
|
||||
|
||||
|
||||
@bp.post("/<int:device_id>/host-key/forget")
|
||||
@permission_required("devices.edit")
|
||||
@permission_required("devices.write")
|
||||
def forget_host_key(device_id: int):
|
||||
conn = db()
|
||||
device = inventory.get_device(conn, device_id)
|
||||
|
||||
@@ -21,6 +21,7 @@ from tesm_core.db import Database
|
||||
from tesm_core.extension import core, db, transaction
|
||||
|
||||
from ..services import kea
|
||||
from ..permissions import DHCP_PAGE
|
||||
|
||||
bp = Blueprint("dhcp", __name__, url_prefix="/dhcp")
|
||||
|
||||
@@ -53,7 +54,7 @@ def _settings_values() -> dict[str, Any]:
|
||||
|
||||
|
||||
@bp.get("/")
|
||||
@permission_required("dhcp.view", "dhcp.edit", "dhcp.execute", mode="any")
|
||||
@permission_required(*DHCP_PAGE, mode="any")
|
||||
def overview():
|
||||
extension = core()
|
||||
conn = db()
|
||||
@@ -106,7 +107,7 @@ def overview():
|
||||
|
||||
|
||||
@bp.post("/settings")
|
||||
@permission_required("dhcp.edit")
|
||||
@permission_required("dhcp_subnets.write")
|
||||
def save_settings():
|
||||
extension = core()
|
||||
conn = db()
|
||||
@@ -123,7 +124,7 @@ def save_settings():
|
||||
|
||||
|
||||
@bp.post("/subnet")
|
||||
@permission_required("dhcp.edit")
|
||||
@permission_required("dhcp_subnets.create")
|
||||
def add_subnet():
|
||||
extension = core()
|
||||
conn = db()
|
||||
@@ -159,7 +160,7 @@ def add_subnet():
|
||||
|
||||
|
||||
@bp.post("/subnet/<int:subnet_id>")
|
||||
@permission_required("dhcp.edit")
|
||||
@permission_required("dhcp_subnets.write")
|
||||
def update_subnet(subnet_id: int):
|
||||
extension = core()
|
||||
conn = db()
|
||||
@@ -217,7 +218,7 @@ def update_subnet(subnet_id: int):
|
||||
|
||||
|
||||
@bp.post("/subnet/<int:subnet_id>/delete")
|
||||
@permission_required("dhcp.edit")
|
||||
@permission_required("dhcp_subnets.delete")
|
||||
def delete_subnet(subnet_id: int):
|
||||
with transaction() as conn:
|
||||
conn.execute("DELETE FROM dhcp_subnets WHERE id=?", (subnet_id,))
|
||||
@@ -232,7 +233,7 @@ def delete_subnet(subnet_id: int):
|
||||
|
||||
|
||||
@bp.post("/option/value")
|
||||
@permission_required("dhcp.edit")
|
||||
@permission_required("dhcp_options.write")
|
||||
def set_option_value():
|
||||
conn = db()
|
||||
option_id = int(request.form.get("option_id") or 0)
|
||||
@@ -271,7 +272,7 @@ def _device_option_values(conn) -> dict[int, list[dict[str, Any]]]: # type: ign
|
||||
|
||||
|
||||
@bp.post("/option/device")
|
||||
@permission_required("dhcp.edit")
|
||||
@permission_required("dhcp_options.write")
|
||||
def set_device_option_value():
|
||||
"""Setzt eine Option fuer ein einzelnes Geraet.
|
||||
|
||||
@@ -319,7 +320,7 @@ def set_device_option_value():
|
||||
|
||||
|
||||
@bp.post("/option/new")
|
||||
@permission_required("dhcp.edit")
|
||||
@permission_required("dhcp_options.write")
|
||||
def add_custom_option():
|
||||
conn = db()
|
||||
try:
|
||||
@@ -350,7 +351,7 @@ def add_custom_option():
|
||||
|
||||
|
||||
@bp.post("/option/<int:option_id>/delete")
|
||||
@permission_required("dhcp.edit")
|
||||
@permission_required("dhcp_options.write")
|
||||
def delete_custom_option(option_id: int):
|
||||
conn = db()
|
||||
option = Database.one(conn, "SELECT * FROM dhcp_options WHERE id=?", (option_id,))
|
||||
@@ -375,7 +376,7 @@ def delete_custom_option(option_id: int):
|
||||
|
||||
|
||||
@bp.post("/reservation")
|
||||
@permission_required("dhcp.edit")
|
||||
@permission_required("dhcp_reservations.create")
|
||||
def add_reservation():
|
||||
conn = db()
|
||||
mac = kea.normalize_mac(request.form.get("mac", ""))
|
||||
@@ -447,7 +448,7 @@ def add_reservation():
|
||||
|
||||
|
||||
@bp.post("/reservation/<int:reservation_id>/delete")
|
||||
@permission_required("dhcp.edit")
|
||||
@permission_required("dhcp_reservations.delete")
|
||||
def delete_reservation(reservation_id: int):
|
||||
with transaction() as conn:
|
||||
conn.execute("DELETE FROM dhcp_reservations WHERE id=?", (reservation_id,))
|
||||
@@ -515,13 +516,13 @@ def _write_and_redirect():
|
||||
|
||||
|
||||
@bp.post("/write")
|
||||
@permission_required("dhcp.edit")
|
||||
@permission_required("dhcp_config.execute")
|
||||
def write_config():
|
||||
return _write_and_redirect()
|
||||
|
||||
|
||||
@bp.post("/service/<action>")
|
||||
@permission_required("dhcp.execute")
|
||||
@permission_required("dhcp_service.execute")
|
||||
def service_action(action: str):
|
||||
allowed = {"install", "start", "stop"}
|
||||
if action not in allowed:
|
||||
|
||||
@@ -84,7 +84,7 @@ def _mount_or_404(token: str) -> service.Mount:
|
||||
|
||||
|
||||
@bp.get("/")
|
||||
@permission_required("fileshare.view")
|
||||
@permission_required("fileshare.read")
|
||||
def browse():
|
||||
conn = db()
|
||||
mounts = service.doppelte_zusammenfassen(
|
||||
@@ -148,7 +148,7 @@ def download(token: str):
|
||||
|
||||
|
||||
@bp.get("/<token>/preview")
|
||||
@permission_required("fileshare.view")
|
||||
@permission_required("fileshare.read")
|
||||
def preview(token: str):
|
||||
mount = _mount_or_404(token)
|
||||
relative = request.args.get("path") or ""
|
||||
@@ -221,7 +221,7 @@ def upload(token: str):
|
||||
|
||||
|
||||
@bp.get("/<token>/signature")
|
||||
@permission_required("fileshare.view")
|
||||
@permission_required("fileshare.read")
|
||||
def signature_state(token: str):
|
||||
"""Passt die Datei noch zu ihrem Eintrag im Aenderungsprotokoll?
|
||||
|
||||
@@ -237,7 +237,7 @@ def signature_state(token: str):
|
||||
|
||||
|
||||
@bp.get("/<token>/folders")
|
||||
@permission_required("fileshare.view")
|
||||
@permission_required("fileshare.read")
|
||||
def folders(token: str):
|
||||
"""Die Ordner **dieser** Freigabe als Vorschlagsliste.
|
||||
|
||||
@@ -299,7 +299,7 @@ def save_form(token: str):
|
||||
# Formularfelder, ist sie weiter veraenderbar -- dafuer braucht es
|
||||
# "PDF-Formulare: Bearbeiten". Ein umgebogenes Feld im Browser hilft damit
|
||||
# niemandem weiter.
|
||||
if service.hat_formularfelder(daten) and not current_user.has("fileshare_pdf.edit"):
|
||||
if service.hat_formularfelder(daten) and not current_user.has("fileshare_pdf.write"):
|
||||
return {
|
||||
"error": "Sie dürfen ausgefüllte Formulare nur abgeschlossen speichern "
|
||||
"(Felder gesperrt). Bitte im Auswahlfeld 'Abgeschlossen' wählen."
|
||||
@@ -351,6 +351,11 @@ def save_form(token: str):
|
||||
wohin = ziel.relative_to(ziel_mount.path.resolve()).as_posix()
|
||||
pruefsumme = hashlib.sha256(daten).hexdigest()
|
||||
unterschriften = _unterschriften(request.form.get("unterschriften") or "")
|
||||
# Unterschreiben ist kein Aendern: am Ende steht eine Fassung, die niemand
|
||||
# mehr bearbeiten kann, mit Namen im Dokument und Pruefsumme im Protokoll.
|
||||
# Wer Dateien ablegen darf, darf das nicht automatisch.
|
||||
if unterschriften and not current_user.has("fileshare_signature.execute"):
|
||||
return {"error": "Zum Unterschreiben fehlt die Berechtigung."}, 403
|
||||
|
||||
# Zwei Angaben, die man nicht verwechseln darf, und deshalb stehen beide
|
||||
# da: der Name **in** der Unterschrift ist eine Angabe des
|
||||
@@ -412,7 +417,7 @@ def make_directory(token: str):
|
||||
|
||||
|
||||
@bp.post("/<token>/rename")
|
||||
@permission_required("fileshare.edit")
|
||||
@permission_required("fileshare.write")
|
||||
def rename(token: str):
|
||||
mount = _mount_or_404(token)
|
||||
relative = request.form.get("path") or ""
|
||||
@@ -446,7 +451,7 @@ def delete(token: str):
|
||||
|
||||
|
||||
@bp.post("/<token>/release")
|
||||
@permission_required("fileshare.view")
|
||||
@permission_required("fileshare.read")
|
||||
def release(token: str):
|
||||
mount = _mount_or_404(token)
|
||||
with transaction() as conn:
|
||||
@@ -462,7 +467,7 @@ def release(token: str):
|
||||
|
||||
|
||||
@bp.get("/mappings")
|
||||
@permission_required("fileshare.edit")
|
||||
@permission_required("fileshare_mappings.read")
|
||||
def mappings():
|
||||
from tesm_core.auth import directory
|
||||
|
||||
@@ -476,7 +481,7 @@ def mappings():
|
||||
|
||||
|
||||
@bp.post("/mappings")
|
||||
@permission_required("fileshare.edit")
|
||||
@permission_required("fileshare_mappings.create")
|
||||
def mapping_add():
|
||||
try:
|
||||
with transaction() as conn:
|
||||
@@ -506,7 +511,7 @@ def mapping_add():
|
||||
|
||||
|
||||
@bp.post("/mappings/<int:mapping_id>")
|
||||
@permission_required("fileshare.edit")
|
||||
@permission_required("fileshare_mappings.write")
|
||||
def mapping_update(mapping_id: int):
|
||||
try:
|
||||
with transaction() as conn:
|
||||
@@ -536,7 +541,7 @@ def mapping_update(mapping_id: int):
|
||||
|
||||
|
||||
@bp.post("/mappings/<int:mapping_id>/delete")
|
||||
@permission_required("fileshare.edit")
|
||||
@permission_required("fileshare_mappings.delete")
|
||||
def mapping_delete(mapping_id: int):
|
||||
with transaction() as conn:
|
||||
entfernt = service.get_mapping(conn, mapping_id)
|
||||
|
||||
@@ -36,8 +36,12 @@ bp = Blueprint("hostkeys", __name__, url_prefix="/monitoring/host-keys")
|
||||
KIND_DEVICE = "client"
|
||||
KIND_SWITCH = "switch"
|
||||
|
||||
#: Welches Recht welche Art von Ziel freigibt.
|
||||
EDIT_RIGHT = {KIND_DEVICE: "devices.edit", KIND_SWITCH: "switches.edit"}
|
||||
#: Freigeben ist eine eigene Zeile: es war das Recht fuer Stammdaten
|
||||
#: ("Gerät ändern"), obwohl es der Vorgang ist, mit dem man bestaetigt, dass
|
||||
#: ein Geraet wirklich das ist, fuer das es sich ausgibt. Die Art des Ziels
|
||||
#: entscheidet nicht mehr mit -- Schluessel ist Schluessel.
|
||||
FREIGABE_RECHT = "hostkeys.execute"
|
||||
EDIT_RIGHT = {KIND_DEVICE: FREIGABE_RECHT, KIND_SWITCH: FREIGABE_RECHT}
|
||||
|
||||
|
||||
def _audit(action: str, target: str, detail: str = "", severity: str = "info") -> None:
|
||||
@@ -112,7 +116,7 @@ def _orphans(conn: Any, targets: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
|
||||
|
||||
@bp.get("/")
|
||||
@permission_required("devices.view", "switches.view", mode="any")
|
||||
@permission_required("hostkeys.read")
|
||||
def overview():
|
||||
conn = db()
|
||||
targets = _targets(conn)
|
||||
@@ -143,7 +147,7 @@ def _require(kind: str) -> None:
|
||||
|
||||
|
||||
@bp.post("/<kind>/<int:target_id>/probe")
|
||||
@permission_required("devices.view", "switches.view", mode="any")
|
||||
@permission_required("hostkeys.read")
|
||||
def probe(kind: str, target_id: int):
|
||||
conn = db()
|
||||
_require(kind)
|
||||
@@ -170,7 +174,7 @@ def probe(kind: str, target_id: int):
|
||||
|
||||
|
||||
@bp.post("/<kind>/<int:target_id>/trust")
|
||||
@permission_required("devices.view", "switches.view", mode="any")
|
||||
@permission_required("hostkeys.read")
|
||||
@reauth_required
|
||||
def trust(kind: str, target_id: int):
|
||||
conn = db()
|
||||
@@ -204,7 +208,7 @@ def trust(kind: str, target_id: int):
|
||||
|
||||
|
||||
@bp.post("/<kind>/<int:target_id>/forget")
|
||||
@permission_required("devices.view", "switches.view", mode="any")
|
||||
@permission_required("hostkeys.read")
|
||||
def forget(kind: str, target_id: int):
|
||||
conn = db()
|
||||
_require(kind)
|
||||
@@ -223,7 +227,7 @@ def forget(kind: str, target_id: int):
|
||||
|
||||
|
||||
@bp.post("/orphaned/forget")
|
||||
@permission_required("devices.edit", "switches.edit", mode="any")
|
||||
@permission_required("hostkeys.execute")
|
||||
def forget_orphan():
|
||||
"""Entfernt eine Freigabe, zu der es kein Ziel mehr gibt."""
|
||||
host = (request.form.get("host") or "").strip()
|
||||
|
||||
@@ -43,7 +43,7 @@ def _audit(action: str, target: str = "", detail: str = "", severity: str = "inf
|
||||
|
||||
|
||||
@bp.get("/")
|
||||
@permission_required("settings_license.view")
|
||||
@permission_required("settings_license.read")
|
||||
def overview():
|
||||
extension = core()
|
||||
conn = db()
|
||||
@@ -70,7 +70,7 @@ def overview():
|
||||
|
||||
|
||||
@bp.post("/install")
|
||||
@permission_required("settings_license.edit")
|
||||
@permission_required("settings_license.write")
|
||||
def upload():
|
||||
extension = core()
|
||||
manager = extension.license
|
||||
@@ -111,13 +111,13 @@ def upload():
|
||||
|
||||
|
||||
@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():
|
||||
"""Gibt die Lizenz frei, damit sie auf einer anderen Maschine genutzt werden kann.
|
||||
@@ -129,7 +129,7 @@ def deactivate():
|
||||
|
||||
|
||||
@bp.post("/heartbeat")
|
||||
@permission_required("settings_license.edit")
|
||||
@permission_required("settings_license.write")
|
||||
def heartbeat():
|
||||
return _handshake("heartbeat")
|
||||
|
||||
@@ -159,7 +159,7 @@ def _handshake(action: str):
|
||||
|
||||
|
||||
@bp.post("/offline-response")
|
||||
@permission_required("settings_license.edit")
|
||||
@permission_required("settings_license.write")
|
||||
def offline_response():
|
||||
extension = core()
|
||||
manager = extension.license
|
||||
@@ -178,7 +178,7 @@ def offline_response():
|
||||
|
||||
|
||||
@bp.post("/server-certificate")
|
||||
@permission_required("settings_license.edit")
|
||||
@permission_required("settings_license.write")
|
||||
def server_certificate():
|
||||
"""Hinterlegt das Zertifikat des Lizenzservers.
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ MAX_TAIL_LINES = 2000
|
||||
|
||||
|
||||
@bp.get("/dhcp")
|
||||
@permission_required("logs_dhcp.view")
|
||||
@permission_required("logs_dhcp.read")
|
||||
def dhcp():
|
||||
extension = core()
|
||||
if extension.license is not None and not extension.license.module_active("dhcp"):
|
||||
@@ -39,7 +39,7 @@ def dhcp():
|
||||
|
||||
|
||||
@bp.get("/restarts")
|
||||
@permission_required("devices.view")
|
||||
@permission_required("devices.read")
|
||||
def restarts():
|
||||
"""Alle Neustarts -- ueber den PoE-Port und per Befehl aus der Wartung.
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ from tesm_core.auth.service import client_ip
|
||||
from tesm_core.db import Database
|
||||
from tesm_core.extension import core, db, transaction
|
||||
|
||||
from ..permissions import SERVICING_PAGE
|
||||
from ..services import inventory, maintenance as service, ssh, windowsops, monitor
|
||||
|
||||
bp = Blueprint("maintenance", __name__, url_prefix="/maintenance")
|
||||
@@ -75,7 +76,7 @@ def _query_devices(conn) -> list[dict]: # type: ignore[no-untyped-def]
|
||||
|
||||
|
||||
@bp.get("/")
|
||||
@permission_required("maintenance.view", "maintenance.execute", mode="any")
|
||||
@permission_required(*SERVICING_PAGE, mode="any")
|
||||
def overview():
|
||||
conn = db()
|
||||
return render_template(
|
||||
@@ -106,7 +107,7 @@ def _windows_tool_missing(conn) -> bool: # type: ignore[no-untyped-def]
|
||||
|
||||
|
||||
@bp.post("/install-package")
|
||||
@permission_required("maintenance.execute")
|
||||
@permission_required("servicing_tool.execute")
|
||||
def install_windows_tool():
|
||||
"""Installiert das Paket, das der Fehlermeldung nach fehlt.
|
||||
|
||||
@@ -139,7 +140,7 @@ def install_windows_tool():
|
||||
|
||||
|
||||
@bp.get("/status")
|
||||
@permission_required("maintenance.view", "maintenance.execute", mode="any")
|
||||
@permission_required(*SERVICING_PAGE, mode="any")
|
||||
def status():
|
||||
"""Zustand der Auftraege -- aus der Datenbank, nicht aus dem Prozessspeicher."""
|
||||
conn = db()
|
||||
@@ -166,7 +167,7 @@ def status():
|
||||
|
||||
|
||||
@bp.get("/job/<job_id>")
|
||||
@permission_required("maintenance.view", "maintenance.execute", mode="any")
|
||||
@permission_required(*SERVICING_PAGE, mode="any")
|
||||
def job_detail(job_id: str):
|
||||
conn = db()
|
||||
job = service.get_job(conn, job_id)
|
||||
@@ -176,7 +177,7 @@ def job_detail(job_id: str):
|
||||
|
||||
|
||||
@bp.get("/job/<job_id>/output")
|
||||
@permission_required("maintenance.view", "maintenance.execute", mode="any")
|
||||
@permission_required(*SERVICING_PAGE, mode="any")
|
||||
@fragment
|
||||
def job_output(job_id: str):
|
||||
conn = db()
|
||||
@@ -277,14 +278,14 @@ def _start(kind: str) -> None:
|
||||
|
||||
|
||||
@bp.post("/update")
|
||||
@permission_required("maintenance.execute")
|
||||
@permission_required("servicing_update.execute")
|
||||
def start_update():
|
||||
_start(service.JOB_KIND_UPDATE)
|
||||
return redirect(url_for("maintenance.overview"))
|
||||
|
||||
|
||||
@bp.post("/restart")
|
||||
@permission_required("maintenance.execute")
|
||||
@permission_required("servicing_reboot.execute")
|
||||
def start_reboot():
|
||||
_start(service.JOB_KIND_REBOOT)
|
||||
return redirect(url_for("maintenance.overview"))
|
||||
@@ -65,7 +65,7 @@ def _timezone_choices() -> list[str]:
|
||||
|
||||
|
||||
@bp.get("/system")
|
||||
@permission_required("settings_system.view")
|
||||
@permission_required("settings_system.read")
|
||||
def system():
|
||||
extension = core()
|
||||
conn = db()
|
||||
@@ -103,7 +103,7 @@ def system():
|
||||
|
||||
|
||||
@bp.post("/system")
|
||||
@permission_required("settings_system.edit")
|
||||
@permission_required("settings_system.write")
|
||||
def system_save():
|
||||
extension = core()
|
||||
conn = db()
|
||||
|
||||
@@ -47,7 +47,7 @@ def _form_values() -> dict[str, Any]:
|
||||
|
||||
|
||||
@bp.get("/")
|
||||
@permission_required("switches.view")
|
||||
@permission_required("switches.read")
|
||||
def overview():
|
||||
conn = db()
|
||||
return render_template(
|
||||
@@ -79,7 +79,7 @@ def create():
|
||||
|
||||
|
||||
@bp.post("/<int:switch_id>")
|
||||
@permission_required("switches.edit")
|
||||
@permission_required("switches.write")
|
||||
def update(switch_id: int):
|
||||
try:
|
||||
with transaction() as conn:
|
||||
@@ -115,7 +115,7 @@ def delete(switch_id: int):
|
||||
|
||||
|
||||
@bp.get("/<int:switch_id>")
|
||||
@permission_required("switches.view")
|
||||
@permission_required("switches.read")
|
||||
def detail(switch_id: int):
|
||||
conn = db()
|
||||
switch = inventory.get_switch(conn, switch_id)
|
||||
@@ -152,7 +152,7 @@ def detail(switch_id: int):
|
||||
|
||||
|
||||
@bp.post("/<int:switch_id>/host-key/probe")
|
||||
@permission_required("switches.edit")
|
||||
@permission_required("switches.write")
|
||||
def probe_host_key(switch_id: int):
|
||||
conn = db()
|
||||
switch = inventory.get_switch(conn, switch_id)
|
||||
@@ -177,7 +177,7 @@ def probe_host_key(switch_id: int):
|
||||
|
||||
|
||||
@bp.post("/<int:switch_id>/host-key/trust")
|
||||
@permission_required("switches.edit")
|
||||
@permission_required("switches.write")
|
||||
@reauth_required
|
||||
def trust_host_key(switch_id: int):
|
||||
"""Gibt einen Host-Schluessel ausdruecklich frei.
|
||||
@@ -216,7 +216,7 @@ def trust_host_key(switch_id: int):
|
||||
|
||||
|
||||
@bp.post("/<int:switch_id>/host-key/forget")
|
||||
@permission_required("switches.edit")
|
||||
@permission_required("switches.write")
|
||||
def forget_host_key(switch_id: int):
|
||||
conn = db()
|
||||
switch = inventory.get_switch(conn, switch_id)
|
||||
@@ -235,7 +235,7 @@ def forget_host_key(switch_id: int):
|
||||
|
||||
|
||||
@bp.post("/<int:switch_id>/command")
|
||||
@permission_required("switches.execute")
|
||||
@permission_required("switch_console.execute")
|
||||
@reauth_required
|
||||
def run_command(switch_id: int):
|
||||
"""Fuehrt ein Kommando aus der Positivliste aus.
|
||||
|
||||
@@ -12,7 +12,7 @@ Zwei Ziele, ein Ablauf:
|
||||
|
||||
* **Switch** -- ``switches.execute``. Interaktive ArubaOS-Sitzung ueber
|
||||
:class:`~tesm.services.switchcli.SwitchCli`.
|
||||
* **Client** -- ``maintenance.execute`` und das lizenzpflichtige Modul
|
||||
* **Client** -- ``device_console.execute`` und das lizenzpflichtige Modul
|
||||
``maintenance``. Ein Kommando je ``exec``-Kanal ueber
|
||||
:class:`~tesm.services.shellcli.ShellSession`.
|
||||
|
||||
@@ -167,7 +167,7 @@ def _render_console(target: Target): # type: ignore[no-untyped-def]
|
||||
|
||||
|
||||
@bp.get("/monitoring/switches/<int:switch_id>/console")
|
||||
@permission_required("switches.execute")
|
||||
@permission_required("switch_console.execute")
|
||||
@reauth_required
|
||||
def switch_console(switch_id: int):
|
||||
target = _load_target(db(), KIND_SWITCH, switch_id)
|
||||
@@ -177,7 +177,7 @@ def switch_console(switch_id: int):
|
||||
|
||||
|
||||
@bp.get("/monitoring/clients/<int:device_id>/console")
|
||||
@permission_required("maintenance.execute")
|
||||
@permission_required("device_console.execute")
|
||||
@reauth_required
|
||||
def device_console(device_id: int):
|
||||
if not _client_module_active():
|
||||
@@ -225,7 +225,9 @@ def _handle_socket(ws, kind: str, target_id: int) -> None: # type: ignore[no-un
|
||||
|
||||
# -- Berechtigungen: dieselben Pruefungen wie in der HTTP-Route. Ein
|
||||
# WebSocket umgeht Dekoratoren, also stehen sie hier ausdruecklich.
|
||||
required = "switches.execute" if kind == KIND_SWITCH else "maintenance.execute"
|
||||
required = (
|
||||
"switch_console.execute" if kind == KIND_SWITCH else "device_console.execute"
|
||||
)
|
||||
if not current_user.is_authenticated or not current_user.has(required):
|
||||
_send(ws, "error", "Keine Berechtigung für diese Konsole.")
|
||||
return
|
||||
|
||||
@@ -13,6 +13,8 @@ from typing import Any
|
||||
from tesm_core.extension import core
|
||||
from tesm_core.nav import NavItem, NavTree
|
||||
|
||||
from .permissions import DHCP_PAGE, SERVICING_PAGE
|
||||
|
||||
|
||||
def _module(name: str):
|
||||
"""Bedingung: Vollmodul lizenziert?"""
|
||||
@@ -36,7 +38,7 @@ def _has_fileshare_mounts(user: Any) -> bool:
|
||||
return False
|
||||
if not _module("fileshare")(user):
|
||||
return False
|
||||
if user.has("fileshare.edit"):
|
||||
if user.has("fileshare.write"):
|
||||
return True
|
||||
from .services import fileshare
|
||||
|
||||
@@ -56,28 +58,28 @@ NAVIGATION = NavTree(
|
||||
label="Clients",
|
||||
icon="cpu",
|
||||
endpoint="devices.overview",
|
||||
permissions=("devices.view",),
|
||||
permissions=("devices.read",),
|
||||
),
|
||||
NavItem(
|
||||
key="switches",
|
||||
label="Switche",
|
||||
icon="switch",
|
||||
endpoint="switches.overview",
|
||||
permissions=("switches.view",),
|
||||
permissions=("switches.read",),
|
||||
),
|
||||
NavItem(
|
||||
key="credentials",
|
||||
label="Zugangsdaten",
|
||||
icon="key",
|
||||
endpoint="credentials.overview",
|
||||
permissions=("credentials.view",),
|
||||
permissions=("credentials.read",),
|
||||
),
|
||||
NavItem(
|
||||
key="hostkeys",
|
||||
label="Host-Schlüssel",
|
||||
icon="shield",
|
||||
endpoint="hostkeys.overview",
|
||||
permissions=("devices.view", "switches.view"),
|
||||
permissions=("devices.read", "switches.read"),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -86,7 +88,7 @@ NAVIGATION = NavTree(
|
||||
label="Wartung",
|
||||
icon="wrench",
|
||||
endpoint="maintenance.overview",
|
||||
permissions=("maintenance.view", "maintenance.execute"),
|
||||
permissions=SERVICING_PAGE,
|
||||
condition=_module("maintenance"),
|
||||
),
|
||||
NavItem(
|
||||
@@ -94,7 +96,7 @@ NAVIGATION = NavTree(
|
||||
label="DHCP-Server",
|
||||
icon="network",
|
||||
endpoint="dhcp.overview",
|
||||
permissions=("dhcp.view", "dhcp.edit", "dhcp.execute"),
|
||||
permissions=DHCP_PAGE,
|
||||
condition=_module("dhcp"),
|
||||
),
|
||||
NavItem(
|
||||
@@ -108,14 +110,14 @@ NAVIGATION = NavTree(
|
||||
label="Meine Freigaben",
|
||||
icon="folder",
|
||||
endpoint="fileshare.browse",
|
||||
permissions=("fileshare.view",),
|
||||
permissions=("fileshare.read",),
|
||||
),
|
||||
NavItem(
|
||||
key="fileshare_mappings",
|
||||
label="Zuordnungen",
|
||||
icon="link",
|
||||
endpoint="fileshare.mappings",
|
||||
permissions=("fileshare.edit",),
|
||||
permissions=("fileshare.write",),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -129,28 +131,28 @@ 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"),
|
||||
),
|
||||
NavItem(
|
||||
key="logs_dhcp",
|
||||
label="DHCP",
|
||||
icon="network",
|
||||
endpoint="tesmlogs.dhcp",
|
||||
permissions=("logs_dhcp.view",),
|
||||
permissions=("logs_dhcp.read",),
|
||||
condition=_module("dhcp"),
|
||||
),
|
||||
NavItem(
|
||||
@@ -158,7 +160,7 @@ NAVIGATION = NavTree(
|
||||
label="Neustarts",
|
||||
icon="power",
|
||||
endpoint="tesmlogs.restarts",
|
||||
permissions=("devices.view",),
|
||||
permissions=("devices.read",),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -172,42 +174,42 @@ 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_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",
|
||||
@@ -216,28 +218,28 @@ NAVIGATION = NavTree(
|
||||
endpoint="license.overview",
|
||||
# Bewusst immer erreichbar, auch ohne gueltige Lizenz --
|
||||
# sonst liesse sich eine neue Lizenz nie einspielen.
|
||||
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",),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -30,67 +30,219 @@ A = Action
|
||||
DEVICES = Resource(
|
||||
key="devices",
|
||||
label="Clients",
|
||||
actions=(A.VIEW, A.CREATE, A.EDIT, A.DELETE, A.EXECUTE),
|
||||
hint="Überwachte PoE-Endgeräte",
|
||||
actions=(A.READ, A.CREATE, A.WRITE, A.DELETE),
|
||||
hint="Stammdaten überwachter PoE-Endgeräte: Name, MAC, IP, Switch und Port",
|
||||
action_hints={
|
||||
A.EXECUTE: "PoE-Neustart eines Geräts manuell auslösen. Der automatische "
|
||||
"Neustart bei Ausfall ist davon unabhängig und läuft immer.",
|
||||
A.DELETE: "Gerät in den Papierkorb legen.",
|
||||
},
|
||||
)
|
||||
|
||||
#: Drei Wege, drei Rechte -- bisher hingen alle an ``devices.execute``.
|
||||
#:
|
||||
#: Wer jemandem den PoE-Neustart erlauben wollte (der Weg, der auch bei einem
|
||||
#: haengenden Geraet noch wirkt und nichts Laufendes unterbricht), erlaubte ihm
|
||||
#: damit zugleich, ein arbeitendes System ueber SSH herunterzufahren. Das sind
|
||||
#: drei verschiedene Eingriffe mit drei verschiedenen Folgen -- also drei
|
||||
#: Zeilen. Die Reihenfolge ist die der Rangfolge in ``services.restart``.
|
||||
DEVICE_RESTART_POE = Resource(
|
||||
key="device_restart_poe",
|
||||
parent="devices",
|
||||
label="Clients: Neustart über PoE",
|
||||
actions=(A.READ, A.EXECUTE),
|
||||
hint="Portzyklus am Switch – wirkt in Sekunden, auch wenn das Gerät hängt",
|
||||
action_hints={
|
||||
A.READ: "Den Knopf sehen.",
|
||||
A.EXECUTE: "Neustart auslösen. Bewusst nicht lizenzpflichtig: ein hängendes "
|
||||
"Gerät soll man auch ohne gültige Lizenz von Hand neu starten können.",
|
||||
},
|
||||
)
|
||||
|
||||
DEVICE_RESTART_SSH = Resource(
|
||||
key="device_restart_ssh",
|
||||
parent="devices",
|
||||
label="Clients: Neustart über SSH",
|
||||
actions=(A.READ, A.EXECUTE),
|
||||
hint="Sauberes Herunterfahren eines Linux-Geräts (Modul Wartung)",
|
||||
action_hints={
|
||||
A.EXECUTE: "Neustartauftrag anlegen. Dauert Minuten und setzt ein "
|
||||
"antwortendes Gerät samt freigegebenem Host-Schlüssel voraus.",
|
||||
},
|
||||
)
|
||||
|
||||
DEVICE_RESTART_RPC = Resource(
|
||||
key="device_restart_rpc",
|
||||
parent="devices",
|
||||
label="Clients: Neustart über RPC",
|
||||
actions=(A.READ, A.EXECUTE),
|
||||
hint="Windows-Gerät ohne SSH und ohne Host-Schlüssel (Modul Wartung)",
|
||||
action_hints={A.EXECUTE: "Neustartauftrag anlegen (wie 'shutdown /m')."},
|
||||
)
|
||||
|
||||
#: Die Client-Konsole lief unter ``maintenance.execute`` mit -- demselben Recht
|
||||
#: wie die Paketaktualisierung. Eine Sitzung auf einem Geraet ist aber etwas
|
||||
#: anderes als ein Auftrag, den der Server abarbeitet: hier tippt ein Mensch.
|
||||
DEVICE_CONSOLE = Resource(
|
||||
key="device_console",
|
||||
parent="devices",
|
||||
label="Clients: Konsole",
|
||||
actions=(A.READ, A.EXECUTE),
|
||||
hint="Browser-Terminal auf einem Client (Modul Wartung)",
|
||||
action_hints={
|
||||
A.EXECUTE: "Sitzung öffnen. Verlangt eine frische Passwortbestätigung; "
|
||||
"jede Sitzung wird vollständig mitgeschrieben.",
|
||||
},
|
||||
)
|
||||
|
||||
SWITCHES = Resource(
|
||||
key="switches",
|
||||
label="Switche",
|
||||
actions=(A.VIEW, A.CREATE, A.EDIT, A.DELETE, A.EXECUTE),
|
||||
hint="Aruba-Switche und ihre Erreichbarkeit",
|
||||
actions=(A.READ, A.CREATE, A.WRITE, A.DELETE),
|
||||
hint="Aruba-Switche, ihre Erreichbarkeit und Ports",
|
||||
)
|
||||
|
||||
SWITCH_CONSOLE = Resource(
|
||||
key="switch_console",
|
||||
parent="switches",
|
||||
label="Switche: Konsole",
|
||||
actions=(A.READ, A.EXECUTE),
|
||||
hint="Kommandos auf dem Switch ausführen",
|
||||
action_hints={
|
||||
A.EXECUTE: "Browser-Terminal öffnen und Kommandos auf dem Switch ausführen. "
|
||||
"Verlangt zusätzlich eine frische Passwortbestätigung; jede Sitzung wird "
|
||||
"vollständig protokolliert.",
|
||||
A.EXECUTE: "Sitzung öffnen. Verlangt eine frische Passwortbestätigung; "
|
||||
"jede Sitzung wird vollständig mitgeschrieben.",
|
||||
},
|
||||
)
|
||||
|
||||
#: Einen geaenderten Host-Schluessel freizugeben hing an "Gerät ändern" bzw.
|
||||
#: "Switch ändern" -- also an einem Recht fuer Stammdaten. Es ist aber der
|
||||
#: Vorgang, mit dem man sagt: "dieses Geraet ist wirklich das, fuer das es sich
|
||||
#: ausgibt". Wer Namen und Standort pflegt, muss das nicht entscheiden duerfen.
|
||||
HOSTKEYS = Resource(
|
||||
key="hostkeys",
|
||||
label="Host-Schlüssel",
|
||||
actions=(A.READ, A.EXECUTE),
|
||||
hint="Ein Gerät meldet sich mit einem anderen SSH-Schlüssel als beim letzten Mal",
|
||||
action_hints={
|
||||
A.READ: "Offene und freigegebene Schlüssel sehen.",
|
||||
A.EXECUTE: "Einen geänderten Schlüssel freigeben.",
|
||||
},
|
||||
)
|
||||
|
||||
CREDENTIALS = Resource(
|
||||
key="credentials",
|
||||
label="Zugangsdaten",
|
||||
actions=(A.VIEW, A.CREATE, A.EDIT, A.DELETE, A.SECRETS),
|
||||
actions=(A.READ, A.CREATE, A.WRITE, A.DELETE, A.SECRETS),
|
||||
hint="Wiederverwendbare SSH-Logins",
|
||||
action_hints={
|
||||
A.VIEW: "Namen und Benutzer sehen – nicht das Passwort.",
|
||||
A.READ: "Namen und Benutzer sehen – nicht das Passwort.",
|
||||
A.SECRETS: "Passwort im Klartext anzeigen. Verlangt eine frische "
|
||||
"Passwortbestätigung und wird protokolliert.",
|
||||
},
|
||||
)
|
||||
|
||||
MAINTENANCE = Resource(
|
||||
key="maintenance",
|
||||
# Nicht "Wartung": so heisst der Bereich. Die Zeile sagt, was sie erlaubt.
|
||||
label="Aktualisierung und Neustart",
|
||||
actions=(A.VIEW, A.EXECUTE),
|
||||
hint="Paketaktualisierung und Neustart per SSH (lizenzpflichtiges Modul)",
|
||||
action_hints={A.EXECUTE: "Aktualisierung oder Neustart auf ausgewählten Geräten starten."},
|
||||
#: Aus einer Zeile ("Aktualisierung und Neustart") wurden drei. Sie deckte
|
||||
#: vier Funktionen ab: Pakete aktualisieren, reihenweise neu starten, das
|
||||
#: Windows-Werkzeug installieren -- und die Client-Konsole, die jetzt bei den
|
||||
#: Clients steht, wo sie hingehoert.
|
||||
SERVICING_UPDATE = Resource(
|
||||
key="servicing_update",
|
||||
label="Paketaktualisierung",
|
||||
actions=(A.READ, A.EXECUTE),
|
||||
hint="Pakete auf ausgewählten Geräten aktualisieren (lizenzpflichtiges Modul)",
|
||||
action_hints={A.EXECUTE: "Auftrag starten."},
|
||||
)
|
||||
|
||||
DHCP = Resource(
|
||||
key="dhcp",
|
||||
label="DHCP-Server",
|
||||
actions=(A.VIEW, A.EDIT, A.EXECUTE),
|
||||
hint="DHCP: Subnetze, Optionen, Reservierungen (lizenzpflichtiges Modul)",
|
||||
SERVICING_REBOOT = Resource(
|
||||
key="servicing_reboot",
|
||||
label="Neustart als Auftrag",
|
||||
actions=(A.READ, A.EXECUTE),
|
||||
hint="Reihenweise Neustarts mit Wartezeit und Rückmeldung",
|
||||
action_hints={A.EXECUTE: "Auftrag starten."},
|
||||
)
|
||||
|
||||
SERVICING_TOOL = Resource(
|
||||
key="servicing_tool",
|
||||
label="Windows-Werkzeug installieren",
|
||||
actions=(A.READ, A.EXECUTE),
|
||||
hint="Voraussetzung für den RPC-Weg auf einem Windows-Gerät",
|
||||
action_hints={A.EXECUTE: "Installation auslösen."},
|
||||
)
|
||||
|
||||
#: Die Wartungsseite zeigt alle drei Aufgaben. Sie oeffnet, wer **eine** davon
|
||||
#: sehen darf -- und zeigt dann nur die. Als Konstante, damit die vier Routen
|
||||
#: dieselbe Antwort geben; einzeln getippt liefen sie irgendwann auseinander.
|
||||
SERVICING_PAGE: tuple[str, ...] = (
|
||||
"servicing_update.read",
|
||||
"servicing_reboot.read",
|
||||
"servicing_tool.read",
|
||||
)
|
||||
|
||||
#: Elf Funktionen hingen an ``dhcp.edit``: Grundeinstellungen, Subnetze,
|
||||
#: Optionen, Geraeteoptionen, eigene Optionen, Reservierungen und das
|
||||
#: Schreiben der Dienstkonfiguration. Wer eine Reservierung eintragen sollte,
|
||||
#: durfte damit auch Subnetze loeschen und die Konfiguration ueberschreiben.
|
||||
DHCP_SUBNETS = Resource(
|
||||
key="dhcp_subnets",
|
||||
label="DHCP: Subnetze",
|
||||
actions=(A.READ, A.CREATE, A.WRITE, A.DELETE),
|
||||
hint="Bereiche, Gateway, DNS, Reihenfolge (lizenzpflichtiges Modul)",
|
||||
)
|
||||
|
||||
DHCP_OPTIONS = Resource(
|
||||
key="dhcp_options",
|
||||
parent="dhcp_subnets",
|
||||
label="DHCP: Optionen",
|
||||
actions=(A.READ, A.WRITE),
|
||||
hint="Globale Optionen, Optionen je Gerät und eigene Optionen",
|
||||
action_hints={A.WRITE: "Werte setzen, eigene Optionen anlegen und entfernen."},
|
||||
)
|
||||
|
||||
DHCP_RESERVATIONS = Resource(
|
||||
key="dhcp_reservations",
|
||||
parent="dhcp_subnets",
|
||||
label="DHCP: Reservierungen",
|
||||
actions=(A.READ, A.CREATE, A.WRITE, A.DELETE),
|
||||
hint="Feste Adressen zu einer MAC",
|
||||
)
|
||||
|
||||
DHCP_CONFIG = Resource(
|
||||
key="dhcp_config",
|
||||
parent="dhcp_subnets",
|
||||
label="DHCP: Konfiguration schreiben",
|
||||
actions=(A.READ, A.EXECUTE),
|
||||
hint="Den bearbeiteten Stand in die Dienstkonfiguration übernehmen",
|
||||
action_hints={
|
||||
A.EDIT: "Konfiguration ändern und schreiben.",
|
||||
A.EXECUTE: "Dienst installieren, starten oder stoppen.",
|
||||
A.EXECUTE: "Schreiben und neu laden. Bis dahin ändert sich am laufenden "
|
||||
"Dienst nichts – Bearbeiten und Wirksamwerden sind zwei Schritte.",
|
||||
},
|
||||
)
|
||||
|
||||
DHCP_SERVICE = Resource(
|
||||
key="dhcp_service",
|
||||
parent="dhcp_subnets",
|
||||
label="DHCP: Dienst",
|
||||
actions=(A.READ, A.EXECUTE),
|
||||
hint="Kea installieren, starten, stoppen",
|
||||
action_hints={A.EXECUTE: "Dienstzustand ändern."},
|
||||
)
|
||||
|
||||
#: Die DHCP-Seite zeigt alle Abschnitte; jeder Abschnitt haengt an seiner
|
||||
#: eigenen Zeile. Geoeffnet wird sie, wer einen davon sehen darf.
|
||||
DHCP_PAGE: tuple[str, ...] = (
|
||||
"dhcp_subnets.read",
|
||||
"dhcp_options.read",
|
||||
"dhcp_reservations.read",
|
||||
"dhcp_config.read",
|
||||
"dhcp_service.read",
|
||||
)
|
||||
|
||||
FILESHARE = Resource(
|
||||
key="fileshare",
|
||||
label="Dateifreigaben",
|
||||
actions=(A.VIEW, A.CREATE, A.EDIT, A.DELETE, A.EXPORT),
|
||||
actions=(A.READ, A.CREATE, A.WRITE, A.DELETE, A.EXPORT),
|
||||
hint="SMB-Freigaben aus dem Verzeichnisdienst (lizenzpflichtiges Modul)",
|
||||
action_hints={
|
||||
A.CREATE: "Ordner anlegen und Dateien hochladen.",
|
||||
A.EDIT: "Umbenennen und verschieben.",
|
||||
A.WRITE: "Umbenennen und verschieben.",
|
||||
# Genau die Bedeutung aus der Legende: Daten aus dem System
|
||||
# heraustragen. Ansehen und Mitnehmen sind zweierlei -- eine Vorschau
|
||||
# bleibt im Haus, eine heruntergeladene Datei nicht.
|
||||
@@ -109,20 +261,47 @@ FILESHARE = Resource(
|
||||
#: soll deshalb nicht automatisch veraenderbare Formulare ablegen duerfen.
|
||||
FILESHARE_PDF = Resource(
|
||||
key="fileshare_pdf",
|
||||
parent="fileshare",
|
||||
label="PDF-Formulare",
|
||||
actions=(A.VIEW, A.CREATE, A.EDIT),
|
||||
actions=(A.READ, A.CREATE, A.WRITE),
|
||||
hint="Formulare in der Vorschau ausfüllen und in der Freigabe ablegen",
|
||||
action_hints={
|
||||
A.VIEW: "In der Vorschau ausfüllen (ohne Speichern).",
|
||||
A.READ: "In der Vorschau ausfüllen (ohne Speichern).",
|
||||
A.CREATE: "Ausgefüllt speichern – abgeschlossen, Felder gesperrt.",
|
||||
A.EDIT: "Ausgefüllt speichern – weiter ausfüllbar, inklusive Bild.",
|
||||
A.WRITE: "Ausgefüllt speichern – weiter ausfüllbar, inklusive Bild.",
|
||||
},
|
||||
)
|
||||
|
||||
#: Unterschreiben ist kein Aendern: am Ende steht eine Fassung, die niemand
|
||||
#: mehr bearbeiten kann, mit Namen und Zeitpunkt im Dokument und einer
|
||||
#: Pruefsumme im Aenderungsprotokoll. Wer Dateien umbenennen darf, soll das
|
||||
#: nicht automatisch duerfen.
|
||||
FILESHARE_SIGNATURE = Resource(
|
||||
key="fileshare_signature",
|
||||
parent="fileshare",
|
||||
label="Unterschriften",
|
||||
actions=(A.READ, A.EXECUTE),
|
||||
hint="Mit dem Finger unterschreiben; das Ergebnis ist unveränderbar",
|
||||
action_hints={
|
||||
A.READ: "Unterschriebene Dateien öffnen und die Prüfung sehen.",
|
||||
A.EXECUTE: "Unterschreiben und abgeschlossen ablegen.",
|
||||
},
|
||||
)
|
||||
|
||||
#: Welche Verzeichnisfreigabe unter welchem Namen erscheint, ist Administration
|
||||
#: und kein Dateibetrieb -- es lief unter demselben Recht wie "umbenennen".
|
||||
FILESHARE_MAPPINGS = Resource(
|
||||
key="fileshare_mappings",
|
||||
parent="fileshare",
|
||||
label="Freigabe-Zuordnungen",
|
||||
actions=(A.READ, A.CREATE, A.WRITE, A.DELETE),
|
||||
hint="Verzeichnisgruppe, UNC-Pfad und Anzeigename einer Freigabe",
|
||||
)
|
||||
|
||||
LOGS_DHCP = Resource(
|
||||
key="logs_dhcp",
|
||||
label="DHCP-Protokoll",
|
||||
actions=(A.VIEW,),
|
||||
actions=(A.READ,),
|
||||
)
|
||||
|
||||
MONITORING_AREA = Area(
|
||||
@@ -131,7 +310,17 @@ MONITORING_AREA = Area(
|
||||
icon="cpu",
|
||||
hint="Geräte, Switche und Zugangsdaten. Ohne diesen Zugang bleibt der "
|
||||
"gesamte Bereich verborgen.",
|
||||
resources=(DEVICES, SWITCHES, CREDENTIALS),
|
||||
resources=(
|
||||
DEVICES,
|
||||
DEVICE_RESTART_POE,
|
||||
DEVICE_RESTART_SSH,
|
||||
DEVICE_RESTART_RPC,
|
||||
DEVICE_CONSOLE,
|
||||
SWITCHES,
|
||||
SWITCH_CONSOLE,
|
||||
HOSTKEYS,
|
||||
CREDENTIALS,
|
||||
),
|
||||
)
|
||||
|
||||
#: Wartung ueberwacht nichts, sie greift ein -- und sie steht seit 2.5.0 als
|
||||
@@ -144,21 +333,21 @@ MAINTENANCE_AREA = Area(
|
||||
icon="wrench",
|
||||
hint="Paketpflege und Neustarts. Ohne diesen Zugang bleibt der Bereich "
|
||||
"verborgen – auch wenn einzelne Rechte darunter gesetzt sind.",
|
||||
resources=(MAINTENANCE,),
|
||||
resources=(SERVICING_UPDATE, SERVICING_REBOOT, SERVICING_TOOL),
|
||||
)
|
||||
|
||||
NETWORK_AREA = Area(
|
||||
key="network",
|
||||
label="Netzwerkdienste",
|
||||
icon="network",
|
||||
resources=(DHCP,),
|
||||
resources=(DHCP_SUBNETS, DHCP_OPTIONS, DHCP_RESERVATIONS, DHCP_CONFIG, DHCP_SERVICE),
|
||||
)
|
||||
|
||||
FILES_AREA = Area(
|
||||
key="files",
|
||||
label="Dateifreigaben",
|
||||
icon="folder",
|
||||
resources=(FILESHARE, FILESHARE_PDF),
|
||||
resources=(FILESHARE, FILESHARE_PDF, FILESHARE_SIGNATURE, FILESHARE_MAPPINGS),
|
||||
)
|
||||
|
||||
PERMISSIONS = PermissionTree(
|
||||
@@ -173,15 +362,41 @@ 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: Geräte und Switche ansehen, laufendes Protokoll lesen.",
|
||||
grants=(
|
||||
("devices", (A.VIEW,)),
|
||||
("switches", (A.VIEW,)),
|
||||
("logs_live", (A.VIEW,)),
|
||||
("devices", (A.READ,)),
|
||||
("switches", (A.READ,)),
|
||||
("logs_live", (A.READ,)),
|
||||
),
|
||||
is_default=True,
|
||||
),
|
||||
@@ -191,12 +406,20 @@ ROLE_PRESETS: tuple[RolePreset, ...] = (
|
||||
description="Geräte und Switche pflegen, Neustarts auslösen, Wartung fahren.",
|
||||
grants=(
|
||||
("devices", "*"),
|
||||
("switches", (A.VIEW, A.CREATE, A.EDIT, A.EXECUTE)),
|
||||
("credentials", (A.VIEW,)),
|
||||
("maintenance", "*"),
|
||||
("logs_live", (A.VIEW,)),
|
||||
("logs_history", (A.VIEW,)),
|
||||
("trash", (A.VIEW, A.EDIT)),
|
||||
("device_restart_poe", "*"),
|
||||
("device_restart_ssh", "*"),
|
||||
("device_restart_rpc", "*"),
|
||||
("device_console", "*"),
|
||||
("switches", (A.READ, A.CREATE, A.WRITE)),
|
||||
("switch_console", "*"),
|
||||
("hostkeys", "*"),
|
||||
("credentials", (A.READ,)),
|
||||
("servicing_update", "*"),
|
||||
("servicing_reboot", "*"),
|
||||
("servicing_tool", "*"),
|
||||
("logs_live", (A.READ,)),
|
||||
("logs_history", (A.READ,)),
|
||||
("trash", (A.READ, A.WRITE)),
|
||||
),
|
||||
),
|
||||
RolePreset(
|
||||
@@ -204,9 +427,13 @@ ROLE_PRESETS: tuple[RolePreset, ...] = (
|
||||
name="Support",
|
||||
description="Erste Hilfe am Telefon: Status sehen, PoE-Neustart auslösen.",
|
||||
grants=(
|
||||
("devices", (A.VIEW, A.EXECUTE)),
|
||||
("switches", (A.VIEW,)),
|
||||
("logs_live", (A.VIEW,)),
|
||||
("devices", (A.READ,)),
|
||||
# Nur der PoE-Weg: der wirkt auch bei einem haengenden Geraet und
|
||||
# unterbricht nichts Laufendes. SSH und RPC fahren ein arbeitendes
|
||||
# System herunter -- das gehoert nicht in die erste Hilfe.
|
||||
("device_restart_poe", "*"),
|
||||
("switches", (A.READ,)),
|
||||
("logs_live", (A.READ,)),
|
||||
),
|
||||
),
|
||||
RolePreset(
|
||||
@@ -214,11 +441,15 @@ ROLE_PRESETS: tuple[RolePreset, ...] = (
|
||||
name="Netzwerk",
|
||||
description="DHCP und Host-Netzwerkeinstellungen.",
|
||||
grants=(
|
||||
("dhcp", "*"),
|
||||
("dhcp_subnets", "*"),
|
||||
("dhcp_options", "*"),
|
||||
("dhcp_reservations", "*"),
|
||||
("dhcp_config", "*"),
|
||||
("dhcp_service", "*"),
|
||||
("settings_network", "*"),
|
||||
("logs_dhcp", (A.VIEW,)),
|
||||
("devices", (A.VIEW,)),
|
||||
("switches", (A.VIEW,)),
|
||||
("logs_dhcp", (A.READ,)),
|
||||
("devices", (A.READ,)),
|
||||
("switches", (A.READ,)),
|
||||
),
|
||||
),
|
||||
RolePreset(
|
||||
@@ -226,17 +457,19 @@ ROLE_PRESETS: tuple[RolePreset, ...] = (
|
||||
name="Revision",
|
||||
description="Nur lesen – inklusive Änderungsprotokoll und Diagnose.",
|
||||
grants=(
|
||||
("devices", (A.VIEW,)),
|
||||
("switches", (A.VIEW,)),
|
||||
("credentials", (A.VIEW,)),
|
||||
("dhcp", (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,)),
|
||||
("trash", (A.VIEW,)),
|
||||
("devices", (A.READ,)),
|
||||
("switches", (A.READ,)),
|
||||
("credentials", (A.READ,)),
|
||||
("dhcp_subnets", (A.READ,)),
|
||||
("dhcp_options", (A.READ,)),
|
||||
("dhcp_reservations", (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,)),
|
||||
("trash", (A.READ,)),
|
||||
),
|
||||
),
|
||||
RolePreset(
|
||||
@@ -247,16 +480,25 @@ 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", "*"),
|
||||
("backup", (A.VIEW, A.EXECUTE, A.EXPORT)),
|
||||
("backup_export", (A.READ, A.EXPORT)),
|
||||
("backup_import", "*"),
|
||||
("trash", "*"),
|
||||
("diagnostics", (A.VIEW,)),
|
||||
("logs_audit", (A.VIEW,)),
|
||||
("diagnostics", (A.READ,)),
|
||||
("logs_audit", (A.READ,)),
|
||||
),
|
||||
),
|
||||
FUNKTIONEN_VOLLZUGRIFF,
|
||||
)
|
||||
|
||||
__all__ = ["PERMISSIONS", "ROLE_PRESETS", "LOGS_DHCP"]
|
||||
__all__ = [
|
||||
"PERMISSIONS",
|
||||
"ROLE_PRESETS",
|
||||
"LOGS_DHCP",
|
||||
"SERVICING_PAGE",
|
||||
"DHCP_PAGE",
|
||||
]
|
||||
@@ -257,19 +257,19 @@ CREATE INDEX IF NOT EXISTS idx_restart_events_method ON restart_events(method, t
|
||||
V4 = """
|
||||
INSERT OR IGNORE INTO group_permissions (group_id, permission)
|
||||
SELECT group_id, 'fileshare.export' FROM group_permissions
|
||||
WHERE permission = 'fileshare.view';
|
||||
WHERE permission = 'fileshare.read';
|
||||
|
||||
INSERT OR IGNORE INTO group_permissions (group_id, permission)
|
||||
SELECT group_id, 'fileshare_pdf.view' FROM group_permissions
|
||||
WHERE permission = 'fileshare.view';
|
||||
SELECT group_id, 'fileshare_pdf.read' FROM group_permissions
|
||||
WHERE permission = 'fileshare.read';
|
||||
|
||||
INSERT OR IGNORE INTO group_permissions (group_id, permission)
|
||||
SELECT group_id, 'fileshare_pdf.create' FROM group_permissions
|
||||
WHERE permission = 'fileshare.create';
|
||||
|
||||
INSERT OR IGNORE INTO group_permissions (group_id, permission)
|
||||
SELECT group_id, 'fileshare_pdf.edit' FROM group_permissions
|
||||
WHERE permission = 'fileshare.edit';
|
||||
SELECT group_id, 'fileshare_pdf.write' FROM group_permissions
|
||||
WHERE permission = 'fileshare.write';
|
||||
"""
|
||||
|
||||
|
||||
@@ -297,13 +297,21 @@ DELETE FROM settings WHERE key = 'terminal_enabled';
|
||||
# Bewusst nicht "alle, die ein Wartungsrecht haben": wer den Bereich
|
||||
# Ueberwachung nicht hatte, kam auch vorher nicht an die Wartung. Ein Update
|
||||
# darf Zugang erhalten, aber keinen neuen schaffen.
|
||||
#
|
||||
# Die Bedingung nennt absichtlich keine festen Schluessel, sondern Praefixe:
|
||||
# Wer von einer Fassung vor diesem Schritt direkt auf 2.8 geht, hat vorher die
|
||||
# Migrationen des Kerns durchlaufen (core laeuft vor der Anwendung) -- die
|
||||
# Rechte heissen dann schon "read"/"write" und die Wartungszeilen "servicing_".
|
||||
# Mit ausgeschriebenen Schluesseln fand dieser Schritt in genau diesem Fall
|
||||
# nichts und der Bereich waere doch weg. Ein Ueberrest ist die Zeile nicht:
|
||||
# eine angewendete Migration laeuft nie wieder.
|
||||
V6 = """
|
||||
INSERT OR IGNORE INTO group_permissions (group_id, permission)
|
||||
SELECT DISTINCT group_id, 'servicing.view'
|
||||
SELECT DISTINCT group_id, 'servicing.read'
|
||||
FROM group_permissions
|
||||
WHERE permission IN ('maintenance.view', 'maintenance.execute')
|
||||
WHERE (permission LIKE 'maintenance.%' OR permission LIKE 'servicing%')
|
||||
AND group_id IN (
|
||||
SELECT group_id FROM group_permissions WHERE permission = 'monitoring.view'
|
||||
SELECT group_id FROM group_permissions WHERE permission = 'monitoring.read'
|
||||
);
|
||||
"""
|
||||
|
||||
@@ -371,12 +379,37 @@ def uebernehme_signaturen(conn) -> None: # type: ignore[no-untyped-def]
|
||||
)
|
||||
|
||||
|
||||
def uebergang_funktionsgruppe(conn) -> None: # type: ignore[no-untyped-def]
|
||||
"""Bestehende Administratoren behalten ihre Funktionen -- ueber eine Gruppe.
|
||||
|
||||
Steht hier und nicht im Kern, weil erst die Anwendung weiss, welche Zeilen
|
||||
ausserhalb der Verwaltung es gibt. Laeuft nach den Migrationen des Kerns
|
||||
(die Rechte heissen dann schon read/write und sind aufgeteilt).
|
||||
"""
|
||||
from tesm_core.rbac import service as rbac
|
||||
|
||||
from .permissions import FUNKTIONEN_VOLLZUGRIFF, PERMISSIONS
|
||||
|
||||
rbac.funktionsgruppe_fuer_administratoren(conn, PERMISSIONS, FUNKTIONEN_VOLLZUGRIFF)
|
||||
|
||||
|
||||
TESM_MIGRATIONS: tuple[Migration, ...] = (
|
||||
Migration(version=1, name="baseline", sql=V1),
|
||||
Migration(version=2, name="seed-dhcp-options", fn=seed_dhcp_options),
|
||||
Migration(version=3, name="restart-events", sql=V3),
|
||||
Migration(version=4, name="fileshare-pdf-rechte", sql=V4),
|
||||
Migration(
|
||||
version=4,
|
||||
name="fileshare-pdf-rechte",
|
||||
sql=V4,
|
||||
geaendert_weil="Die Rechteschluessel heissen seit 2.8.0 read/write. Auf einer frischen Installation laufen die Migrationen des Kerns zuerst, also muss dieser Schritt die neuen Namen setzen -- mit den alten blieben sie ungueltig.",
|
||||
),
|
||||
Migration(version=5, name="terminal-getrennt", sql=V5),
|
||||
Migration(version=6, name="wartung-eigener-bereich", sql=V6),
|
||||
Migration(
|
||||
version=6,
|
||||
name="wartung-eigener-bereich",
|
||||
sql=V6,
|
||||
geaendert_weil="Die Rechteschluessel heissen seit 2.8.0 read/write. Auf einer frischen Installation laufen die Migrationen des Kerns zuerst, also muss dieser Schritt die neuen Namen setzen -- mit den alten blieben sie ungueltig.",
|
||||
),
|
||||
Migration(version=7, name="signaturverzeichnis", sql=V7, fn=uebernehme_signaturen),
|
||||
Migration(version=8, name="funktionsgruppe-fuer-admins", fn=uebergang_funktionsgruppe),
|
||||
)
|
||||
@@ -22,7 +22,7 @@ etwa bei einem Geraet, das schreibend auf eine Platte zugreift.
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
from typing import Any, Callable
|
||||
|
||||
METHOD_POE = "poe"
|
||||
METHOD_SSH = "ssh"
|
||||
@@ -40,6 +40,16 @@ class Method:
|
||||
#: Braucht dieser Weg das lizenzpflichtige Modul "Wartung"?
|
||||
licensed: bool = False
|
||||
|
||||
@property
|
||||
def recht(self) -> str:
|
||||
"""Das Recht, das genau diesen Weg freigibt.
|
||||
|
||||
Am Weg und nicht in der Route: die Liste der Wege wird an vier Stellen
|
||||
gebraucht (Dashboard, Geraeteliste, Detailseite, Route), und ein Recht,
|
||||
das man an vier Stellen von Hand dazuschreibt, fehlt an der fuenften.
|
||||
"""
|
||||
return f"device_restart_{self.key}.execute"
|
||||
|
||||
@property
|
||||
def is_poe(self) -> bool:
|
||||
return self.key == METHOD_POE
|
||||
@@ -78,12 +88,22 @@ DEFINITIONS: dict[str, Method] = {
|
||||
}
|
||||
|
||||
|
||||
def available(device: Any, *, maintenance_active: bool = True) -> list[Method]:
|
||||
def available(
|
||||
device: Any,
|
||||
*,
|
||||
maintenance_active: bool = True,
|
||||
darf: Callable[[str], bool] | None = None,
|
||||
) -> list[Method]:
|
||||
"""Alle Wege, die bei diesem Geraet tatsaechlich zu etwas fuehren.
|
||||
|
||||
Ein Weg, der nur *fast* geht -- Switch zugeordnet, aber kein Port --, gilt
|
||||
als nicht verfuegbar. Ein Knopf, der verlaesslich nichts bewirkt, ist
|
||||
schlimmer als kein Knopf.
|
||||
|
||||
``darf`` filtert zusaetzlich nach dem Recht des jeweiligen Weges (in der
|
||||
Anwendung ``current_user.has``). Damit verschwindet der Knopf fuer den, der
|
||||
ihn nicht druecken darf -- und zwar an allen Stellen zugleich, weil alle
|
||||
dieselbe Liste benutzen. Ohne ``darf`` ist die Antwort rein technisch.
|
||||
"""
|
||||
|
||||
def field(name: str) -> Any:
|
||||
@@ -102,7 +122,10 @@ def available(device: Any, *, maintenance_active: bool = True) -> list[Method]:
|
||||
# der PoE-Weg, und der heisst dort "Einschalten": er schaltet den Port
|
||||
# wieder ein, statt ein laufendes System neu zu starten.
|
||||
if str(field("state") or "") == "offline":
|
||||
return [DEFINITIONS[POWER_ON]] if hat_poe else []
|
||||
einschalten = [DEFINITIONS[POWER_ON]] if hat_poe else []
|
||||
if darf is not None:
|
||||
einschalten = [entry for entry in einschalten if darf(entry.recht)]
|
||||
return einschalten
|
||||
|
||||
category = str(field("credential_category") or "")
|
||||
has_login = bool(field("credential_id"))
|
||||
@@ -112,23 +135,39 @@ def available(device: Any, *, maintenance_active: bool = True) -> list[Method]:
|
||||
elif category == "linux":
|
||||
methods.append(DEFINITIONS[METHOD_SSH])
|
||||
|
||||
if darf is not None:
|
||||
methods = [entry for entry in methods if darf(entry.recht)]
|
||||
return sorted(methods, key=lambda entry: PREFERENCE.index(entry.key))
|
||||
|
||||
|
||||
def preferred(device: Any, *, maintenance_active: bool = True) -> Method | None:
|
||||
def preferred(
|
||||
device: Any,
|
||||
*,
|
||||
maintenance_active: bool = True,
|
||||
darf: Callable[[str], bool] | None = None,
|
||||
) -> Method | None:
|
||||
"""Der Weg, der vorausgewaehlt wird -- oder ``None``, wenn es keinen gibt."""
|
||||
options = available(device, maintenance_active=maintenance_active)
|
||||
options = available(device, maintenance_active=maintenance_active, darf=darf)
|
||||
return options[0] if options else None
|
||||
|
||||
|
||||
def resolve(device: Any, requested: str, *, maintenance_active: bool = True) -> Method | None:
|
||||
def resolve(
|
||||
device: Any,
|
||||
requested: str,
|
||||
*,
|
||||
maintenance_active: bool = True,
|
||||
darf: Callable[[str], bool] | None = None,
|
||||
) -> Method | None:
|
||||
"""Prueft eine Anforderung gegen das, was tatsaechlich moeglich ist.
|
||||
|
||||
Die Oberflaeche darf vorschlagen; entscheiden tut der Server. Ein Weg, der
|
||||
fuer dieses Geraet nicht offensteht, wird nicht "irgendwie" ersetzt --
|
||||
sonst startet jemand ein Geraet anders neu, als er dachte.
|
||||
"""
|
||||
options = {entry.key: entry for entry in available(device, maintenance_active=maintenance_active)}
|
||||
options = {
|
||||
entry.key: entry
|
||||
for entry in available(device, maintenance_active=maintenance_active, darf=darf)
|
||||
}
|
||||
if not options:
|
||||
return None
|
||||
if not requested:
|
||||
|
||||
@@ -14,6 +14,7 @@ from typing import Any, Callable, Iterable
|
||||
|
||||
from tesm_core.db import Database
|
||||
from tesm_core.keystore import Keystore
|
||||
from tesm_core.rbac.legacy_permissions import umgeschrieben
|
||||
from tesm_core.transfer import (
|
||||
MIN_PASSPHRASE_LENGTH,
|
||||
PREVIEW_TTL_MINUTES,
|
||||
@@ -254,6 +255,9 @@ def apply_import(
|
||||
"""
|
||||
from .inventory import ValidationError, aad_for_credential, normalize_mac
|
||||
|
||||
#: Merkt, ob das Paket alte Rechteschluessel mitbringt (siehe unten).
|
||||
alte_fassung: set[bool] = set()
|
||||
|
||||
def mac_or_none(value: Any) -> str | None:
|
||||
"""MAC normalisieren, ohne den ganzen Import zu sprengen.
|
||||
|
||||
@@ -574,9 +578,21 @@ def apply_import(
|
||||
group_id = int(existing["id"])
|
||||
bump("groups", "updated")
|
||||
conn.execute("DELETE FROM group_permissions WHERE group_id=?", (group_id,))
|
||||
# Eine Sicherung aus 2.7.x nennt devices.view und devices.edit.
|
||||
# Ohne die Umschreibung landen lauter unbekannte Schluessel in der
|
||||
# Gruppe, und sanitize() wirft Unbekanntes still weg: die
|
||||
# wiederhergestellte Gruppe waere leer, ohne eine Zeile Meldung.
|
||||
mitgebracht = [str(recht) for recht in entry.get("permissions", []) or []]
|
||||
neue = umgeschrieben(mitgebracht)
|
||||
if sorted(set(mitgebracht)) != neue:
|
||||
# Das Paket kennt die alten Schluessel -- es stammt also von
|
||||
# vor 2.8.0. Ein Versionsfeld gibt es im Umschlag nicht, aber
|
||||
# die Schluessel selbst sind das verlaesslichere Kennzeichen:
|
||||
# sie stehen genau dann drin, wenn die Sicherung aelter ist.
|
||||
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:
|
||||
@@ -683,4 +699,17 @@ def apply_import(
|
||||
set_setting(str(key), value)
|
||||
bump("settings", "updated")
|
||||
|
||||
if alte_fassung and "users" in wanted:
|
||||
# Dieselbe Ueberlegung wie in der Migration: in der Welt, aus der
|
||||
# diese Sicherung kommt, konnte ein Administrator alles. Ohne die
|
||||
# Gruppe fehlt ihm nach dem Einlesen die halbe Anwendung -- und er
|
||||
# haelt das fuer einen Fehler der Wiederherstellung.
|
||||
from tesm_core.rbac import service as rbac
|
||||
|
||||
from ..permissions import FUNKTIONEN_VOLLZUGRIFF, PERMISSIONS
|
||||
|
||||
rbac.funktionsgruppe_fuer_administratoren(
|
||||
conn, PERMISSIONS, FUNKTIONEN_VOLLZUGRIFF
|
||||
)
|
||||
|
||||
return report
|
||||
@@ -84,7 +84,7 @@
|
||||
<a class="btn btn--sm btn--ghost" href="{{ url_for('devices.detail', device_id=tile.id) }}">
|
||||
Details
|
||||
</a>
|
||||
{% if current_user.has('devices.execute') and tile.restart_methods %}
|
||||
{% if tile.restart_methods %}
|
||||
{% set method = tile.restart_methods[0] %}
|
||||
<form method="post" data-no-guard
|
||||
action="{{ url_for('devices.restart_device', device_id=tile.id) }}">
|
||||
@@ -99,7 +99,7 @@
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if current_user.has('devices.edit') and tile.state == 'disabled' %}
|
||||
{% if current_user.has('devices.write') and tile.state == 'disabled' %}
|
||||
<form method="post" action="{{ url_for('devices.toggle_active', device_id=tile.id) }}"
|
||||
data-no-guard>
|
||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||
|
||||
@@ -79,7 +79,7 @@ davon abhängt, welche Geräte auf der Wartungsseite erscheinen.', 'info') }}
|
||||
data-confirm-style="primary">{{ icon('eye', size=15) }}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if current_user.has('credentials.edit') %}
|
||||
{% if current_user.has('credentials.write') %}
|
||||
<button class="btn btn--sm btn--ghost" type="button"
|
||||
data-dialog-open="credential-edit-{{ entry.id }}" aria-label="Bearbeiten">
|
||||
{{ icon('pencil', size=15) }}
|
||||
@@ -121,7 +121,7 @@ davon abhängt, welche Geräte auf der Wartungsseite erscheinen.', 'info') }}
|
||||
</form>
|
||||
{% endcall %}
|
||||
{% endif %}
|
||||
{% if current_user.has('credentials.edit') %}
|
||||
{% if current_user.has('credentials.write') %}
|
||||
{% for entry in credentials %}
|
||||
{% call modal('credential-edit-' ~ entry.id, entry.name) %}
|
||||
<form method="post" action="{{ url_for('credentials.update', credential_id=entry.id) }}">
|
||||
|
||||
@@ -11,12 +11,12 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block topbar_extra %}
|
||||
{% if terminal_enabled and current_user.has('maintenance.execute') and device.credential_id %}
|
||||
{% if terminal_enabled and current_user.has('device_console.execute') and device.credential_id %}
|
||||
<a class="btn btn--sm" href="{{ url_for('terminal.device_console', device_id=device.id) }}">
|
||||
{{ icon('terminal', size=15) }} Konsole
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if current_user.has('devices.execute') and restart_methods %}
|
||||
{% if restart_methods %}
|
||||
{% for method in restart_methods %}
|
||||
<form method="post" style="display:inline"
|
||||
action="{{ url_for('devices.restart_device', device_id=device.id) }}" data-no-guard>
|
||||
@@ -138,7 +138,7 @@
|
||||
Ohne freigegebenen Schlüssel verweigern Wartung und Konsole die Verbindung.
|
||||
</div>
|
||||
</div>
|
||||
{% if current_user.has('devices.edit') %}
|
||||
{% if current_user.has('devices.write') %}
|
||||
<form method="post" action="{{ url_for('devices.probe_host_key', device_id=device.id) }}"
|
||||
data-no-guard>
|
||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||
@@ -151,7 +151,7 @@
|
||||
<div class="card__body stack">
|
||||
{% set probed_type = request.args.get('probed_type') %}
|
||||
{% set probed_fingerprint = request.args.get('probed_fingerprint') %}
|
||||
{% if probed_type and probed_fingerprint and current_user.has('devices.edit') %}
|
||||
{% if probed_type and probed_fingerprint and current_user.has('devices.write') %}
|
||||
<div class="alert alert--warning">
|
||||
{{ icon('alert', size=18) }}
|
||||
<div class="alert__body">
|
||||
@@ -182,7 +182,7 @@
|
||||
<dd class="mono fs-xs break-all">{{ key.fingerprint }}</dd>
|
||||
{% endfor %}
|
||||
</dl>
|
||||
{% if current_user.has('devices.edit') %}
|
||||
{% if current_user.has('devices.write') %}
|
||||
<form method="post"
|
||||
action="{{ url_for('devices.forget_host_key', device_id=device.id) }}"
|
||||
data-no-guard>
|
||||
@@ -206,7 +206,7 @@
|
||||
<section class="card">
|
||||
<div class="card__header">
|
||||
<div class="flex-1"><div class="card__title">{{ icon('info', size=17) }} Stammdaten</div></div>
|
||||
{% if current_user.has('devices.edit') %}
|
||||
{% if current_user.has('devices.write') %}
|
||||
<a class="btn btn--sm" href="{{ url_for('devices.overview') }}">Bearbeiten</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -232,7 +232,7 @@
|
||||
</dl>
|
||||
{% if device.notes %}<p class="fs-sm muted mt-3">{{ device.notes }}</p>{% endif %}
|
||||
</div>
|
||||
{% if current_user.has('devices.edit') %}
|
||||
{% if current_user.has('devices.write') %}
|
||||
<div class="card__footer">
|
||||
<form method="post" action="{{ url_for('devices.toggle_active', device_id=device.id) }}" data-no-guard>
|
||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||
|
||||
@@ -213,10 +213,10 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="actions">
|
||||
{% if current_user.has('devices.execute') %}
|
||||
{% if restart_methods.get(device.id) %}
|
||||
{{ restart_control(device, restart_methods.get(device.id, []), compact=True) }}
|
||||
{% endif %}
|
||||
{% if current_user.has('devices.edit') %}
|
||||
{% if current_user.has('devices.write') %}
|
||||
<button class="btn btn--sm btn--ghost" type="button"
|
||||
data-dialog-open="device-edit-{{ device.id }}" aria-label="Bearbeiten">
|
||||
{{ icon('pencil', size=15) }}
|
||||
@@ -263,7 +263,7 @@ nicht -- damit kein Neustart vorgetäuscht wird, der nichts bewirkt.', 'info') }
|
||||
{% endcall %}
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.has('devices.edit') %}
|
||||
{% if current_user.has('devices.write') %}
|
||||
{% for device in devices %}
|
||||
{% call modal('device-edit-' ~ device.id, device.name, device.mac, 'wide') %}
|
||||
<form method="post" action="{{ url_for('devices.update', device_id=device.id) }}">
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<div class="stat">
|
||||
<span class="stat__label">Reservierungen</span>
|
||||
<span class="stat__value">{{ reservations | length }}</span>
|
||||
<span class="stat__hint">automatisch + manüll</span>
|
||||
<span class="stat__hint">automatisch + manuell</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat__label">Leases</span>
|
||||
@@ -39,7 +39,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if current_user.has('dhcp.execute') %}
|
||||
{% if current_user.has('dhcp_service.execute') %}
|
||||
<section class="card">
|
||||
<div class="card__header">
|
||||
<div class="flex-1">
|
||||
@@ -89,7 +89,7 @@
|
||||
<td class="mono fs-sm">{{ subnet.dns or '–' | safe }}</td>
|
||||
<td>{% if subnet.enabled %}{{ pill('aktiv', 'online') }}{% else %}{{ pill('pausiert', '') }}{% endif %}</td>
|
||||
<td class="actions">
|
||||
{% if current_user.has('dhcp.edit') %}
|
||||
{% if current_user.has('dhcp_subnets.write') %}
|
||||
<form method="post" action="{{ url_for('dhcp.delete_subnet', subnet_id=subnet.id) }}"
|
||||
style="display:inline" data-no-guard>
|
||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||
@@ -109,7 +109,7 @@
|
||||
{{ empty_state('Kein Subnetz', 'Ohne Subnetz startet Kea nicht.', 'network') }}
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.has('dhcp.edit') %}
|
||||
{% if current_user.has('dhcp_subnets.create') %}
|
||||
<form method="post" action="{{ url_for('dhcp.add_subnet') }}">
|
||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||
<div class="card__body">
|
||||
@@ -135,7 +135,7 @@
|
||||
</section>
|
||||
|
||||
{# -- Globale Einstellungen --------------------------------------------- #}
|
||||
{% if current_user.has('dhcp.edit') %}
|
||||
{% if current_user.has('dhcp_subnets.write') %}
|
||||
<section class="card">
|
||||
<div class="card__header">
|
||||
<div class="flex-1"><div class="card__title">{{ icon('sliders', size=17) }} Globale Einstellungen</div></div>
|
||||
@@ -183,7 +183,7 @@
|
||||
</td>
|
||||
<td class="fs-xs mono">{{ option.type }}</td>
|
||||
<td>
|
||||
{% if current_user.has('dhcp.edit') %}
|
||||
{% if current_user.has('dhcp_options.write') %}
|
||||
<form method="post" action="{{ url_for('dhcp.set_option_value') }}" class="row row--tight"
|
||||
data-no-guard>
|
||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||
@@ -197,7 +197,7 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="actions">
|
||||
{% if current_user.has('dhcp.edit') and not option.is_standard %}
|
||||
{% if current_user.has('dhcp_options.write') and not option.is_standard %}
|
||||
<form method="post" action="{{ url_for('dhcp.delete_custom_option', option_id=option.id) }}"
|
||||
data-no-guard>
|
||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||
@@ -213,7 +213,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if current_user.has('dhcp.edit') %}
|
||||
{% if current_user.has('dhcp_options.write') %}
|
||||
<form method="post" action="{{ url_for('dhcp.add_custom_option') }}">
|
||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||
<div class="card__body">
|
||||
@@ -339,7 +339,7 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="actions">
|
||||
{% if reservation.source == 'manüll' and current_user.has('dhcp.edit') %}
|
||||
{% if reservation.source == 'manuell' and current_user.has('dhcp_reservations.delete') %}
|
||||
<form method="post" data-no-guard
|
||||
action="{{ url_for('dhcp.delete_reservation', reservation_id=reservation.reservation_id) }}">
|
||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||
@@ -355,7 +355,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if current_user.has('dhcp.edit') %}
|
||||
{% if current_user.has('dhcp_reservations.create') %}
|
||||
<form method="post" action="{{ url_for('dhcp.add_reservation') }}">
|
||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||
<div class="card__body">
|
||||
@@ -434,7 +434,7 @@
|
||||
{{ icon('chevron-down', size=16) }}
|
||||
</summary>
|
||||
<pre class="log-view">{{ preview }}</pre>
|
||||
{% if current_user.has('dhcp.edit') %}
|
||||
{% if current_user.has('dhcp_config.execute') %}
|
||||
<div class="card__footer">
|
||||
<form method="post" action="{{ url_for('dhcp.write_config') }}" data-no-guard>
|
||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% block page_sub %}{{ mounts | length }} eingebundene Freigabe(n){% endblock %}
|
||||
|
||||
{% block topbar_extra %}
|
||||
{% if current_user.has('fileshare.edit') %}
|
||||
{% if current_user.has('fileshare.write') %}
|
||||
<a class="btn btn--sm" href="{{ url_for('fileshare.mappings') }}">
|
||||
{{ icon('settings', size=15) }} Zuordnungen
|
||||
</a>
|
||||
@@ -152,7 +152,7 @@
|
||||
{% if current_user.has('fileshare.export') %}data-fill-Download="{{ url_for('fileshare.download', token=token, path=entry.relative) }}"{% endif %}
|
||||
aria-label="Vorschau">{{ icon('eye', size=15) }}</button>
|
||||
{% endif %}
|
||||
{% if current_user.has('fileshare.edit') %}
|
||||
{% if current_user.has('fileshare.write') %}
|
||||
<button class="btn btn--sm btn--ghost" type="button"
|
||||
data-dialog-open="file-rename"
|
||||
data-fill-Pfad="{{ entry.relative }}"
|
||||
@@ -237,7 +237,7 @@
|
||||
<div class="modal__body modal__body--flush" data-viewer
|
||||
{# Ohne das Recht zum Ausfuellen zeigt die Vorschau das Dokument so,
|
||||
wie es ist -- mit seinen eigenen Marken, ohne Eingabefelder. #}
|
||||
data-viewer-formulare="{{ '1' if current_user.has('fileshare_pdf.view') else '' }}"
|
||||
data-viewer-formulare="{{ '1' if current_user.has('fileshare_pdf.read') else '' }}"
|
||||
data-viewer-pdfjs="{{ asset_url('vendor/pdfjs/pdf.min.js') }}"
|
||||
data-viewer-pdflib="{{ asset_url('vendor/pdflib/pdf-lib.min.js') }}"
|
||||
data-viewer-konto="{{ current_user.username }}"
|
||||
@@ -330,7 +330,7 @@
|
||||
PDF-Formulare/Bearbeiten, "abgeschlossen" an Anlegen. Wer nur
|
||||
anlegen darf, sieht eine Zeile statt einer Auswahl. #}
|
||||
<select class="select" data-viewer-modus>
|
||||
{% if current_user.has('fileshare_pdf.edit') %}
|
||||
{% if current_user.has('fileshare_pdf.write') %}
|
||||
<option value="formular">Formular – weiter ausfüllbar</option>
|
||||
{% endif %}
|
||||
<option value="fertig">Abgeschlossen – Felder gesperrt</option>
|
||||
@@ -374,10 +374,16 @@
|
||||
{{ icon('download', size=15) }} Herunterladen
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if current_user.has('fileshare_pdf.create') %}
|
||||
{# Unterschreiben ist eine eigene Zeile der Matrix: wer ausgefuellte
|
||||
Formulare ablegen darf, darf damit nicht unterschreiben. Der Server
|
||||
weist es ohnehin ab -- aber ein Knopf, der immer in eine Meldung
|
||||
laeuft, ist keiner. #}
|
||||
{% if current_user.has('fileshare_signature.execute') %}
|
||||
<button class="btn btn--sm" type="button" data-viewer-unterschreiben hidden>
|
||||
{{ icon('pencil', size=15) }} Unterschreiben
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if current_user.has('fileshare_pdf.create') %}
|
||||
<button class="btn btn--sm" type="button" data-viewer-speichern hidden
|
||||
data-url="{{ url_for('fileshare.save_form', token=token) }}">
|
||||
{{ icon('save', size=15) }} Ausgefüllt speichern
|
||||
@@ -388,7 +394,7 @@
|
||||
{% endcall %}
|
||||
{% endif %}
|
||||
|
||||
{% if listing and current_user.has('fileshare.edit') %}
|
||||
{% if listing and current_user.has('fileshare.write') %}
|
||||
{% call modal('file-rename', 'Umbenennen', '', 'narrow') %}
|
||||
<form method="post" action="{{ url_for('fileshare.rename', token=token) }}">
|
||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||
|
||||
@@ -33,7 +33,7 @@ Einbindungen.', 'info') }}
|
||||
<td class="fs-sm">{{ entry.ad_group_name }}
|
||||
<div class="fs-xs faint break-all">{{ entry.ad_group_dn }}</div></td>
|
||||
<td class="actions">
|
||||
{% if current_user.has('fileshare.edit') %}
|
||||
{% if current_user.has('fileshare.write') %}
|
||||
<button class="btn btn--sm btn--ghost" type="button"
|
||||
data-dialog-open="mapping-edit-{{ entry.id }}"
|
||||
aria-label="Bearbeiten">{{ icon('pencil', size=15) }}</button>
|
||||
@@ -80,7 +80,7 @@ Einbindungen.', 'info') }}
|
||||
{# Bearbeiten statt neu anlegen: bei einem Tippfehler im UNC-Pfad oder im
|
||||
Anzeigenamen soll man ihn korrigieren können. Sonst legt man daneben einen
|
||||
zweiten Eintrag an und vergisst, den ersten zu löschen. #}
|
||||
{% if current_user.has('fileshare.edit') %}
|
||||
{% if current_user.has('fileshare.write') %}
|
||||
{% for entry in mappings %}
|
||||
{% call modal('mapping-edit-' ~ entry.id, entry.label, 'Zuordnung ändern') %}
|
||||
<form method="post" action="{{ url_for('fileshare.mapping_update', mapping_id=entry.id) }}">
|
||||
|
||||
@@ -160,7 +160,7 @@ wird nie stillschweigend übernommen: das ist der Fall, der bei einem Angriff au
|
||||
<td class="mono fs-xs break-all">{{ row.fingerprint }}</td>
|
||||
<td class="fs-sm">{{ row.added_at | dt }} <span class="faint">von {{ row.added_by }}</span></td>
|
||||
<td>
|
||||
{% if current_user.has('devices.edit') or current_user.has('switches.edit') %}
|
||||
{% if current_user.has('devices.write') or current_user.has('switches.write') %}
|
||||
<form method="post" action="{{ url_for('hostkeys.forget_orphan') }}" data-no-guard>
|
||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="host" value="{{ row.host }}">
|
||||
|
||||
@@ -76,7 +76,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('license.activate') }}" data-no-guard>
|
||||
@@ -155,7 +155,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">
|
||||
@@ -190,7 +190,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">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% block topbar_extra %}{{ search_box('restart-table', 'Gerät, Ergebnis, Methode ...') }}{% endblock %}
|
||||
{% block content %}
|
||||
{{ alert('Jeder Neustart wird mit Methode, Ergebnis, Dauer und Auslöser festgehalten -- automatisch
|
||||
wie manüll, über den PoE-Port des Switches ebenso wie per Neustart-Befehl aus der Wartung.
|
||||
wie manuell, über den PoE-Port des Switches ebenso wie per Neustart-Befehl aus der Wartung.
|
||||
Der Vorgänger protokollierte Neustarts ausdrücklich nicht.', 'info') }}
|
||||
|
||||
<section class="card">
|
||||
|
||||
@@ -54,7 +54,7 @@ Druckerfreigabe. Der PoE-Neustart ist davon getrennt: er schaltet den Switch-Por
|
||||
<span>Alle auswählen</span>
|
||||
</label>
|
||||
<span class="table-toolbar__spacer"></span>
|
||||
{% if current_user.has('maintenance.execute') %}
|
||||
{% if current_user.has('servicing_update.execute') %}
|
||||
<button class="btn btn--sm btn--primary" type="submit"
|
||||
formaction="{{ url_for('maintenance.start_update') }}"
|
||||
title="Nur für Linux-Clients -- Windows-Geräte werden dabei übersprungen."
|
||||
@@ -91,7 +91,7 @@ Druckerfreigabe. Der PoE-Neustart ist davon getrennt: er schaltet den Switch-Por
|
||||
<label class="check">
|
||||
<input type="checkbox" name="device_ids" value="{{ device.id }}"
|
||||
class="device-select" data-category="{{ device.category }}"
|
||||
{% if not current_user.has('maintenance.execute') %}disabled{% endif %}>
|
||||
{% if not current_user.has('servicing_update.execute') %}disabled{% endif %}>
|
||||
<span class="sr-only">{{ device.name }} auswählen</span>
|
||||
</label>
|
||||
</td>
|
||||
@@ -121,7 +121,7 @@ Druckerfreigabe. Der PoE-Neustart ist davon getrennt: er schaltet den Switch-Por
|
||||
</td>
|
||||
<td class="nowrap">
|
||||
{% if terminal_enabled and device.ssh_managed
|
||||
and current_user.has('maintenance.execute') %}
|
||||
and current_user.has('servicing_update.execute') %}
|
||||
<a class="btn btn--sm btn--ghost"
|
||||
href="{{ url_for('terminal.device_console', device_id=device.id) }}">
|
||||
{{ icon('terminal', size=14) }} Konsole
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
</div>
|
||||
{{ checkbox('monitor_auto_restart', 'Automatischen PoE-Neustart bei Ausfall aktivieren',
|
||||
checked=values.monitor_auto_restart,
|
||||
hint_text='Zusätzlich lizenzpflichtig. Der manülle Knopf im Dashboard
|
||||
hint_text='Zusätzlich lizenzpflichtig. Der manuelle Knopf im Dashboard
|
||||
funktioniert unabhängig davon.') }}
|
||||
{{ checkbox('dashboard_public', 'Statusübersicht ohne Anmeldung erreichbar',
|
||||
checked=values.dashboard_public,
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block topbar_extra %}
|
||||
{% if terminal_enabled and current_user.has('switches.execute') and host_keys %}
|
||||
{% if terminal_enabled and current_user.has('switch_console.execute') and host_keys %}
|
||||
<a class="btn btn--sm btn--primary" href="{{ url_for('terminal.switch_console', switch_id=switch.id) }}">
|
||||
{{ icon('terminal', size=15) }} Konsole
|
||||
</a>
|
||||
@@ -32,7 +32,7 @@
|
||||
Ohne freigegebenen Schlüssel verweigert die Anwendung jeden Zugriff auf diesen Switch.
|
||||
</div>
|
||||
</div>
|
||||
{% if current_user.has('switches.edit') %}
|
||||
{% if current_user.has('switches.write') %}
|
||||
<form method="post" action="{{ url_for('switches.probe_host_key', switch_id=switch.id) }}"
|
||||
data-no-guard>
|
||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||
@@ -53,7 +53,7 @@
|
||||
Vergleichen Sie den Fingerprint mit einer unabhängigen Quelle (Konsolenzugang am
|
||||
Switch, Inventardokumentation), bevor Sie ihn freigeben.
|
||||
</p>
|
||||
{% if current_user.has('switches.edit') %}
|
||||
{% if current_user.has('switches.write') %}
|
||||
<form method="post" class="mt-2"
|
||||
action="{{ url_for('switches.trust_host_key', switch_id=switch.id) }}">
|
||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||
@@ -78,7 +78,7 @@
|
||||
</dd>
|
||||
{% endfor %}
|
||||
</dl>
|
||||
{% if current_user.has('switches.edit') %}
|
||||
{% if current_user.has('switches.write') %}
|
||||
<form method="post" action="{{ url_for('switches.forget_host_key', switch_id=switch.id) }}"
|
||||
data-no-guard>
|
||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||
@@ -95,7 +95,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% if current_user.has('switches.execute') %}
|
||||
{% if current_user.has('switch_console.execute') %}
|
||||
<section class="card">
|
||||
<div class="card__header">
|
||||
<div class="flex-1">
|
||||
|
||||
@@ -83,13 +83,13 @@ stillschweigend übernommen.', 'info', 'Host-Schlüssel') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="actions">
|
||||
{% if terminal_enabled and current_user.has('switches.execute') and switch.host_key_known %}
|
||||
{% if terminal_enabled and current_user.has('switch_console.execute') and switch.host_key_known %}
|
||||
<a class="btn btn--sm btn--ghost" title="Konsole"
|
||||
href="{{ url_for('terminal.switch_console', switch_id=switch.id) }}">
|
||||
{{ icon('terminal', size=15) }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if current_user.has('switches.edit') %}
|
||||
{% if current_user.has('switches.write') %}
|
||||
<button class="btn btn--sm btn--ghost" type="button"
|
||||
data-dialog-open="switch-edit-{{ switch.id }}" aria-label="Bearbeiten">
|
||||
{{ icon('pencil', size=15) }}
|
||||
@@ -135,7 +135,7 @@ stillschweigend übernommen.', 'info', 'Host-Schlüssel') }}
|
||||
</form>
|
||||
{% endcall %}
|
||||
{% endif %}
|
||||
{% if current_user.has('switches.edit') %}
|
||||
{% if current_user.has('switches.write') %}
|
||||
{% for switch in switches %}
|
||||
{% call modal('switch-edit-' ~ switch.id, switch.hostname, switch.ip) %}
|
||||
<form method="post" action="{{ url_for('switches.update', switch_id=switch.id) }}">
|
||||
|
||||
Reference in new issue
Block a user