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:
@@ -48,7 +48,7 @@ Reguli de contract (detalii in `docs/api-rar-contract.md`): `FINALIZATA` e termi
|
||||
> PRD-uri (`docs/prd/prd-X.Y-*.md`), linkate in coloana Detalii. La fiecare livrabila terminata:
|
||||
> schimba statusul + data + linkul PRD si actualizeaza "Ultima actualizare".
|
||||
|
||||
**Ultima actualizare**: 2026-06-17 — 3.1 LIVRAT (CLI `tools/account.py` + `accounts.active` + index unic CUI + helper-e `app/accounts.py`; 299 teste pass). Urmeaza 3.2. Deferat din 3.1 (P3, fara SQL manual): `rename`/`set-cui` (corectie typo), `--if-not-exists` (provisioning idempotent); `set-password --account N` se implementeaza in 3.3 cu `app/users.py`.
|
||||
**Ultima actualizare**: 2026-06-17 — 3.2 LIVRAT (scope pe cont la toate GET-urile API `/v1/*` care ating `submissions`/`operations_mapping`: `account_scope_clause` cu regula NULL→cont 1, 404 cross-account byte-identic, allowlist campuri pe detaliu, index `(account_id,status)`, regula B8 in contract; nomenclator ramane global. 14 teste noi, 313 pass. VERIFY=PASS context curat). Urmeaza 3.3 (self-onboarding web). Deferat din 3.1 (P3): `rename`/`set-cui`, `--if-not-exists`; `set-password --account N` in 3.3 cu `app/users.py`.
|
||||
|
||||
### Etapa 1 — Canal API ROAAUTO (Treapta 1)
|
||||
|
||||
@@ -73,7 +73,7 @@ Reguli de contract (detalii in `docs/api-rar-contract.md`): `FINALIZATA` e termi
|
||||
| # | Livrabila | Status | Data | Detalii |
|
||||
|---|-----------|--------|------|---------|
|
||||
| 3.1 | Creare cont nou (CLI dedicat) | DONE | 2026-06-17 | CLI `tools/account.py` (create/list[--pending]/activate/deactivate, `--with-key` atomic) + `accounts.active` + index unic CUI + `app/accounts.py`. 20 teste noi. PRD: [prd-3.1](prd/prd-3.1-creare-cont.md) |
|
||||
| 3.2 | Filtrare pe cont a GET-urilor de listare | TODO (PRD aprobat) | | scope cheie pe `/v1/prezentari`, `/v1/mapari`, `/v1/audit/export`; nomenclator global. PRD: [prd-3.2](prd/prd-3.2-filtrare-cont-get.md) |
|
||||
| 3.2 | Filtrare pe cont a GET-urilor de listare | DONE | 2026-06-17 | scope cheie pe `/v1/prezentari(/{id})`, `/v1/mapari(/pending)`, `/v1/audit/export` (NULL→cont 1); nomenclator global; 404 cross-account identic (B3) + allowlist campuri detaliu (B4) + helper `account_scope_clause` (B2) + index (B5). 14 teste noi, 313 pass. PRD: [prd-3.2](prd/prd-3.2-filtrare-cont-get.md) |
|
||||
| 3.3 | Self-onboarding web + interfata admin | TODO (PRD aprobat) | | signup/login/sesiuni + cont "in asteptare" + gate worker + CSRF + panou admin web + email. 12 stories. PRD: [prd-3.3](prd/prd-3.3-self-onboarding-web.md) |
|
||||
|
||||
### Etapa 4 — Viitor (Treapta 3)
|
||||
|
||||
@@ -294,6 +294,19 @@ de 18 coduri la boot (`app/nomenclator_seed.py`) ca editorul sa mearga offline.
|
||||
Auth API-key (CORE) inca neimplementat -> `account_id` curge ca `NULL` si e atribuit
|
||||
contului default `id=1` (seed in schema); cand auth livreaza, account_id real curge natural.
|
||||
|
||||
## Regula de scope pe cont (B8, PRD 3.2)
|
||||
|
||||
Orice GET nou pe `/v1/*` care atinge `submissions` sau `operations_mapping` **PORNESTE**
|
||||
cu `account_id: int = Depends(resolve_account_id)` si clauza de scope pe cont in SQL.
|
||||
Varianta globala (fara scope) e exceptie justificata explicit — singurul exemplu actual
|
||||
este `GET /v1/nomenclator` (cache de referinta RAR fara PII, partajat intre conturi).
|
||||
|
||||
Pentru `submissions` (account_id nullable): foloseste `account_scope_clause(account_id)`
|
||||
din `app/mapping.py` care produce `(account_id = ? OR (account_id IS NULL AND ? = 1))`.
|
||||
Randurile legacy cu `account_id IS NULL` apartin contului 1 (OV-2, back-compat).
|
||||
|
||||
Pentru `operations_mapping` (account_id NOT NULL): `WHERE account_id = ?` simplu.
|
||||
|
||||
## Open questions rămase (actualizat)
|
||||
|
||||
1. ~~Sursa pozei odometrului~~ — **închis** (poză opțională).
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user