gendesign/tradein-mvp/backend/app/core/fdw.py
bot-backend 3d075632a9
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 2m28s
chore(tradein/geocoder): удалить Яндекс-геокодер (#2593)
Убирает ядро Yandex Geocoder (forward/reverse/suggest lookups + region-check
+ bias-хелперы + EKB_BBOX dict) из app/services/geocoder.py — Yandex demo-key
исчерпан, Nominatim/DaData/локальные ЕКБ-тиры (geoportal/cadastral) остаются
единственными живыми провайдерами. Цепочка тиров после удаления: кэш →
геопортал ЕКБ → кадастр (house-match) → кадастр (raw) → Nominatim; в
подсказках дополнительно DaData.

НЕ затронуто (намеренно): Yandex.Недвижимость как источник объявлений
(source='yandex', yandex_city_sweep*, providers/yandex/serp.py:geocoderAddress),
Avito geocoder (providers/avito/imv.py:_geocode), EKB_BBOX_TIGHT/WIDE,
_nominatim_region_ok, scripts/*_yandex_reverse.py и их тесты, tests/fixtures/
yandex_geocode_sample.json (всё ещё используется test_audit_address_mismatch.py).

_SNAP_PRECISIONS оставлен с "exact" (недостижимо без Yandex-tier, но дёшево
хранить — parity с frontend MapPicker.tsx SNAP_PRECISIONS и не ломает
test_snap_precision_useful_exact_and_number).
2026-07-31 21:22:09 +03:00

86 lines
3.2 KiB
Python

"""postgres_fdw USER MAPPING management for gendesign_remote server.
Password lives in env GENDESIGN_FDW_PASSWORD — never in SQL migration or git.
This helper:
- validates password format (alphanumeric/hex, length >= 32) to prevent SQL
injection via embedded quotes (the postgres CREATE USER MAPPING syntax does
not accept bind parameters — password must be inlined into the DDL string),
- applies idempotent CREATE or ALTER mapping on every backend startup so
password rotation through .env.runtime is picked up after restart.
"""
from __future__ import annotations
import logging
import re
from sqlalchemy import text
from sqlalchemy.orm import Session
from app.core.config import settings
logger = logging.getLogger(__name__)
# Strict whitelist — only hex/alphanumeric and a few separators. Refuses ANY
# quote, backslash, unicode whitespace, control char. Min length 32 forces
# rotation away from weak defaults.
_PASSWORD_RE = re.compile(r"^[A-Za-z0-9_\-]{32,256}$")
def ensure_fdw_user_mapping(db: Session) -> None:
"""Idempotent CREATE OR ALTER USER MAPPING for tradein → gendesign_remote.
Skips silently if password not configured (dev / first deploy / forgotten env).
Raises ValueError if password fails format whitelist — fail-fast on misconfig.
"""
password = settings.gendesign_fdw_password
if not password or not str(password).strip():
logger.warning(
"GENDESIGN_FDW_PASSWORD not set — skipping FDW user mapping "
"(gendesign_cad_buildings queries will fail; cadastral lookups will "
"fall back to Nominatim)"
)
return
if not _PASSWORD_RE.fullmatch(password):
# Do NOT echo the password into logs / exception even partially.
raise ValueError(
"GENDESIGN_FDW_PASSWORD failed format whitelist "
"(allowed: 32-256 chars, [A-Za-z0-9_-]). Refusing to apply USER MAPPING "
"— password rotation must use the same format."
)
# NB: even after whitelist validation we still cannot use SQLAlchemy bind
# parameters here — postgres CREATE/ALTER USER MAPPING is DDL and treats
# OPTIONS values as literal text. Whitelist guarantees the value is
# injection-safe; we still wrap in single quotes for the DDL syntax.
exists = db.execute(
text(
"SELECT 1 FROM pg_user_mappings "
"WHERE srvname = 'gendesign_remote' "
"AND (usename = current_user OR usename IS NULL)"
)
).first()
if exists is None:
db.execute(
text(
f"CREATE USER MAPPING FOR CURRENT_USER SERVER gendesign_remote "
f"OPTIONS (user 'tradein_fdw_reader', password '{password}')"
)
)
logger.info("created FDW user mapping for gendesign_remote")
else:
db.execute(
text(
f"ALTER USER MAPPING FOR CURRENT_USER SERVER gendesign_remote "
f"OPTIONS (SET password '{password}')"
)
)
logger.info("refreshed FDW user mapping password for gendesign_remote")
try:
db.commit()
except Exception:
db.rollback()
raise