Files
rar-autopass/docs/prd/prd-3.2-filtrare-cont-get.md
Claude Agent 748ab8b289 feat(api): scope pe cont la GET-urile de listare /v1/* (PRD 3.2)
Inchide scurgerea cross-account pe GET /v1/prezentari(/{id}),
/v1/mapari(/pending) si /v1/audit/export. Toate primesc
Depends(resolve_account_id) + account_scope_clause (regula NULL->cont 1,
OV-2). Nomenclatorul ramane global (referinta partajata, fara PII).

- B3: 404 cross-account byte-identic cu 404 inexistent (fara oracol enumerare)
- B4: get_prezentare cu allowlist de campuri (nu mai expune rar_creds_enc/
  payload_json/idempotency_key/rar_error)
- B1: pending_unmapped filtreaza in SQL; default None = global doar pentru web
- B2: helper account_scope_clause (DRY, doar pe submissions nullable)
- B5: index idx_submissions_account_status
- B8: regula de scope documentata in api-rar-contract.md
- TD-3.2: ?account_id != contul cheii -> 400

14 teste noi (cross-account, legacy NULL, B3, B4); suita 313 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:35:50 +00:00

12 KiB
Raw Permalink Blame History

PRD 3.2 — Filtrare pe cont a GET-urilor de listare

Stare: inchis

Proces complet: docs/ROADMAP.md §5. Contract RAR (sursa de adevar): docs/api-rar-contract.md. Starea trece: draft → aprobat → in-executie → verify-pass → inchis (actualizata de lead).

1. Obiectiv

Inchide scurgerea de date intre conturi pe canalul API: azi GET /v1/prezentari, /v1/prezentari/{id}, /v1/mapari(/pending) si /v1/audit/export sunt globale — orice cheie valida vede coada, maparile si auditul tuturor conturilor. POST-urile sunt deja account-scoped (resolve_account_id); aducem GET-urile la aceeasi disciplina. Pre-cerinta de securitate inainte ca 3.3 sa aduca useri web reali.

2. Non-Goals (anti scope-creep)

  • Nomenclatorul ramane global. GET /v1/nomenclator + nomenclator_rar sunt cache de referinta RAR partajat (fara account_id, fara PII) — nu are sens sa-l filtram. Decizie explicita, nu omisiune.
  • Fara auth pe rutele web. Rutele app/web/routes.py raman hardcodate pe contul default (id=1) pana la 3.3. 3.2 priveste doar GET-urile API /v1/*. (pending_unmapped se parametrizeaza compatibil — vezi US-002 — ca web-ul sa ramana neschimbat.)
  • Fara modificari la POST-uri — sunt deja scoped.
  • Fara paginare/filtre noi — doar adaugam dimensiunea cont la ce exista.

3. Stories atomice

Invariant transversal: randuri legacy cu account_id NULL (OV-2) se trateaza ca apartinand contului default (id=1). Filtrarea foloseste account_or_default + match pe NULL doar pentru id=1, ca sa nu dispara prezentarile vechi din vederea contului 1.

US-001: Scope cont pe GET /v1/prezentari + GET /v1/prezentari/{id}

Ca detinator de cheie API vreau ca listarea/detaliul prezentarilor sa-mi arate doar contul meu pentru ca un client nu trebuie sa vada coada altui client.

  • Depinde de: —
  • Fisiere: app/api/v1/router.py, tests/test_get_scope_prezentari.py (nou) (~2 fisiere)
  • Test intai (RED): tests/test_get_scope_prezentari.pytest_lista_doar_contul_cheii, test_detaliu_cross_account_404, test_legacy_null_vizibil_pentru_cont_1, test_fara_cheie_flag_off_vede_contul_1
  • Acceptance criteria:
    • Ambele rute primesc account_id: int = Depends(resolve_account_id).
    • GET /v1/prezentari adauga WHERE pe cont (NULL→cont 1) la ambele ramuri (cu/fara status).
    • GET /v1/prezentari/{id} al altui cont → 404 (nu 403 — nu confirmam existenta).
    • Cheie A nu vede submission-uri ale contului B (lista si detaliu).
    • require_api_key=false fara cheie → vede contul 1 (back-compat dev).
  • Verificare E2E: doua chei (conturi distincte, via 3.1) → POST pe fiecare → GET /v1/prezentari cu cheia A nu contine id-urile contului B.

US-002: Scope cont pe GET /v1/mapari + GET /v1/mapari/pending

Ca detinator de cheie API vreau ca maparile si pending-ul sa fie ale contului meu pentru ca azi /v1/mapari?account_id= accepta account_id din query (spoofabil) si /pending e global.

  • Depinde de: —
  • Fisiere: app/api/v1/router.py, app/mapping.py (parametru optional la pending_unmapped), tests/test_get_scope_mapari.py (nou) (~3 fisiere)
  • Test intai (RED): tests/test_get_scope_mapari.pytest_mapari_ignora_query_account_id, test_mapari_doar_contul_cheii, test_pending_doar_contul_cheii, test_pending_web_global_neschimbat
  • Acceptance criteria:
    • GET /v1/mapari foloseste Depends(resolve_account_id); parametrul account_id din query este eliminat (un cont nu poate citi maparile altuia trecand un id arbitrar).
    • pending_unmapped(conn, account_id=None) capata param optional: None = global (web, back-compat), valoare = filtrare pe cont. GET /v1/mapari/pending paseaza contul cheii.
    • Apelul web pending_unmapped(conn) din routes.py ramane neatins (global) — confirmat de test_pending_web_global_neschimbat.
  • Verificare E2E: cheie A cu o mapare; cheie B → GET /v1/mapari (B) nu contine maparea lui A.

US-003: Scope cont pe GET /v1/audit/export

Ca detinator de cheie API vreau ca exportul de audit sa contina doar prezentarile mele pentru ca CSV-ul de audit expune VIN/nr. inmatriculare (PII) si nu trebuie sa traverseze conturi.

  • Depinde de: —
  • Fisiere: app/api/v1/router.py, tests/test_get_scope_audit.py (nou) (~2 fisiere)
  • Test intai (RED): tests/test_get_scope_audit.pytest_export_doar_contul_cheii, test_export_legacy_null_pentru_cont_1, test_export_status_all_tot_scoped
  • Acceptance criteria:
    • audit_export primeste Depends(resolve_account_id); _audit_rows filtreaza pe cont (NULL→cont 1) pe langa filtrele de data/status existente.
    • status=all ramane scoped pe cont (nu exporta global).
    • Randurile contului B nu apar in CSV-ul cerut cu cheia A.
  • Verificare E2E: POST pe doua conturi → GET /v1/audit/export (cheie A) → CSV fara VIN-urile B.

4. Riscuri

  • Back-compat dev — cu require_api_key=false si fara cheie, totul colapseaza la contul 1; filtrarea trebuie sa ramana transparenta acolo (testat explicit). Mitigare: account_or_default.
  • Randuri legacy NULL — daca le excludem din vederea contului 1, prezentarile vechi „dispar" din dashboard/audit. Mitigare: regula NULL→cont 1 in fiecare WHERE, cu test dedicat.
  • Regresie pe webpending_unmapped e partajat API/web; semnatura cu default None previne ruperea editorului web (acoperit de test).

5. Intrebari deschise

Se rezolva cu utilizatorul ÎNAINTE de executie (poarta de aprobare PRD).

  • Nomenclatorul ramane global? Propunere: da (referinta partajata, fara PII). Confirmi?
  • GET /v1/mapari — pastram parametrul account_id din query, ignorat, sau il scoatem complet? Propunere: il scoatem (semnal clar ca nu mai e configurabil din afara). Risca sa strice clienti care il trimiteau? (Azi nu exista clienti API multi-cont — risc practic zero.)

6. Valuri de executie (graful de dependente)

Val 1: [US-001, US-002, US-003]   ← toate ating router.py (FISIER COMUN) → NU paralel pe acelasi fisier

Nota de paralelizare: cele trei stories ating app/api/v1/router.py → conform §5.5 nu ruleaza in paralel (fisier comun). Optiuni: secvential (un teammate, US-001→002→003) sau isolation: worktree + merge de lead. Recomandare: secvential, un singur teammate — schimbarile sunt mici si inrudite (acelasi pattern Depends(resolve_account_id) + WHERE account_id).


Addendum review (autoplan, [subagent-only] — Codex indisponibil: usage limit)

Doua voci Claude independente (Eng + Produs/DX-API). Schimbari obligatorii la executie + decizii ridicate la poarta.

B1 [MAJOR, ambele voci] — pending_unmapped filtreaza in SQL, nu post-hoc in Python. Azi filtreaza in dict dupa r["account_id"]. Cu scope pe cont, filtrarea trebuie in query: WHERE status='needs_mapping' AND (account_id=? OR (account_id IS NULL AND ?=1)). Pastreaza default account_id=None=global DAR documenteaza-l ca intentionat pentru web (footgun altfel: un apelant viitor care uita parametrul scurge cross-account — exact bug-ul de fata). AC nou: test_pending_filtreaza_in_sql_cu_regula_null.

B2 [MEDIUM→DRY, ambele voci] — Helper unic pentru clauza de scope. Regula NULL→cont 1 apare in 5+ query-uri (list_prezentari ×2, get_prezentare, _audit_rows, pending_unmapped). Extrage account_scope_clause(account_id) -> (sql_fragment, params) care produce (account_id = ? OR (account_id IS NULL AND ? = 1)). Clarificare critica: se aplica DOAR tabelelor cu account_id nullable (submissions). get_mapari pe operations_mapping (account_id NOT NULL) foloseste simplu WHERE account_id=? — fara ramura NULL. Un singur loc de testat.

B3 [MAJOR, Produs] — 404 cross-account byte-identic cu 404 inexistent. Altfel detail diferit re-deschide oracolul de enumerare pe care 404 trebuia sa-l inchida. AC nou pe get_prezentare: acelasi status + acelasi detail pentru "inexistent" si "exista dar nu e al contului tau".

B4 [MAJOR, ambele — leak adiacent] — get_prezentare foloseste SELECT * → expune rar_creds_enc, idempotency_key, rar_error (poate contine VIN-uri). Scope-ul rezolva cine vede randul, nu ce campuri. Adauga allowlist de campuri in get_prezentare (exclude rar_creds_enc, payload_json; pastreaza doar campurile de monitorizare). Ieftin, inchide o suprafata fragila ("orice coloana noua scapa by default"). AC: test_detaliu_nu_expune_creds.

B5 [MEDIUM, Eng] — Index pe account_id. Adauga CREATE INDEX IF NOT EXISTS idx_submissions_account_status ON submissions(account_id, status) (schema + _migrate). O linie; previne scan dupa ce fiecare query capata predicat pe coloana neindexata. (Volum mic azi, dar e ieftin — P2.)

B6 [MAJOR, ambele — reformulare onesta] — Web-ul ramane GLOBAL, nu "pe cont 1". fragment_submissions, _status_counts, fragment_banner, _render_mapari afiseaza TOATE conturile in dashboard. Non-goal-ul actual ("web hardcodat cont 1") e inselator. Reformulare: "dashboard-ul web e tool INTERN (neexpus clientilor in Treapta 1/2); riscul rezidual global e acceptat constient; 3.3/US-005 il inchide comportamental." Daca web-ul devine expus clientilor inainte de 3.3, 3.2 e incomplet ca pre-cerinta de securitate.

B7 [MEDIUM, Produs] — Dependenta de VERIFY fata de 3.1. Implementarea nu depinde de 3.1, dar E2E cere un al doilea cont. Noteaza: "E2E foloseste cont #2 creat prin 3.1 (tools/account.py) sau INSERT manual daca 3.1 nu e inca livrat."

B8 [MEDIUM, Produs — structural] — Inverseaza default-ul mental. Cauza bug-ului: scope-ul e opt-in via Depends, nu default. Adauga in docs/api-rar-contract.md (sau CLAUDE.md) regula: "orice GET nou pe /v1/* care atinge submissions/operations_mapping PORNESTE cu Depends(resolve_account_id) + clauza de scope; globalul e exceptie justificata (ca nomenclatorul)." Trateaza cauza, nu doar cele 4 simptome.

TD-3.2 [REZOLVAT la poarta] — ?account_id= pe /v1/mapari: contul vine MEREU din cheia API (nespoofabil). Param-ul din query se ignora ca selector; daca e prezent SI difera de contul cheii → 400 explicit (nu schimbare tacita). AC US-002 actualizat: test_mapari_query_account_id_diferit_400, test_mapari_query_account_id_egal_ok.

Raport VERIFY

Completat de subagentul verificator (context curat) in faza VERIFY — vezi ROADMAP §5.6.

Verdict global: PASS (verificator independent, context curat, 2026-06-17).

  • Suita: python3 -m pytest -q313 passed, 0 fail. Teste noi 3.2: 14 passed.
  • Regresia de aur: 313 verzi — POST /v1/prezentari, import, worker neatinse; calea de send nu e modificata.
Criteriu Verdict Dovada
US-001: list_prezentari scoped pe cont (ambele ramuri) PASS router.py + test_lista_doar_contul_cheii
US-001: GET /{id} alt cont → 404 PASS test_detaliu_cross_account_404
US-001: back-compat dev (fara cheie → cont 1) PASS test_fara_cheie_flag_off_vede_contul_1
US-002: GET /mapari scoped; ?account_id difera → 400 (TD-3.2) PASS test_mapari_query_account_id_diferit_400 / _egal_ok
US-002: web pending_unmapped(conn) ramane global PASS routes.py:160 neatins + test_pending_web_global_neschimbat
US-003: audit/export + status=all scoped PASS 3 teste test_get_scope_audit
B1: pending_unmapped filtreaza IN SQL (nu Python) PASS test_pending_filtreaza_in_sql_cu_regula_null
B2: account_scope_clause DOAR pe submissions; get_mapari WHERE account_id=? simplu PASS mapping.py + router.py
B3: 404 cross-account byte-identic cu 404 inexistent PASS un singur detail; test explicit
B4: get_prezentare allowlist (exclude creds/payload/idempotency/error) PASS _PREZENTARE_FIELDS + test_detaliu_nu_expune_creds
B5: index idx_submissions_account_status in schema.sql + _migrate PASS schema.sql + db.py
B8: regula scope documentata in api-rar-contract.md PASS sectiune "Regula de scope pe cont (B8, PRD 3.2)"

Rezerva (acceptata): trimiterea LIVE la RAR test (FINALIZATA) nu a rulat — lipsa .env/credentiale RAR in mediu. Schimbarile 3.2 ating EXCLUSIV GET-uri de citire (POST/worker/send neatinse), deci regresia E2E e acoperita integral de suita automata. De re-confirmat la urmatorul deploy cu creds.