Personalisierung: Passwort-Reset durch die Verwaltung (Migration 0066, ADR-0041 18.10.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-18 09:57:54 +02:00
1 parent 2a8514a1a0
commit 6ea1f876e5
2 files changed
+103 -5

No files matched your search

+81 -5
View File
@@ -5943,7 +5943,9 @@ def personalisierung_anmeldung_anbieten(device_id: str) -> bool:
PERSONALISIERUNG_SPALTEN = (
"p.id, p.device_id, p.status, p.code_gueltig_bis, p.bestehenden_behalten, p.erstellt_von, "
"p.erstellt_am, p.email, p.anmeldename, p.eingereicht_am, p.bestaetigung_bis, p.bestaetigt_am, "
"p.angelegt_am, p.beendet_am, p.gesperrt, p.entfernen, p.geraet_meldung"
"p.angelegt_am, p.beendet_am, p.gesperrt, p.entfernen, p.geraet_meldung, "
"p.reset_angefordert_am, p.reset_angefordert_von, p.reset_gesetzt_am, "
"(p.reset_passwort_hash IS NOT NULL), p.reset_aendern"
)
@@ -5969,6 +5971,11 @@ def personalisierung_als_dict(row) -> dict:
"gesperrt": row[14],
"entfernen": row[15],
"geraet_meldung": row[16],
"reset_angefordert_am": zeit(row[17]),
"reset_angefordert_von": row[18],
"reset_gesetzt_am": zeit(row[19]),
"reset_offen": row[20],
"reset_aendern": row[21],
}
@@ -6207,7 +6214,8 @@ def fetch_persoenliche_konten_fuer_agent(device_id: str) -> list[dict]:
personalisierung_verfallene_markieren(cur)
cur.execute(
"""
SELECT id, status, anmeldename, passwort_hash, gesperrt, entfernen
SELECT id, status, anmeldename, passwort_hash, gesperrt, entfernen,
reset_id, reset_passwort_hash, reset_aendern
FROM personalisierungen
WHERE device_id = %s AND status IN ('bestaetigt', 'angelegt')
ORDER BY erstellt_am
@@ -6217,14 +6225,23 @@ def fetch_persoenliche_konten_fuer_agent(device_id: str) -> list[dict]:
rows = cur.fetchall()
konten = []
for pid, status, anmeldename, passwort_hash, gesperrt, entfernen in rows:
for pid, status, anmeldename, passwort_hash, gesperrt, entfernen, reset_id, reset_hash, reset_aendern in rows:
if status == "bestaetigt":
if not passwort_hash:
continue
konten.append({"id": str(pid), "anmeldename": anmeldename, "aktion": "anlegen", "passwort_hash": passwort_hash})
else:
aktion = "entfernen" if entfernen else "sperren" if gesperrt else "aktiv"
konten.append({"id": str(pid), "anmeldename": anmeldename, "aktion": aktion})
konto = {"id": str(pid), "anmeldename": anmeldename, "aktion": aktion}
# Passwort-Reset der Verwaltung (Migration 0066): einmalig, bis
# das Gerät die reset_id zurückmeldet.
if reset_hash and not entfernen:
konto["passwort_reset"] = {
"id": str(reset_id),
"passwort_hash": reset_hash,
"aendern": reset_aendern,
}
konten.append(konto)
return konten
@@ -6281,6 +6298,18 @@ def record_persoenliche_konten_ergebnisse(device_id: str, ergebnisse: list) -> N
elif eintrag.ergebnis in ("aktiv", "gesperrt"):
cur.execute("UPDATE personalisierungen SET geraet_meldung = NULL WHERE id = %s", (eintrag.id,))
# Nur der gemeldete Reset ist erledigt; ein neuerer (andere
# reset_id) bleibt stehen.
if eintrag.passwort_reset_id and eintrag.ergebnis in ("aktiv", "gesperrt"):
cur.execute(
"""
UPDATE personalisierungen
SET reset_passwort_hash = NULL, reset_gesetzt_am = now()
WHERE id = %s AND reset_id::text = %s AND reset_passwort_hash IS NOT NULL
""",
(eintrag.id, eintrag.passwort_reset_id),
)
def set_personalisierung_gesperrt(device_id: str, personalisierung_id: str, gesperrt: bool) -> bool:
with get_database_connection() as conn:
@@ -6295,6 +6324,23 @@ def set_personalisierung_gesperrt(device_id: str, personalisierung_id: str, gesp
return cur.rowcount > 0
def set_personalisierung_passwort_reset(
device_id: str, personalisierung_id: str, passwort_hash: str, aendern: bool, angefordert_von: str | None
) -> bool:
with get_database_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"""
UPDATE personalisierungen
SET reset_id = %s, reset_passwort_hash = %s, reset_aendern = %s,
reset_angefordert_am = now(), reset_angefordert_von = %s, reset_gesetzt_am = NULL
WHERE id = %s AND device_id = %s AND status = 'angelegt' AND NOT entfernen
""",
(uuid4(), passwort_hash, aendern, angefordert_von, personalisierung_id, device_id),
)
return cur.rowcount > 0
def personalisierungen_beenden_bei_neuinstallation(cur, device_id) -> None:
"""
ADR-0041: Der Fingerprint ist hardwarebasiert, ein neu installiertes
@@ -6304,7 +6350,8 @@ def personalisierungen_beenden_bei_neuinstallation(cur, device_id) -> None:
cur.execute(
"""
UPDATE personalisierungen
SET status = 'beendet', beendet_am = now(), passwort_hash = NULL, bestaetigung_token_hash = NULL
SET status = 'beendet', beendet_am = now(), passwort_hash = NULL, bestaetigung_token_hash = NULL,
reset_passwort_hash = NULL
WHERE device_id = %s AND status IN ('code_offen', 'eingereicht', 'bestaetigt', 'angelegt')
""",
(device_id,),
@@ -6315,6 +6362,14 @@ class PersoenlichesKontoErgebnis(BaseModel):
id: str
ergebnis: str
meldung: str | None = None
# Gesetzt, wenn die Rolle den Passwort-Reset mit dieser ID ausgeführt hat.
passwort_reset_id: str | None = None
class PasswortResetRequest(BaseModel):
passwort_hash: str
aendern: bool = True
angefordert_von: str | None = None
class AgentPersoenlicheKontenRequest(BaseModel):
@@ -6399,6 +6454,27 @@ def post_personalisierung_gesperrt(
return {"success": set_personalisierung_gesperrt(device_id, personalisierung_id, payload.gesperrt)}
@app.post("/api/v1/devices/{device_id}/personalisierung/{personalisierung_id}/passwort-reset")
def post_personalisierung_passwort_reset(
device_id: str,
personalisierung_id: str,
payload: PasswortResetRequest,
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 payload.passwort_hash.startswith("$6$"):
return {"success": False, "error": "invalid_request", "message": "Kein SHA-512-crypt-Hash."}
ok = set_personalisierung_passwort_reset(
device_id, personalisierung_id, payload.passwort_hash, payload.aendern, payload.angefordert_von
)
if not ok:
return {"success": False, "error": "not_found", "message": "Kein eingerichtetes Konto mit dieser ID."}
return {"success": True}
@app.post("/api/v1/agent/persoenliche-konten")
def agent_persoenliche_konten(
payload: AgentPersoenlicheKontenRequest,
@@ -0,0 +1,22 @@
-- Passwort-Reset durch die Verwaltung (ADR-0041, Ergänzung 18.10.2026).
--
-- Die Verwaltung setzt für ein eingerichtetes persönliches Konto ein
-- vorläufiges Passwort. kundenplattform erzeugt es, zeigt es einmal an und
-- schickt anode nur den SHA-512-crypt-Hash. Der Check-in reicht ihn an das
-- Gerät, die Rolle persoenliche-konten setzt ihn und meldet die reset_id
-- zurück; erst dann löscht anode den Hash. Ein neuer Reset vor der Meldung
-- bekommt eine neue reset_id, eine verspätete Meldung des alten löscht ihn
-- deshalb nicht.
BEGIN;
ALTER TABLE personalisierungen
ADD COLUMN reset_id UUID,
ADD COLUMN reset_passwort_hash TEXT,
-- Beim nächsten Anmelden ein eigenes Passwort verlangen.
ADD COLUMN reset_aendern BOOLEAN NOT NULL DEFAULT TRUE,
ADD COLUMN reset_angefordert_am TIMESTAMPTZ,
ADD COLUMN reset_angefordert_von TEXT,
ADD COLUMN reset_gesetzt_am TIMESTAMPTZ;
COMMIT;