fix(mera/b2c): выключенная ручка расчёта подтверждала своё существование и печатала схему #3233
2 changed files with 62 additions and 5 deletions
|
|
@ -548,7 +548,25 @@ _TOKEN_TTL = "7 days"
|
|||
|
||||
|
||||
def _require_public_estimate_enabled() -> None:
|
||||
"""404, пока публичный расчёт не включён владельцем явно."""
|
||||
"""404, пока публичный расчёт не включён владельцем явно.
|
||||
|
||||
ВЕШАЕТСЯ ЧЕРЕЗ `dependencies=[Depends(...)]` НА ДЕКОРАТОР, А НЕ ВЫЗЫВАЕТСЯ
|
||||
ПЕРВОЙ СТРОКОЙ ТЕЛА. Разница не косметическая: FastAPI решает зависимости
|
||||
РАНЬШЕ, чем валидирует тело запроса. Пока проверка стояла в теле, до неё
|
||||
просто не доходило — невалидное тело отбивалось 422 ещё на разборе, и
|
||||
выключенная ручка отвечала так:
|
||||
|
||||
POST /api/public/mera/estimate {} → 422 + перечень полей
|
||||
POST /api/public/mera/estimate {валидное} → 404
|
||||
POST /api/public/mera/nosuchthing → 401 (rbac)
|
||||
|
||||
То есть 422 подтверждал существование ручки (несуществующий путь даёт 401)
|
||||
и заодно выдавал её схему: address, area_m2, rooms, consent. Замер на проде
|
||||
29.08.2026, ровно то, что этот докстринг обещал не делать.
|
||||
|
||||
Как зависимость проверка срабатывает до разбора тела, и выключенная ручка
|
||||
неотличима от отсутствующей при ЛЮБОМ входе.
|
||||
"""
|
||||
if not settings.public_estimate_enabled:
|
||||
raise HTTPException(status_code=404, detail="Not Found")
|
||||
|
||||
|
|
@ -632,7 +650,11 @@ def _coverage_for(
|
|||
)
|
||||
|
||||
|
||||
@router.post("/estimate", response_model=PublicEstimateResult)
|
||||
@router.post(
|
||||
"/estimate",
|
||||
response_model=PublicEstimateResult,
|
||||
dependencies=[Depends(_require_public_estimate_enabled)],
|
||||
)
|
||||
async def public_estimate(
|
||||
request: Request,
|
||||
response: Response,
|
||||
|
|
@ -652,7 +674,6 @@ async def public_estimate(
|
|||
контур (сосед, `/api/v1/trade-in/r/{token}`) открывает его после оплаты по
|
||||
своему токену. Наружу здесь уезжает только `PublicEstimateResult`.
|
||||
"""
|
||||
_require_public_estimate_enabled()
|
||||
_enforce(_estimate_limiter, request, "estimate")
|
||||
|
||||
daily_retry = _daily_estimate_limiter.retry_after(_ESTIMATE_GLOBAL_KEY)
|
||||
|
|
@ -709,7 +730,11 @@ async def public_estimate(
|
|||
)
|
||||
|
||||
|
||||
@router.post("/estimate/read", response_model=PublicEstimateResult)
|
||||
@router.post(
|
||||
"/estimate/read",
|
||||
response_model=PublicEstimateResult,
|
||||
dependencies=[Depends(_require_public_estimate_enabled)],
|
||||
)
|
||||
def public_estimate_read(
|
||||
request: Request,
|
||||
payload: PublicEstimateTokenInput,
|
||||
|
|
@ -726,7 +751,6 @@ def public_estimate_read(
|
|||
Просрочка и «нет такого токена» отвечают ОДИНАКОВО (404): различать их
|
||||
значит подтверждать существование расчёта тому, кто угадал токен.
|
||||
"""
|
||||
_require_public_estimate_enabled()
|
||||
_enforce(_estimate_read_limiter, request, "estimate-read")
|
||||
|
||||
row = db.execute(
|
||||
|
|
|
|||
|
|
@ -382,3 +382,36 @@ def test_daily_budget_stops_estimates_for_everyone(client: TestClient, flag_on:
|
|||
assert resp.status_code == 429, resp.text
|
||||
assert int(resp.headers["Retry-After"]) > 0
|
||||
assert not estimate_mock.called
|
||||
|
||||
|
||||
# ── Выключенная ручка не должна подтверждать, что она существует ─────────────
|
||||
#
|
||||
# Замер на проде 29.08.2026 (флаг выключен) показал, что обещание докстринга не
|
||||
# выполнялось: проверка флага стояла ПЕРВОЙ СТРОКОЙ ТЕЛА, а FastAPI валидирует
|
||||
# тело запроса раньше, чем доходит до кода хендлера. Итог:
|
||||
# {} → 422 со списком полей address/area_m2/rooms/consent
|
||||
# {валидное} → 404
|
||||
# чужой путь → 401
|
||||
# 422 отличался и от 404, и от 401 — то есть подтверждал существование ручки и
|
||||
# заодно печатал её схему. Гейт переехал в `dependencies=[Depends(...)]`:
|
||||
# зависимости решаются ДО разбора тела.
|
||||
def test_disabled_estimate_hides_itself_from_any_input(client: TestClient) -> None:
|
||||
for body in ({}, {"address": "x"}, _BODY):
|
||||
got = client.post(f"{PREFIX}/estimate", json=body)
|
||||
assert got.status_code == 404, (
|
||||
f"выключенная ручка ответила {got.status_code} на теле {body!r}. "
|
||||
"Любой другой код отличает её от несуществующего пути, а 422 "
|
||||
"вдобавок печатает схему запроса"
|
||||
)
|
||||
assert "address" not in got.text, (
|
||||
"в ответе выключенной ручки видно имя поля запроса — схема утекла"
|
||||
)
|
||||
|
||||
|
||||
def test_disabled_read_hides_itself_from_any_input(client: TestClient) -> None:
|
||||
for body in ({}, {"token": "x"}):
|
||||
got = client.post(f"{PREFIX}/estimate/read", json=body)
|
||||
assert got.status_code == 404, (
|
||||
f"выключенное чтение по токену ответило {got.status_code} на {body!r}"
|
||||
)
|
||||
assert "token" not in got.text, "схема чтения утекла при выключенном флаге"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue