From e7a0af5ddc47715574279b6be7bdc98b931d3d3f Mon Sep 17 00:00:00 2001 From: lekss361 Date: Sat, 6 Jun 2026 20:10:26 +0000 Subject: [PATCH] =?UTF-8?q?fix(sf):=20ekb=5Fgeoportal=5Fclient=20verify=3D?= =?UTF-8?q?False=20=E2=80=94=20=D1=80=D0=BE=D1=81=D1=81-=D0=B3=D0=BE=D1=81?= =?UTF-8?q?-CA=20=D0=BD=D0=B5=20=D0=B2=20trust-store=20=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D1=82=D0=B5=D0=B9=D0=BD=D0=B5=D1=80=D0=B0=20(#1103)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _http_get_json/_http_post_json → verify=False (const _VERIFY_TLS): геопортал ЕКБ отдаёт цепочку с росс-гос-CA, которого нет в trust-store контейнера → verify=True падал CERTIFICATE_VERIFY_FAILED, весь geoportal-слой (C8b/D9b/#1085) не работал бы с прода. Данные публичные open-data, секреты не передаются. Прецедент: nspd_lite/ekburg_permits/pzz_loader. Прод-probe подтвердил. +регрессия-тест. Refs #1067. Co-authored-by: lekss361 Co-committed-by: lekss361 --- .../services/scrapers/ekb_geoportal_client.py | 21 ++++++++--- backend/tests/test_ekb_geoportal_client.py | 35 +++++++++++++++++++ 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/backend/app/services/scrapers/ekb_geoportal_client.py b/backend/app/services/scrapers/ekb_geoportal_client.py index 9ddeb183..28341332 100644 --- a/backend/app/services/scrapers/ekb_geoportal_client.py +++ b/backend/app/services/scrapers/ekb_geoportal_client.py @@ -71,23 +71,34 @@ WFS_LAYERS: dict[str, str] = { 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 — мокается в тестах) ─────────────────────────── def _http_get_json(url: str, params: dict[str, str], *, timeout: int = DEFAULT_TIMEOUT) -> Any: """GET WFS-эндпоинта → JSON. Вынесен на module-level для monkeypatch в тестах. - Геопортал ЕКБ — нормальный TLS, без auth: обычный httpx без спец-обвязки (в отличие - от НСПД с нац-CA / WAF). Raises httpx.HTTPStatusError на 4xx/5xx (caller решает). + verify=_VERIFY_TLS (False): росс-гос-CA геопортала не в trust-store контейнера (см. выше). + 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() return resp.json() def _http_post_json(url: str, body: dict[str, Any], *, timeout: int = DEFAULT_TIMEOUT) -> Any: - """POST JSON → JSON (для searchByGeom / urbanCard). Module-level для monkeypatch в тестах.""" - resp = httpx.post(url, json=body, timeout=timeout) + """POST JSON → JSON (searchByGeom / urbanCard). Module-level для monkeypatch в тестах. + + 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() return resp.json() diff --git a/backend/tests/test_ekb_geoportal_client.py b/backend/tests/test_ekb_geoportal_client.py index 73883e02..ae44e05b 100644 --- a/backend/tests/test_ekb_geoportal_client.py +++ b/backend/tests/test_ekb_geoportal_client.py @@ -200,3 +200,38 @@ def test_zone_regulation_at_none_without_terzone(monkeypatch: pytest.MonkeyPatch lambda url, body, **kw: {"10019": [{"typeAlias": "GknParcel", "key": 1}]}, ) 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