fix(sf): ekb_geoportal_client verify=False — росс-гос-CA не в trust-store контейнера (#1067) #1103

Merged
bot-reviewer merged 1 commit from fix/1067-ekb-geoportal-tls-verify into main 2026-06-06 20:10:27 +00:00
2 changed files with 51 additions and 5 deletions
Showing only changes of commit 35899016e0 - Show all commits

View file

@ -71,23 +71,34 @@ WFS_LAYERS: dict[str, str] = {
DEFAULT_TIMEOUT = 20 DEFAULT_TIMEOUT = 20
# Геопортал ЕКБ отдаёт TLS-цепочку, корневой CA которой НЕ в trust-store наших контейнеров
# (российский гос-УЦ, как у НСПД). С verify=True из контейнера падает CERTIFICATE_VERIFY_FAILED
# (подтверждено live на проде 2026-06-06). Отключаем верификацию — данные ПУБЛИЧНЫЕ open-data
# (ПЗЗ/ППТ/КРТ), секреты не передаются → MITM-риск незначим. Прецедент: nspd_lite._SSL_CTX =
# ssl._create_unverified_context() для тех же росс-гос-эндпоинтов.
_VERIFY_TLS = False
# ── HTTP helper (module-level — мокается в тестах) ─────────────────────────── # ── HTTP helper (module-level — мокается в тестах) ───────────────────────────
def _http_get_json(url: str, params: dict[str, str], *, timeout: int = DEFAULT_TIMEOUT) -> Any: def _http_get_json(url: str, params: dict[str, str], *, timeout: int = DEFAULT_TIMEOUT) -> Any:
"""GET WFS-эндпоинта → JSON. Вынесен на module-level для monkeypatch в тестах. """GET WFS-эндпоинта → JSON. Вынесен на module-level для monkeypatch в тестах.
Геопортал ЕКБ нормальный TLS, без auth: обычный httpx без спец-обвязки (в отличие verify=_VERIFY_TLS (False): росс-гос-CA геопортала не в trust-store контейнера (см. выше).
от НСПД с нац-CA / WAF). Raises httpx.HTTPStatusError на 4xx/5xx (caller решает). Raises httpx.HTTPStatusError на 4xx/5xx (caller решает).
""" """
resp = httpx.get(url, params=params, timeout=timeout) resp = httpx.get(url, params=params, timeout=timeout, verify=_VERIFY_TLS)
resp.raise_for_status() resp.raise_for_status()
return resp.json() return resp.json()
def _http_post_json(url: str, body: dict[str, Any], *, timeout: int = DEFAULT_TIMEOUT) -> Any: def _http_post_json(url: str, body: dict[str, Any], *, timeout: int = DEFAULT_TIMEOUT) -> Any:
"""POST JSON → JSON (для searchByGeom / urbanCard). Module-level для monkeypatch в тестах.""" """POST JSON → JSON (searchByGeom / urbanCard). Module-level для monkeypatch в тестах.
resp = httpx.post(url, json=body, timeout=timeout)
verify=_VERIFY_TLS (False): см. _http_get_json росс-гос-CA не в trust-store контейнера.
"""
resp = httpx.post(url, json=body, timeout=timeout, verify=_VERIFY_TLS)
resp.raise_for_status() resp.raise_for_status()
return resp.json() return resp.json()

View file

@ -200,3 +200,38 @@ def test_zone_regulation_at_none_without_terzone(monkeypatch: pytest.MonkeyPatch
lambda url, body, **kw: {"10019": [{"typeAlias": "GknParcel", "key": 1}]}, lambda url, body, **kw: {"10019": [{"typeAlias": "GknParcel", "key": 1}]},
) )
assert EKBGeoportalClient().zone_regulation_at(0.0, 0.0) is None assert EKBGeoportalClient().zone_regulation_at(0.0, 0.0) is None
def test_http_helpers_disable_tls_verify(monkeypatch: Any) -> None:
"""Регрессия: _http_get_json/_http_post_json должны слать verify=False.
Росс-гос-CA геопортала не в trust-store контейнера verify=True падает
CERTIFICATE_VERIFY_FAILED на проде (см. fix #1067). Ловим молчаливый ре-брейк.
"""
from app.services.scrapers import ekb_geoportal_client as mod
captured: dict[str, Any] = {}
class _Resp:
def raise_for_status(self) -> None:
return None
def json(self) -> dict[str, Any]:
return {"ok": True}
def _fake_get(url: str, **kw: Any) -> _Resp:
captured["get_verify"] = kw.get("verify")
return _Resp()
def _fake_post(url: str, **kw: Any) -> _Resp:
captured["post_verify"] = kw.get("verify")
return _Resp()
monkeypatch.setattr(mod.httpx, "get", _fake_get)
monkeypatch.setattr(mod.httpx, "post", _fake_post)
mod._http_get_json("https://x/y", {"a": "b"})
mod._http_post_json("https://x/y", {"a": "b"})
assert captured["get_verify"] is False
assert captured["post_verify"] is False