From dd5c5629f34c26a2dade23b42c018145f71b7726 Mon Sep 17 00:00:00 2001 From: Thomas Stallinger Date: Sun, 25 Oct 2026 12:41:05 +0100 Subject: [PATCH] =?UTF-8?q?Einstieg=20Schritt=202:=20WLAN=20der=20Organisa?= =?UTF-8?q?tion=20als=20Einstellung,=20an=20alle=20Ger=C3=A4te=20(Rolle=20?= =?UTF-8?q?org-wlan)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migration 0074: organizations.org_wlan (Optionen der Rolle, Passwort verschlüsselt). GET/PUT /api/v1/organizations/{id}/org-wlan; im Auftragskatalog gilt es für org-wlan, solange OE/Gruppe nichts festlegt. Co-Authored-By: Claude Opus 5.5 --- app.py | 109 +++++++++++++++++++++++++++++++++++ migrations/0074_org_wlan.sql | 16 +++++ 2 files changed, 125 insertions(+) create mode 100644 migrations/0074_org_wlan.sql diff --git a/app.py b/app.py index 4155690..34e0e10 100644 --- a/app.py +++ b/app.py @@ -1645,6 +1645,11 @@ def fetch_auftragskatalog_state(device_id: str): aktiv = workspace_default optionen = {} + if key == ORG_WLAN_MERKMAL and merkmal_id not in overrides: + org_wlan = org_wlan_fuer_geraet(device_id) + if org_wlan: + aktiv, optionen = True, org_wlan + optionen = decrypt_sensitive_optionen(optionen) if key == VEYON_MERKMAL and aktiv: optionen = veyon_optionen_anreichern(device_id, merkmal_id, optionen) @@ -4173,6 +4178,87 @@ def create_organization(name: str, kundengruppe_id: str | None = None): return {"id": str(organization_id), "name": name, "standard_workspace": standard} +# WLAN der Organisation (Einstieg vereinfachen, Schritt 2, Migration 0074). +# Gespeichert in der Form der Merkmal-Optionen der Rolle org-wlan; ein am +# Merkmal selbst (OE/Gruppe) gesetzter Wert hat Vorrang. +ORG_WLAN_MERKMAL = "org-wlan" + + +def org_wlan_pruefen(ssid: str, passwort: str, offen: bool) -> str | None: + if not ssid: + return None + if len(ssid.encode("utf-8")) > 32 or "\n" in ssid or "\r" in ssid: + return "Der WLAN-Name (SSID) darf höchstens 32 Bytes lang sein." + if offen: + return None + if not passwort: + return "Bitte das WLAN-Passwort eingeben oder „Offenes WLAN“ wählen." + if "\n" in passwort or "\r" in passwort or not (8 <= len(passwort) <= 63 or re.fullmatch(r"[0-9A-Fa-f]{64}", passwort)): + return "Das WLAN-Passwort muss 8 bis 63 Zeichen lang sein." + return None + + +def fetch_org_wlan(organization_id: str, mit_passwort: bool = False) -> dict | None: + """{"ssid", "offen", "hat_passwort"[, "passwort"]} oder None ohne WLAN.""" + + with get_database_connection() as conn: + with conn.cursor() as cur: + cur.execute("SELECT org_wlan FROM organizations WHERE id = %s", (organization_id,)) + row = cur.fetchone() + if not row or not row[0] or not row[0].get("wlan_ssid"): + return None + gespeichert = row[0] + ergebnis = { + "ssid": gespeichert["wlan_ssid"], + "offen": not gespeichert.get("wlan_passwort"), + "hat_passwort": bool(gespeichert.get("wlan_passwort")), + } + if mit_passwort: + ergebnis["passwort"] = decrypt_sensitive_optionen(gespeichert).get("wlan_passwort") or "" + return ergebnis + + +def set_org_wlan(organization_id: str, ssid: str, passwort: str, offen: bool) -> str | None: + """Speichert das WLAN der Organisation; leere SSID entfernt es. Ein leeres + Passwort bei gleicher SSID behält das gespeicherte. Fehlermeldung oder None.""" + + ssid = (ssid or "").strip() + with get_database_connection() as conn: + with conn.cursor() as cur: + cur.execute("SELECT org_wlan FROM organizations WHERE id = %s FOR UPDATE", (organization_id,)) + row = cur.fetchone() + if row is None: + return "Die Organisation wurde nicht gefunden." + bisher = row[0] or {} + if ssid and not offen and not passwort and bisher.get("wlan_ssid") == ssid and bisher.get("wlan_passwort"): + neu = bisher + else: + fehler = org_wlan_pruefen(ssid, passwort, offen) + if fehler: + return fehler + neu = encrypt_sensitive_optionen({"wlan_ssid": ssid, "wlan_passwort": "" if offen else passwort}) if ssid else None + cur.execute( + "UPDATE organizations SET org_wlan = %s, updated_at = now() WHERE id = %s", + (Jsonb(neu) if neu else None, organization_id), + ) + return None + + +def org_wlan_fuer_geraet(device_id: str) -> dict | None: + """Optionen für die Rolle org-wlan aus dem WLAN der Organisation des Geräts.""" + + with get_database_connection() as conn: + with conn.cursor() as cur: + cur.execute( + "SELECT o.org_wlan FROM devices d JOIN organizations o ON o.id = d.organization_id WHERE d.id = %s", + (device_id,), + ) + row = cur.fetchone() + if not row or not row[0] or not row[0].get("wlan_ssid"): + return None + return dict(row[0]) + + def standard_ausstattung_anlegen(cur, organization_id, standard_oe_id, kundengruppe_id) -> str | None: """ Einstieg vereinfachen, Schritt 1 (25.10.2026): Hat die Art der @@ -8021,6 +8107,29 @@ def get_organization_endpoint(organization_id: str, authorization: str | None = return {"success": True, "organization": organization} +class OrgWlanRequest(BaseModel): + ssid: str = "" + passwort: str = "" + offen: bool = False + + +@app.get("/api/v1/organizations/{organization_id}/org-wlan") +def get_org_wlan_endpoint(organization_id: str, mit_passwort: bool = False, authorization: str | None = Header(default=None)): + if not require_service_token(authorization): + return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."} + return {"success": True, "org_wlan": fetch_org_wlan(organization_id, mit_passwort)} + + +@app.put("/api/v1/organizations/{organization_id}/org-wlan") +def put_org_wlan_endpoint(organization_id: str, payload: OrgWlanRequest, authorization: str | None = Header(default=None)): + if not require_service_token(authorization): + return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."} + fehler = set_org_wlan(organization_id, payload.ssid, payload.passwort, payload.offen) + if fehler: + return {"success": False, "error": "ungueltig", "message": fehler} + return {"success": True, "org_wlan": fetch_org_wlan(organization_id)} + + class SetAusrollstufeRequest(BaseModel): ausrollstufe: str diff --git a/migrations/0074_org_wlan.sql b/migrations/0074_org_wlan.sql new file mode 100644 index 0000000..66d4294 --- /dev/null +++ b/migrations/0074_org_wlan.sql @@ -0,0 +1,16 @@ +BEGIN; + +-- Einstieg vereinfachen, Schritt 2 (platform-docs roadmap/einstieg- +-- vereinfachen.md, 25.10.2026): Das WLAN der Organisation ist eine +-- Einstellung der Organisation, nicht mehr nur das WLAN vom +-- Installationsmedium. Alle Geräte bekommen es über die Rolle org-wlan, +-- auch per Kabel installierte (Befund Liebherr-83, 14.10.2026). +-- +-- Form wie die Merkmal-Optionen der Rolle: {"wlan_ssid", "wlan_passwort"} +-- mit "enc:"-verschlüsseltem Passwort (encrypt_sensitive_optionen); NULL = +-- kein WLAN der Organisation. Ein am Merkmal "Org-WLAN" (OE/Gruppe) +-- gesetzter Wert hat weiter Vorrang. + +ALTER TABLE organizations ADD COLUMN org_wlan JSONB; + +COMMIT;