gendesign/tradein-mvp/backend/tests/test_internal_auth_secret.py
lekss361 ca46411346
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 5m2s
Deploy Trade-In / build-backend (push) Successful in 5m38s
Deploy Trade-In / deploy (push) Successful in 1m4s
fix(tradein/tests): тесты авторизации проверяют настоящий guard + реальный рендер PDF (#2541)
2026-07-26 23:04:43 +00:00

118 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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()