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

@@ -37,7 +37,9 @@ def ping(
account_id — contul rezolvat din cheie (sau 1 in dev fara cheie)
mediu — "test" / "prod" (settings.rar_env)
autentificat_cu_cheie — True daca cererea a venit cu o cheie API reala valida
are_creds_rar — True daca contul are rar_creds_enc stocat
are_creds_rar — True daca contul are creds RAR stocate pe cel putin un mediu (test sau prod)
are_creds_test — True daca contul are creds RAR pentru mediul Testare
are_creds_prod — True daca contul are creds RAR pentru mediul Productie
ts — timestamp ISO UTC al cererii
"""
settings = get_settings()
@@ -55,23 +57,27 @@ def ping(
conn.close()
autentificat_cu_cheie = acct is not None
# Verificam daca contul are creds RAR stocate.
# Verificam daca contul are creds RAR stocate (per-env, US-013).
aid = account_or_default(account_id)
conn = get_connection()
try:
row = conn.execute(
"SELECT rar_creds_enc FROM accounts WHERE id=?", (aid,)
"SELECT rar_creds_test_enc, rar_creds_prod_enc FROM accounts WHERE id=?", (aid,)
).fetchone()
finally:
conn.close()
are_creds_rar = bool(row and row["rar_creds_enc"])
are_creds_test = bool(row and row["rar_creds_test_enc"])
are_creds_prod = bool(row and row["rar_creds_prod_enc"])
are_creds_rar = are_creds_test or are_creds_prod
return JSONResponse({
"account_id": aid,
"mediu": settings.rar_env,
"autentificat_cu_cheie": autentificat_cu_cheie,
"are_creds_rar": are_creds_rar,
"are_creds_test": are_creds_test,
"are_creds_prod": are_creds_prod,
"ts": datetime.now(timezone.utc).isoformat(),
})

View File

@@ -149,7 +149,7 @@ def create_prezentari(
account_id vine din cheia API (resolve_account_id): cont real cu cheie,
implicit id=1 in dev fara cheie, 401 fara cheie valida in prod.
Cand rar_credentials lipseste, submission-ul intra fara creds efemere: worker-ul
cade pe creds-urile durabile ale contului (`accounts.rar_creds_enc`).
cade pe creds-urile durabile ale contului (per-env: `accounts.rar_creds_{env}_enc`).
"""
acct = account_or_default(account_id)
# Creds RAR efemere: criptate si lipite de fiecare submission nou pana la
@@ -276,12 +276,11 @@ def create_prezentari(
cl["rar_error"], creds_enc, env, existing["id"]),
)
if cur.rowcount == 1:
# Creds noi se propaga si in canalul durabil (accounts.rar_creds_enc)
# — ambele canale converg pe parola corectata.
# US-013: muta pe slot env dupa login (write-back conservator).
# Creds noi se propaga si in slotul durabil per-env al contului
# — ambele canale converg pe parola corectata (US-013, env-aware).
if req.rar_credentials is not None:
conn.execute(
"UPDATE accounts SET rar_creds_enc=? WHERE id=?",
f"UPDATE accounts SET rar_creds_{env}_enc=?, rar_{env}_enabled=1 WHERE id=?",
(encrypt_creds(req.rar_credentials.model_dump()), acct),
)
_emite_text_rule_hits(conn, acct, existing["id"], cl["resolved"])
@@ -742,10 +741,16 @@ def create_mapare(
class RarCredsIn(BaseModel):
"""Creds RAR durabile per-cont. Stocate criptate (Fernet) in accounts.rar_creds_enc."""
"""Creds RAR durabile per-cont, stocate criptat (Fernet) in slotul per-mediu.
`rar_target` selecteaza mediul: 'test' | 'prod'. Absent -> mediul ancorei globale
(AUTOPASS_RAR_ENV), implicit 'test'. Schimbare aditiva — clientii vechi care nu trimit
`rar_target` continua sa functioneze (comportament consistent cu ancora globala).
"""
email: str = Field(..., min_length=1)
password: str = Field(..., min_length=1, repr=False)
rar_target: str | None = None # 'test' | 'prod' | None -> ancora globala
@router.post("/conturi/rar-creds")
@@ -753,21 +758,26 @@ def set_rar_creds(
req: RarCredsIn,
account_id: int = Depends(resolve_account_id),
) -> dict:
"""Seteaza creds RAR durabile per-cont.
"""Seteaza creds RAR durabile per-cont, in slotul per-mediu (US-013, env-aware).
Criptate Fernet in accounts.rar_creds_enc. Worker-ul le foloseste ca fallback
cand submission-ul nu mai are creds (canal web fara re-pusher, restart worker).
Contul vine din cheia API.
Slotul tinta: `req.rar_target` ('test'/'prod') sau ancora globala (AUTOPASS_RAR_ENV).
Activeaza mediul selectat (`rar_{env}_enabled=1`). Worker-ul le foloseste ca
fallback cand submission-ul nu mai are creds efemere. Contul vine din cheia API.
"""
from ...config import get_settings as _gs
_s = _gs()
env = req.rar_target if req.rar_target in ("test", "prod") else (
_s.rar_env if _s.rar_env in ("test", "prod") else "test"
)
acct = account_or_default(account_id)
enc = encrypt_creds({"email": req.email, "password": req.password})
conn = get_connection()
try:
conn.execute(
"UPDATE accounts SET rar_creds_enc=? WHERE id=?",
f"UPDATE accounts SET rar_creds_{env}_enc=?, rar_{env}_enabled=1 WHERE id=?",
(enc, acct),
)
return {"ok": True, "account_id": acct}
return {"ok": True, "account_id": acct, "rar_env": env}
finally:
conn.close()
@@ -775,12 +785,27 @@ def set_rar_creds(
@router.delete("/conturi/rar-creds")
def delete_rar_creds(
account_id: int = Depends(resolve_account_id),
env: str | None = None,
) -> dict:
"""Sterge creds RAR durabile per-cont (revenire la modelul efemer Treapta 1)."""
"""Sterge creds RAR durabile per-cont (revenire la modelul efemer Treapta 1).
`env` (query param): 'test' | 'prod' -> sterge DOAR slotul acelui mediu + dezactiveaza-l.
Absent -> sterge AMBELE sloturi (revenire completa). Schimbare aditiva (back-compat).
"""
acct = account_or_default(account_id)
conn = get_connection()
try:
conn.execute("UPDATE accounts SET rar_creds_enc=NULL WHERE id=?", (acct,))
if env in ("test", "prod"):
conn.execute(
f"UPDATE accounts SET rar_creds_{env}_enc=NULL, rar_{env}_enabled=0 WHERE id=?",
(acct,),
)
else:
conn.execute(
"UPDATE accounts SET rar_creds_test_enc=NULL, rar_test_enabled=0, "
"rar_creds_prod_enc=NULL, rar_prod_enabled=0 WHERE id=?",
(acct,),
)
return {"ok": True, "account_id": acct}
finally:
conn.close()