feat(mera/b2c): анти-абуз для анонимного трафика — этап 2 из 8 #2546

Merged
lekss361 merged 3 commits from feat/mera-b2c-antiabuse into main 2026-08-25 16:27:03 +00:00

3 commits

Author SHA1 Message Date
bot-backend
bfe58f9160 feat(mera/b2c): бесплатный лимит анонима — 5 расчётов в месяц
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
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 4m33s
Решение владельца при разборе PR #2546 (16.08.2026): в первой редакции
стояло 3. Пять проб — больше шансов, что человек дойдёт до ценности и
купит платный отчёт, ценой чуть более высокого потолка злоупотребления.

Значение переопределяется переменной окружения ANON_ESTIMATE_QUOTA_LIMIT
без выкатки кода, так что пересмотр не требует релиза.

Тесты берут лимит из настроек, а не из литерала, поэтому правка их не
трогает. Единственное место с числом — _ANON_DEFAULT в тесте передачи
произвольного default; там 3 оставлено намеренно и снабжено пояснением,
что число специально не совпадает ни с одной настройкой.
2026-08-17 10:44:03 +03:00
bot-backend
ddcd526284 merge(mera/b2c-antiabuse): влить main — платный флоу, скрубберы Sentry, ревайвл мёртвых оценок
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
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 4m31s
Ветка отстала на ~395 коммитов; за это время в main приехали B2C-этапы 1 и 4,
платный флоу, бесплатная проба покрытия и скрубберы Sentry для платежей (#2794).

Конфликты (везде — совмещение обеих сторон, не выбор):
- tradein-mvp/backend/app/api/v1/trade_in.py: _estimate_limiter/
  _resolve_quota_identity (анти-абуз анонимов, этот PR) сохранены рядом с
  ESTIMATE_READABLE_SQL/estimate_readable (retention, PR-D1, main); оба
  комментария перед вызовом estimate_quality() объединены.
- tradein-mvp/backend/tests/conftest.py: обе autouse-фикстуры
  (_reset_estimate_rate_limiter из этого PR и _no_leaked_password_verify_slots
  из main) сохранены, докстринг и импорты (sys, Path) объединены.

Полный прогон pytest после слияния: 4609 passed, 1 skipped (allowlisted —
WeasyPrint native libs), 0 failed. ruff check app tests — чисто.
2026-08-15 23:10:43 +03:00
bot-backend
eef10f406f feat(mera/b2c): анти-абуз для анонимного трафика — этап 2 из 8
All checks were successful
CI / changes (pull_request) Successful in 12s
CI Trade-In / changes (pull_request) Successful in 12s
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 1m12s
Блокер номер один перед открытием эндпоинта оценки наружу: анонимный запрос
означал БЕЗЛИМИТ. В сервисе квот отсутствие имени пользователя трактовалось
как unlimited во всех функциях, с комментарием «dev без Caddy, fail-open».
Единственной защитой был общий лимит 300 запросов в минуту на IP — это
анти-флуд для дешёвых запросов, а не бизнес-лимит для пайплайна на десятки
секунд.

1. Fail-open больше не по умолчанию. Вызывающая сторона передаёт пустую
   личность только при явно включённом флаге разработки (по умолчанию выкл).

2. Анонимная личность — подписанная кука с HMAC-SHA256, отдельным каналом
   от X-Authenticated-User. Тот заголовок ставит Caddy и валидирует внутренним
   секретом; смешивать схемы нельзя, это сломало бы модель безопасности.
   Ключ подписи из окружения; если не задан — эфемерный на процесс, с
   предупреждением в лог.

3. Анонимная квота на паре «сессия + IP», переиспользует существующую таблицу
   и тот же атомарный инкремент под WHERE used < lim (защита от гонки #747).
   Честно закомментировано: смена IP или чистка куки обходит лимит — задача
   поднять стоимость злоупотребления, а не сделать его невозможным.

4. Отдельный жёсткий лимит частоты на оценку, проверяется ДО квоты.
   Переиспользован готовый SlidingWindowLimiter. Redis намеренно не задействован:
   прод работает одним воркером, состояние теряется только при рестарте, а
   основная защита — месячная квота в Postgres. Компромисс задокументирован.

5. Потолок времени ответа. Вызов Avito IMV шёл БЕЗ бюджета, в отличие от всех
   соседних — единственный источник неограниченного времени. Обёрнут.
   Суммарный худший случай: было ~186 с (36 с ограниченных плюс IMV без
   границы ~150 с), стало 56 с.

Тесты: 2754 passed. Два существующих теста обновлены под изменившееся
поведение fail-open — это ожидаемое изменение, не регрессия.
2026-07-28 15:21:03 +03:00