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>
This commit is contained in:
Claude Agent
2026-06-17 17:35:50 +00:00
parent 1c5b0cbc18
commit 748ab8b289
10 changed files with 655 additions and 61 deletions

View File

@@ -1,6 +1,6 @@
# PRD 3.2 — Filtrare pe cont a GET-urilor de listare
**Stare**: aprobat
**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).
@@ -39,11 +39,11 @@ contul meu **pentru ca** un client nu trebuie sa vada coada altui client.
`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).
- [x] Ambele rute primesc `account_id: int = Depends(resolve_account_id)`.
- [x] `GET /v1/prezentari` adauga `WHERE` pe cont (NULL→cont 1) la ambele ramuri (cu/fara `status`).
- [x] `GET /v1/prezentari/{id}` al altui cont → **404** (nu 403 — nu confirmam existenta).
- [x] Cheie A nu vede submission-uri ale contului B (lista si detaliu).
- [x] `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.
@@ -57,11 +57,11 @@ azi `/v1/mapari?account_id=` accepta `account_id` din query (spoofabil) si `/pen
- **Test intai (RED)**: `tests/test_get_scope_mapari.py``test_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
- [x] `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,
- [x] `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
- [x] 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.
@@ -74,10 +74,10 @@ azi `/v1/mapari?account_id=` accepta `account_id` din query (spoofabil) si `/pen
- **Test intai (RED)**: `tests/test_get_scope_audit.py``test_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
- [x] `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.
- [x] `status=all` ramane scoped pe cont (nu exporta global).
- [x] 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
@@ -171,4 +171,25 @@ cheii → **400** explicit (nu schimbare tacita). AC US-002 actualizat:
## 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.
**Verdict global: PASS** (verificator independent, context curat, 2026-06-17).
- **Suita**: `python3 -m pytest -q`**313 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.