feat(5.20): US-013 retragere accounts.rar_creds_enc -> per-env + DROP cu garda

Toate citirile pe coloana legacy accounts.rar_creds_enc mutate pe sloturile
per-env (rar_creds_test_enc/rar_creds_prod_enc): worker fallback+keepalive,
are_creds (web) si are_creds_rar (integrare, +are_creds_test/_prod), write-back
API la reactivare, purjare la stergere cont, _get_acasa_context/_fetch_cont_env_state.

Contract API (aditiv): POST /v1/conturi/rar-creds primeste rar_target optional
(test/prod), scrie in slotul corect + activeaza mediul; DELETE primeste ?env
(sterge un slot sau ambele). Documentat in docs/api-rar-contract.md.

DROP cu garda in db.py (schema.sql fara coloana pe DB fresh):
- 6a: eliminat ADD COLUMN rar_creds_enc (fara ping-pong re-ADD dupa DROP)
- 6b: try/except fail-safe (nu crapa boot-ul) + garda sqlite_version >= 3.35
- 6c: re-backfill old->new imediat inainte de assert (ancora globala)
- garda orfane: DROP anulat daca vreun creds legacy nu a aterizat in slot per-env
- backup criptat accounts_rar_creds_enc_backup inainte de DROP
- 6d: verificare prin PRAGMA table_info (NU grep — submissions are aceeasi coloana)
Garda one-way, idempotenta la boot repetat (verificat). submissions.rar_creds_enc
ramane neatinsa.

tests/test_retragere_creds_enc.py: niciun read pe coloana veche, conturi rar-creds
env-aware, are_creds per-env, DROP blocat de garda la lipsa copiere. 9 teste
existente actualizate pe sloturi per-env.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-07-02 21:03:08 +00:00
parent 3d3eb71a1e
commit b1d825e66b
19 changed files with 657 additions and 138 deletions

View File

@@ -86,11 +86,12 @@ def _migrate(conn: sqlite3.Connection) -> None:
# Coloane accounts
acc_cols = {r["name"] for r in conn.execute("PRAGMA table_info(accounts)").fetchall()}
if "rar_creds_enc" not in acc_cols:
conn.execute("ALTER TABLE accounts ADD COLUMN rar_creds_enc TEXT")
acc_cols.add("rar_creds_enc")
# AUTO-FIX 6a ELIMINAT (US-013): NU mai adaugam accounts.rar_creds_enc — coloana e dropata.
# _migrate_accounts_medii gestioneaza absenta coloana (guard la ~219: if "rar_creds_enc" not in acc_cols: return).
# Medii RAR per cont (PRD 5.20 US-001): activare + slot creds + default, per mediu.
_migrate_accounts_medii(conn, acc_cols)
# US-013: DROP coloana legacy accounts.rar_creds_enc dupa backfill complet.
_drop_legacy_accounts_rar_creds(conn, acc_cols)
if "active" not in acc_cols:
# Conturi existente raman active (default 1).
conn.execute("ALTER TABLE accounts ADD COLUMN active INTEGER NOT NULL DEFAULT 1")
@@ -229,6 +230,98 @@ def _migrate_accounts_medii(conn: sqlite3.Connection, acc_cols: set[str]) -> Non
)
def _drop_legacy_accounts_rar_creds(conn: sqlite3.Connection, acc_cols: set[str]) -> None:
"""PRD 5.20 US-013: DROP coloana legacy `accounts.rar_creds_enc` dupa backfill complet.
Idempotent si sigur la fiecare boot (garda one-way: coloana absenta = nimic de facut).
La eroare LOGHEAZA si lasa coloana pe loc (fail-safe — nu crapa boot-ul ambelor procese).
Structura separata pentru testabilitate: `_garda_si_drop(conn)` expune pasul de
assert+backup+DROP izolat (fara re-backfill 6c), apelabil direct din teste.
"""
if "rar_creds_enc" not in acc_cols:
return # garda one-way: coloana deja dropata sau DB fresh
try:
_drop_legacy_rar_creds_impl(conn)
except Exception as exc:
print(
f"[db] AVERTISMENT: DROP coloana legacy accounts.rar_creds_enc esuat: {exc}. "
"Coloana ramane pe loc (fail-safe).",
flush=True,
)
def _drop_legacy_rar_creds_impl(conn: sqlite3.Connection) -> None:
"""Re-backfill (AUTO-FIX 6c) + delegate la _garda_si_drop.
Re-backfill-ul 6c acopera creds setate via POST /v1/conturi/rar-creds intre US-001
si US-013 (pot fi DOAR in coloana veche). Ancora globala: AUTOPASS_RAR_ENV.
"""
if sqlite3.sqlite_version_info < (3, 35, 0):
print(
f"[db] SQLite {sqlite3.sqlite_version} < 3.35.0 — DROP COLUMN nesuportat; "
"coloana legacy accounts.rar_creds_enc ramane.",
flush=True,
)
return
# AUTO-FIX 6c: re-backfill creds din coloana veche in slotul per-env (ancora globala).
# Independent de _migrate_accounts_medii (care sare pe DB deja migrat cu guard newly_added).
env = get_settings().rar_env if get_settings().rar_env in ("test", "prod") else "test"
slot = f"rar_creds_{env}_enc"
conn.execute(
f"UPDATE accounts SET {slot}=rar_creds_enc, rar_{env}_enabled=1 "
f"WHERE rar_creds_enc IS NOT NULL AND TRIM(rar_creds_enc)<>'' "
f"AND ({slot} IS NULL OR TRIM({slot})='')"
)
_garda_si_drop(conn)
def _garda_si_drop(conn: sqlite3.Connection) -> None:
"""Garda de siguranta + backup + DROP accounts.rar_creds_enc. Testabila izolat.
Verifica ca niciun cont nu are creds DOAR in coloana veche (ambele sloturi per-env goale).
Daca exista orfane -> NU dropa (fail-safe: fara pierdere de date).
Altfel: backup criptat, DROP, verificare PRAGMA (AUTO-FIX 6d).
"""
# Garda: orfane = cont cu creds in coloana veche DAR ambele sloturi per-env goale.
orphan_count = conn.execute(
"SELECT COUNT(*) FROM accounts "
"WHERE rar_creds_enc IS NOT NULL AND TRIM(rar_creds_enc)<>'' "
"AND (rar_creds_test_enc IS NULL OR TRIM(rar_creds_test_enc)='') "
"AND (rar_creds_prod_enc IS NULL OR TRIM(rar_creds_prod_enc)='')"
).fetchone()[0]
if orphan_count > 0:
print(
f"[db] AVERTISMENT: {orphan_count} cont(uri) cu rar_creds_enc ne-copiat in niciun slot "
"per-env. DROP anulat (fail-safe: fara pierdere de date).",
flush=True,
)
return
# Backup criptat inainte de DROP (blob-urile sunt deja criptate Fernet).
conn.execute(
"CREATE TABLE IF NOT EXISTS accounts_rar_creds_enc_backup "
"(account_id INTEGER, rar_creds_enc TEXT, backed_up_at TEXT)"
)
conn.execute(
"INSERT INTO accounts_rar_creds_enc_backup "
"SELECT id, rar_creds_enc, datetime('now') FROM accounts "
"WHERE rar_creds_enc IS NOT NULL"
)
# DROP coloana legacy.
conn.execute("ALTER TABLE accounts DROP COLUMN rar_creds_enc")
# AUTO-FIX 6d: verifica prin PRAGMA (pe tabela accounts, NU grep — submissions are aceeasi coloana).
cols_after = {r["name"] for r in conn.execute("PRAGMA table_info(accounts)").fetchall()}
if "rar_creds_enc" in cols_after:
raise RuntimeError("DROP COLUMN rar_creds_enc esuat: coloana inca prezenta dupa ALTER TABLE")
print("[db] DROP coloana legacy accounts.rar_creds_enc: OK", flush=True)
def _backfill_submissions_rar_env(conn: sqlite3.Connection) -> None:
"""PRD 5.20 US-001 (AUTO-FIX G + E4/3): backfill rar_env + recompute idempotency_key.