feat(securitate-CORE): redactare creds + auth API-key per cont

Redactare:
- handler RequestValidationError dropeaza input/ctx din 422 (vectorul de
  scurgere a rar_credentials.password pe /v1/prezentari); pastreaza type/loc/msg
- app/security.py: scrub/scrub_text + CredentialRedactingFilter pe root+uvicorn
- models.py: password cu repr=False

Auth API-key:
- app/auth.py: hash SHA-256 in api_keys (cheia in clar emisa o singura data),
  header X-API-Key / Authorization: Bearer, dependency resolve_account_id
- enforcement pe flag AUTOPASS_require_api_key (prod on->401, dev off->cont
  default id=1; cheie prezenta invalida->401 mereu)
- account_id real curge din cheie in ingestie + mapare
- tools/apikey.py: CLI create/rotate/revoke/list (fara endpoint HTTP admin)

16 teste noi (tests/test_security.py). 85 pass total.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-15 20:02:07 +00:00
parent a6df3b636f
commit c17c1aa4f4
9 changed files with 647 additions and 13 deletions

View File

@@ -13,9 +13,10 @@ from __future__ import annotations
import json
from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from ...auth import resolve_account_id
from ...db import get_connection
from ...idempotency import idempotency_key
from ...mapping import (
@@ -33,17 +34,20 @@ router = APIRouter(prefix="/v1", tags=["v1"])
@router.post("/prezentari", response_model=PrezentariResponse)
def create_prezentari(req: PrezentareRequest) -> PrezentariResponse:
def create_prezentari(
req: PrezentareRequest,
account_id: int = Depends(resolve_account_id),
) -> PrezentariResponse:
"""Enqueue una/mai multe prezentari. Idempotent: continut identic -> acelasi submission.
Validarea de continut (T3, app.validation) ruleaza inainte de enqueue:
esecurile NU resping cererea, ci enqueue-aza cu status `needs_data` + motiv
(plan.md sect. 3). JSON malformat -> 422 din Pydantic (validare de shape).
TODO(auth): rezolva account_id din API key (acum None).
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.
Nota: rar_credentials NU se persista (zero-storage) — worker-ul le va primi
pe alt canal (T2); in schelet enqueue-ul doar stocheaza prezentarea.
"""
account_id = None # TODO(auth): din API key
acct = account_or_default(account_id)
conn = get_connection()
results: list[SubmissionResult] = []
@@ -174,18 +178,21 @@ def get_mapari_pending() -> dict:
class MapareIn(BaseModel):
account_id: int | None = None
cod_op_service: str = Field(..., min_length=1)
cod_prestatie: str = Field(..., min_length=1)
auto_send: bool = True
@router.post("/mapari")
def create_mapare(req: MapareIn) -> dict:
def create_mapare(
req: MapareIn,
account_id: int = Depends(resolve_account_id),
) -> dict:
"""Salveaza/actualizeaza o mapare op->cod si re-rezolva submission-urile blocate.
Verifica intai ca `cod_prestatie` exista in nomenclator (nu lasam mapari catre
coduri inexistente). Apoi upsert + re-rezolvare automata a `needs_mapping`.
Contul vine din cheia API (NU din body) — un cont nu poate edita maparile
altuia. Verifica intai ca `cod_prestatie` exista in nomenclator (nu lasam
mapari catre coduri inexistente). Apoi upsert + re-rezolvare `needs_mapping`.
"""
conn = get_connection()
try:
@@ -195,8 +202,8 @@ def create_mapare(req: MapareIn) -> dict:
).fetchone()
if not exists:
raise HTTPException(status_code=422, detail=f"cod_prestatie '{cod}' nu exista in nomenclator")
save_mapping(conn, req.account_id, req.cod_op_service, cod, req.auto_send)
stats = reresolve_account(conn, req.account_id)
save_mapping(conn, account_id, req.cod_op_service, cod, req.auto_send)
stats = reresolve_account(conn, account_id)
return {"saved": {"cod_op_service": req.cod_op_service.strip(), "cod_prestatie": cod}, "reresolve": stats}
finally:
conn.close()