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>
87 lines
3.3 KiB
Python
87 lines
3.3 KiB
Python
"""Configurare gateway. Env vars (prefix AUTOPASS_) + valori implicite.
|
|
|
|
NU stocheaza parole RAR. Credentialele RAR vin per-cerere de la ROAAUTO
|
|
(vezi plan.md sect. 5). Helper-ul `load_test_credentials` citeste blocul
|
|
<test> din settings.xml DOAR pentru dev local / probe pe mediul de test.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import xml.etree.ElementTree as ET
|
|
from functools import lru_cache
|
|
from pathlib import Path
|
|
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
model_config = SettingsConfigDict(env_prefix="AUTOPASS_", env_file=".env", extra="ignore")
|
|
|
|
# --- Bază de date ---
|
|
db_path: Path = ROOT / "data" / "autopass.db"
|
|
|
|
# --- Securitate (CORE) ---
|
|
# Enforcement auth API-key pe /v1/* protejat. False (dev/test): fara cheie ->
|
|
# cont implicit id=1. True (prod): fara cheie valida -> 401. O cheie PREZENTA
|
|
# dar invalida da 401 indiferent de flag.
|
|
require_api_key: bool = False
|
|
|
|
# --- RAR ---
|
|
rar_env: str = "test" # "test" | "prod"
|
|
rar_base_url_test: str = "https://apps.rarom.ro/test-rar-autopass"
|
|
rar_base_url_prod: str = "https://apps.rarom.ro/rar-autopass"
|
|
|
|
# WAF-ul RAR da 403 fara User-Agent de browser (confirmat live, vezi
|
|
# docs/api-rar-contract.md). Toate apelurile httpx il trimit.
|
|
http_user_agent: str = "Mozilla/5.0"
|
|
http_timeout_s: float = 30.0
|
|
|
|
# --- Worker ---
|
|
worker_poll_interval_s: float = 5.0
|
|
worker_heartbeat_stale_s: int = 30 # /healthz considera worker-ul mort peste atat
|
|
# In schelet send-ul e DEZACTIVAT (nu trimite la RAR). Activeaza-l explicit
|
|
# pentru proba end-to-end. Reconcilierea/retry-ul complet = T2.
|
|
worker_send_enabled: bool = False
|
|
# Dev: foloseste creds <test> din settings.xml pt login worker. In productie
|
|
# creds vin per-cerere de la ROAAUTO (T2) — lasa False.
|
|
worker_use_test_creds: bool = False
|
|
# T2 — recuperare orfane + retry/backoff:
|
|
worker_sending_lease_s: int = 120 # rand 'sending' mai vechi de atat = orfan (worker mort mid-POST)
|
|
worker_retry_base_s: int = 5 # backoff = base * 2^retry (plafonat la max)
|
|
worker_retry_max_s: int = 300
|
|
worker_max_retries: int = 8 # peste atat -> error + banner (pana persistenta)
|
|
|
|
@property
|
|
def rar_base_url(self) -> str:
|
|
return self.rar_base_url_prod if self.rar_env == "prod" else self.rar_base_url_test
|
|
|
|
|
|
@lru_cache
|
|
def get_settings() -> Settings:
|
|
return Settings()
|
|
|
|
|
|
def load_test_credentials(settings_xml: Path | None = None) -> dict | None:
|
|
"""Citeste credentialele <test> din settings.xml (dev local / probe test).
|
|
|
|
Intoarce {"email", "password"} sau None daca fisierul lipseste / e template.
|
|
NU se foloseste in productie — acolo creds vin per-cerere de la ROAAUTO.
|
|
"""
|
|
path = settings_xml or (ROOT / "settings.xml")
|
|
if not path.exists():
|
|
return None
|
|
try:
|
|
root = ET.parse(path).getroot()
|
|
node = root.find("./test/credentials")
|
|
if node is None:
|
|
return None
|
|
email = (node.findtext("email") or "").strip()
|
|
password = (node.findtext("password") or "").strip()
|
|
if not email or not password or email.startswith("EMAIL_"):
|
|
return None
|
|
return {"email": email, "password": password}
|
|
except ET.ParseError:
|
|
return None
|