Fix: optionen-Update überschrieb bisherige Werte statt sie zu ergänzen + OE-/Gruppen-Select-Endpunkte akzeptieren jetzt optionen-Payload
Zwei zusammenhängende Fixes, beide Voraussetzung für die neue
optionen-Eingabe-UI in kundenplattform (struktur.py/gruppen.py):
1. set_auftrag_selection()/set_oe_auswahl()/set_gruppe_auswahl()
ersetzten optionen bisher komplett (kein natives JSONB-Merge im
UPSERT) - ein im Formular leer gelassenes Passwort-Feld
(parse_optionen_form() lässt solche Keys bewusst weg, siehe
optionen_felder.py) hätte ein zuvor gesetztes Passwort beim nächsten
Bearbeiten stillschweigend gelöscht. Neue merge_optionen()-Funktion:
bestehenden Wert holen, mit den neu eingegebenen Feldern
zusammenführen, erst dann verschlüsseln (encrypt_sensitive_optionen()
dafür idempotent gemacht - erkennt bereits "enc:"-präfigierte Werte
und verschlüsselt nicht doppelt).
2. /api/v1/organisationseinheiten/{oe_id}/merkmale/{merkmal_key}/select
und die Gruppen-Entsprechung nahmen bisher gar keinen Payload-Body
entgegen (hartcodiert leeres optionen-Dict) - jetzt wie der
geräteweise Endpunkt mit AuftragSelectRequest. Ohne diesen Fix hätte
die neue OE-/Gruppen-UI (Kundenplattform) eingegebene AD-Domäne o.ä.
nie beim Server ankommen können.
Zusätzlich: fetch_oe_merkmale_listing()/fetch_gruppe_merkmale_listing()
liefern jetzt auch optionen (maskiert, siehe mask_sensitive_optionen())
für die "bereits gesetzt"-Anzeige im Formular.
ECHT gegen die Produktions-DB getestet (nicht nur Code-Review): OE-Ebene
erst mit Domain+Passwort setzen, dann NUR die Domain erneut setzen ohne
Passwort-Feld - Passwort blieb korrekt erhalten, Domain wurde aktualisiert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLk93dhDMrkPXefVdknMV9
This commit is contained in:
1 parent
d970936b7c
commit
b270103eab
1 file changed
+77
-20
@@ -881,14 +881,32 @@ def encrypt_sensitive_optionen(optionen: dict) -> dict:
|
||||
gruppen_merkmale/device_merkmale.optionen landet - siehe
|
||||
decrypt_sensitive_optionen() für die Gegenrichtung. Nur String-Werte
|
||||
werden angefasst, alles andere bleibt unverändert (z.B. Booleans wie
|
||||
bei brave/standardbrowser)."""
|
||||
bei brave/standardbrowser). Idempotent (bereits "enc:"-präfigierte
|
||||
Werte werden nicht erneut verschlüsselt) - wichtig für
|
||||
merge_optionen(), das teilweise schon verschlüsselte Altwerte mit
|
||||
frischem Klartext aus einem Formular zusammenführt, bevor hier
|
||||
verschlüsselt wird."""
|
||||
ergebnis = dict(optionen)
|
||||
for key, value in optionen.items():
|
||||
if key.endswith(SENSITIVE_OPTIONEN_SUFFIXES) and isinstance(value, str) and value:
|
||||
if key.endswith(SENSITIVE_OPTIONEN_SUFFIXES) and isinstance(value, str) and value and not value.startswith("enc:"):
|
||||
ergebnis[key] = "enc:" + _fernet().encrypt(value.encode("utf-8")).decode("ascii")
|
||||
return ergebnis
|
||||
|
||||
|
||||
def merge_optionen(bestehend: dict, neu: dict) -> dict:
|
||||
"""Führt neu eingegebene optionen-Werte mit den bereits gespeicherten
|
||||
zusammen - set_oe_auswahl()/set_gruppe_auswahl()/set_auftrag_selection()
|
||||
ersetzen den kompletten optionen-Wert (kein natives JSONB-Merge im
|
||||
UPSERT), ein leer gelassenes Passwort-Feld im Formular
|
||||
(parse_optionen_form() in kundenplattform/optionen_felder.py lässt
|
||||
solche Keys komplett weg) würde ohne dieses Merge ein zuvor gesetztes
|
||||
Passwort stillschweigend löschen. Checkbox-Felder sind davon nicht
|
||||
betroffen (die werden vom Formular immer explizit mitgeschickt)."""
|
||||
ergebnis = dict(bestehend)
|
||||
ergebnis.update(neu)
|
||||
return ergebnis
|
||||
|
||||
|
||||
def decrypt_sensitive_optionen(optionen: dict) -> dict:
|
||||
"""Gegenrichtung zu encrypt_sensitive_optionen() - wird beim
|
||||
Zusammenstellen der Checkin-Antwort aufgerufen, damit der Agent (und
|
||||
@@ -1190,8 +1208,6 @@ def set_auftrag_selection(device_id: str, merkmal_key: str, aktiv: bool, optione
|
||||
existiert (unbekannter Key oder im_auftragskatalog = false).
|
||||
"""
|
||||
|
||||
optionen = encrypt_sensitive_optionen(optionen)
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
@@ -1205,6 +1221,19 @@ def set_auftrag_selection(device_id: str, merkmal_key: str, aktiv: bool, optione
|
||||
|
||||
merkmal_id = merkmal_row[0]
|
||||
|
||||
# Bestehende optionen holen und mit den neu eingegebenen
|
||||
# zusammenführen (siehe merge_optionen()) - sonst würde ein leer
|
||||
# gelassenes Passwort-Feld im Formular ein zuvor gesetztes
|
||||
# Passwort löschen, statt es unverändert zu lassen.
|
||||
cur.execute(
|
||||
"SELECT optionen FROM device_merkmale WHERE device_id = %s AND merkmal_id = %s",
|
||||
(device_id, merkmal_id),
|
||||
)
|
||||
bestehend_row = cur.fetchone()
|
||||
optionen = encrypt_sensitive_optionen(
|
||||
merge_optionen(bestehend_row[0] if bestehend_row else {}, optionen)
|
||||
)
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO device_merkmale (id, device_id, merkmal_id, aktiv, optionen)
|
||||
@@ -1381,8 +1410,6 @@ def delete_organisationseinheit(oe_id: str):
|
||||
def set_oe_auswahl(oe_id: str, merkmal_key: str, aktiv: bool, optionen: dict):
|
||||
"""Setzt die OE-weite Auswahl eines katalogfähigen Merkmals (Upsert in oe_merkmale), analog zu set_auftrag_selection."""
|
||||
|
||||
optionen = encrypt_sensitive_optionen(optionen)
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
@@ -1394,6 +1421,19 @@ def set_oe_auswahl(oe_id: str, merkmal_key: str, aktiv: bool, optionen: dict):
|
||||
if merkmal_row is None:
|
||||
return False
|
||||
|
||||
merkmal_id = merkmal_row[0]
|
||||
|
||||
# Merge statt Ersetzen, siehe set_auftrag_selection() für die
|
||||
# ausführliche Begründung (leeres Passwort-Feld != löschen).
|
||||
cur.execute(
|
||||
"SELECT optionen FROM oe_merkmale WHERE oe_id = %s AND merkmal_id = %s",
|
||||
(oe_id, merkmal_id),
|
||||
)
|
||||
bestehend_row = cur.fetchone()
|
||||
optionen = encrypt_sensitive_optionen(
|
||||
merge_optionen(bestehend_row[0] if bestehend_row else {}, optionen)
|
||||
)
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO oe_merkmale (oe_id, merkmal_id, aktiv, optionen)
|
||||
@@ -1401,7 +1441,7 @@ def set_oe_auswahl(oe_id: str, merkmal_key: str, aktiv: bool, optionen: dict):
|
||||
ON CONFLICT (oe_id, merkmal_id) DO UPDATE
|
||||
SET aktiv = EXCLUDED.aktiv, optionen = EXCLUDED.optionen
|
||||
""",
|
||||
(oe_id, merkmal_row[0], aktiv, Jsonb(optionen)),
|
||||
(oe_id, merkmal_id, aktiv, Jsonb(optionen)),
|
||||
)
|
||||
|
||||
return True
|
||||
@@ -1440,7 +1480,7 @@ def fetch_oe_merkmale_listing(oe_id: str):
|
||||
"""
|
||||
SELECT m.key, m.name, m.description,
|
||||
k.key, k.name, k.sort_order,
|
||||
om.aktiv
|
||||
om.aktiv, COALESCE(om.optionen, '{}'::jsonb)
|
||||
FROM merkmale m
|
||||
LEFT JOIN kategorien k ON k.id = m.kategorie_id
|
||||
LEFT JOIN oe_merkmale om ON om.merkmal_id = m.id AND om.oe_id = %s
|
||||
@@ -1459,8 +1499,12 @@ def fetch_oe_merkmale_listing(oe_id: str):
|
||||
if kat_key is not None else None
|
||||
),
|
||||
"aktiv": aktiv,
|
||||
# Wie im Auftragskatalog-Listing bewusst maskiert (siehe
|
||||
# mask_sensitive_optionen()) - nie Klartext/verschlüsselten
|
||||
# Rohwert an den Browser.
|
||||
"optionen": mask_sensitive_optionen(optionen),
|
||||
}
|
||||
for key, name, description, kat_key, kat_name, sort_order, aktiv in cur.fetchall()
|
||||
for key, name, description, kat_key, kat_name, sort_order, aktiv, optionen in cur.fetchall()
|
||||
]
|
||||
|
||||
def move_device_to_oe(device_id: str, oe_id: str):
|
||||
@@ -1526,8 +1570,6 @@ def delete_gruppe(gruppe_id: str):
|
||||
return cur.fetchone() is not None
|
||||
|
||||
def set_gruppe_auswahl(gruppe_id: str, merkmal_key: str, aktiv: bool, optionen: dict):
|
||||
optionen = encrypt_sensitive_optionen(optionen)
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
@@ -1539,6 +1581,19 @@ def set_gruppe_auswahl(gruppe_id: str, merkmal_key: str, aktiv: bool, optionen:
|
||||
if merkmal_row is None:
|
||||
return False
|
||||
|
||||
merkmal_id = merkmal_row[0]
|
||||
|
||||
# Merge statt Ersetzen, siehe set_auftrag_selection() für die
|
||||
# ausführliche Begründung (leeres Passwort-Feld != löschen).
|
||||
cur.execute(
|
||||
"SELECT optionen FROM gruppen_merkmale WHERE gruppe_id = %s AND merkmal_id = %s",
|
||||
(gruppe_id, merkmal_id),
|
||||
)
|
||||
bestehend_row = cur.fetchone()
|
||||
optionen = encrypt_sensitive_optionen(
|
||||
merge_optionen(bestehend_row[0] if bestehend_row else {}, optionen)
|
||||
)
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO gruppen_merkmale (gruppe_id, merkmal_id, aktiv, optionen)
|
||||
@@ -1546,7 +1601,7 @@ def set_gruppe_auswahl(gruppe_id: str, merkmal_key: str, aktiv: bool, optionen:
|
||||
ON CONFLICT (gruppe_id, merkmal_id) DO UPDATE
|
||||
SET aktiv = EXCLUDED.aktiv, optionen = EXCLUDED.optionen
|
||||
""",
|
||||
(gruppe_id, merkmal_row[0], aktiv, Jsonb(optionen)),
|
||||
(gruppe_id, merkmal_id, aktiv, Jsonb(optionen)),
|
||||
)
|
||||
|
||||
return True
|
||||
@@ -1577,7 +1632,7 @@ def fetch_gruppe_merkmale_listing(gruppe_id: str):
|
||||
"""
|
||||
SELECT m.key, m.name, m.description,
|
||||
k.key, k.name, k.sort_order,
|
||||
gm.aktiv
|
||||
gm.aktiv, COALESCE(gm.optionen, '{}'::jsonb)
|
||||
FROM merkmale m
|
||||
LEFT JOIN kategorien k ON k.id = m.kategorie_id
|
||||
LEFT JOIN gruppen_merkmale gm ON gm.merkmal_id = m.id AND gm.gruppe_id = %s
|
||||
@@ -1596,8 +1651,9 @@ def fetch_gruppe_merkmale_listing(gruppe_id: str):
|
||||
if kat_key is not None else None
|
||||
),
|
||||
"aktiv": aktiv,
|
||||
"optionen": mask_sensitive_optionen(optionen),
|
||||
}
|
||||
for key, name, description, kat_key, kat_name, sort_order, aktiv in cur.fetchall()
|
||||
for key, name, description, kat_key, kat_name, sort_order, aktiv, optionen in cur.fetchall()
|
||||
]
|
||||
|
||||
def add_device_to_gruppe(device_id: str, gruppe_id: str):
|
||||
@@ -4259,16 +4315,16 @@ def delete_organisationseinheit_endpoint(oe_id: str, authorization: str | None =
|
||||
def select_oe_merkmal(
|
||||
oe_id: str,
|
||||
merkmal_key: str,
|
||||
payload: AuftragSelectRequest = AuftragSelectRequest(),
|
||||
authorization: str | None = Header(default=None),
|
||||
):
|
||||
# Kein Payload-Body wie beim geräteweisen select (siehe
|
||||
# AuftragSelectRequest/OPTIONEN_FELDER in kundenplattform) - die
|
||||
# OE-/Gruppen-Editoren bieten keine Merkmal-Optionen an, nur
|
||||
# aktiv/inaktiv/nicht festgelegt.
|
||||
# Seit 10.09.2026 mit optionen-Payload (vorher hartcodiert {}) - siehe
|
||||
# AD/OIDC/Fileserver-Merkmale, die auf OE-Ebene echte Konfigurationswerte
|
||||
# brauchen (Domäne, Server-Adresse etc.), nicht nur aktiv/inaktiv.
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
if not set_oe_auswahl(oe_id, merkmal_key, True, {}):
|
||||
if not set_oe_auswahl(oe_id, merkmal_key, True, payload.optionen):
|
||||
return {"success": False, "error": "unknown_merkmal", "message": f"Kein katalogfähiges Merkmal mit Key: {merkmal_key}"}
|
||||
|
||||
return {"success": True}
|
||||
@@ -4365,12 +4421,13 @@ def delete_gruppe_endpoint(gruppe_id: str, authorization: str | None = Header(de
|
||||
def select_gruppe_merkmal(
|
||||
gruppe_id: str,
|
||||
merkmal_key: str,
|
||||
payload: AuftragSelectRequest = AuftragSelectRequest(),
|
||||
authorization: str | None = Header(default=None),
|
||||
):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
if not set_gruppe_auswahl(gruppe_id, merkmal_key, True, {}):
|
||||
if not set_gruppe_auswahl(gruppe_id, merkmal_key, True, payload.optionen):
|
||||
return {"success": False, "error": "unknown_merkmal", "message": f"Kein katalogfähiges Merkmal mit Key: {merkmal_key}"}
|
||||
|
||||
return {"success": True}
|
||||
|
||||
Reference in new issue
Block a user