Files
rar-autopass/docs/prd/prd-3.2-filtrare-cont-get.md
Claude Agent 6515de415b docs(prd): PRD-uri Etapa 3 (3.1/3.2/3.3) aprobate dupa autoplan
Faza PLAN pentru multi-cont / self-onboarding. Trei PRD-uri scrise, ancorate in
cod, trecute prin autoplan (voci Claude independente; Codex degradat pe usage-limit)
si aprobate la poarta umana.

- 3.1 creare cont: CLI tools/account.py + accounts.active; CUI unic prin index partial
- 3.2 filtrare GET pe cont: scope pe cheie, allowlist campuri, nomenclator global
- 3.3 self-onboarding web: sesiuni + cont 'in asteptare' + CSRF + interfata admin web
  + email; US-007 promovat in MVP (7->12 stories)

Dashboard ROADMAP actualizat (stare 'PRD aprobat', linkuri PRD).

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

10 KiB
Raw Blame History

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

Stare: aprobat

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. PASS/FAIL per criteriu, cu dovezi. Lipseste pana la VERIFY.