"""Defense-in-depth: shared-secret gate поверх trusted-header auth (#2213). Backend доверяет X-Authenticated-User (его ставит Caddy). На общей docker-сети gendesign_shared любой контейнер мог бы отправить поддельный `X-Authenticated-User: admin` напрямую на tradein-backend:8000, минуя Caddy. Общий секрет X-Internal-Auth-Secret закрывает дыру: если TRADEIN_INTERNAL_AUTH_SECRET задан, каждый запрос с X-Authenticated-User обязан нести валидный секрет (constant-time), иначе 401. Пусто = fail-open (backward-compat до провижининга). Использует РЕАЛЬНЫЙ rbac_guard (app/core/rbac.py) — тот же, что регистрирует app/main.py в проде. Раньше здесь была hand-maintained копия ("MIRROR of rbac_guard из app/main.py"); соседний test_rbac.py держал СВОЮ отдельную копию, которая успела отстать (потеряла именно этот secret-gate) — регрессия в реальном guard'е могла бы пройти CI незамеченной. См. app/core/rbac.py. """ from __future__ import annotations import os os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test") import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from app.core import auth as auth_mod from app.core import config from app.core.rbac import rbac_guard @pytest.fixture(autouse=True) def _reset_auth_cache() -> None: auth_mod.reset_cache_for_tests() def _build_test_app() -> FastAPI: """Test app используя РЕАЛЬНЫЙ rbac_guard (с #2213 secret-gate).""" app = FastAPI() app.middleware("http")(rbac_guard) @app.get("/api/v1/ping") async def ping() -> dict: return {"ok": True} return app @pytest.fixture def client() -> TestClient: return TestClient(_build_test_app()) # --------------------------------------------------------------------------- # (в) секрет НЕ задан → backward-compat: X-Authenticated-User достаточно. # --------------------------------------------------------------------------- def test_secret_unset_backward_compat(client: TestClient, monkeypatch) -> None: monkeypatch.setattr(config.settings, "tradein_internal_auth_secret", "") resp = client.get("/api/v1/ping", headers={"X-Authenticated-User": "admin"}) assert resp.status_code == 200 assert resp.json() == {"ok": True} def test_secret_unset_ignores_provided_secret(client: TestClient, monkeypatch) -> None: # Если секрет не задан — присланный клиентом X-Internal-Auth-Secret игнорируется. monkeypatch.setattr(config.settings, "tradein_internal_auth_secret", "") resp = client.get( "/api/v1/ping", headers={"X-Authenticated-User": "admin", "X-Internal-Auth-Secret": "whatever"}, ) assert resp.status_code == 200 # --------------------------------------------------------------------------- # (а) секрет задан + заголовок юзера БЕЗ секрета → 401. # --------------------------------------------------------------------------- def test_secret_set_missing_header_401(client: TestClient, monkeypatch) -> None: monkeypatch.setattr(config.settings, "tradein_internal_auth_secret", "s3cr3t-value") resp = client.get("/api/v1/ping", headers={"X-Authenticated-User": "admin"}) assert resp.status_code == 401 assert "internal auth secret" in resp.json()["detail"].lower() def test_secret_set_wrong_secret_401(client: TestClient, monkeypatch) -> None: monkeypatch.setattr(config.settings, "tradein_internal_auth_secret", "s3cr3t-value") resp = client.get( "/api/v1/ping", headers={"X-Authenticated-User": "admin", "X-Internal-Auth-Secret": "wrong"}, ) assert resp.status_code == 401 assert "internal auth secret" in resp.json()["detail"].lower() # --------------------------------------------------------------------------- # (б) секрет задан + оба заголовка корректны → 200 (существующее поведение). # --------------------------------------------------------------------------- def test_secret_set_correct_secret_200(client: TestClient, monkeypatch) -> None: monkeypatch.setattr(config.settings, "tradein_internal_auth_secret", "s3cr3t-value") resp = client.get( "/api/v1/ping", headers={"X-Authenticated-User": "admin", "X-Internal-Auth-Secret": "s3cr3t-value"}, ) assert resp.status_code == 200 assert resp.json() == {"ok": True} def test_secret_set_no_user_header_still_401(client: TestClient, monkeypatch) -> None: # Секрет без X-Authenticated-User — no-auth 401 (secret-gate не ослабляет базу). monkeypatch.setattr(config.settings, "tradein_internal_auth_secret", "s3cr3t-value") resp = client.get("/api/v1/ping", headers={"X-Internal-Auth-Secret": "s3cr3t-value"}) assert resp.status_code == 401 assert "no authenticated user" in resp.json()["detail"].lower()