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:
1 parent
2a8514a1a0
commit
6ea1f876e5
2 files changed
+103
-5
No files matched your search
@@ -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;
|
||||
Reference in new issue
Block a user