feat(tradein/auth): auth-core — login/logout, sessions, dual-mode rbac (#2552) #2561

Merged
lekss361 merged 2 commits from feat/tradein-auth-core into main 2026-07-30 17:19:45 +00:00
Owner

Closes #2552 (эпик #2549, шаг 3/8).

  • app/services/auth_session.py: create/get/revoke сессий, sliding TTL (refresh ≤1 раз в 5 мин), DB_ROLE_PATHS (employee≈pilot, manager +/api/v1/team/**, admin /**)
  • app/api/v1/auth.py: POST /login (bcrypt verify, единый 401, rate-limit username+IP → 429, события login_success/login_failed в user_events, httponly+secure+lax cookie), POST /logout
  • app/core/rbac.py: dual-mode — session-cookie приоритетен, legacy X-Authenticated-User+roles.yaml без изменений при AUTH_MODE=dual; /auth/login|logout исключены из auth-гейта
  • app/api/v1/me.py: session-first; app/main.py: mount + non-fatal warning при пустом SESSION_SECRET
  • Прокидка имени в middleware: rbac_guard инжектит X-Authenticated-User в request.scope["headers"] для session-юзеров → RequestAudit (читает после call_next) видит корректное имя; RateLimit (читает до) для session-запросов остаётся по-IP — задокументированный trade-off, не регрессия

Тесты: +34 (session unit + API integration: 401-кейсы, 429, logout, dual/db_only, expired session, legacy regression). Полный uv run pytest -q: 1 failed — test_search_cache_hit, pre-existing (идентично падает на чистом forgejo/main, проверено stash-rerun'ом; на CI main зелёный — локальный env).

Деплой безопасен без SESSION_SECRET (opaque-токены не требуют подписи, только warning).

Closes #2552 (эпик #2549, шаг 3/8). - `app/services/auth_session.py`: create/get/revoke сессий, sliding TTL (refresh ≤1 раз в 5 мин), `DB_ROLE_PATHS` (employee≈pilot, manager +`/api/v1/team/**`, admin `/**`) - `app/api/v1/auth.py`: `POST /login` (bcrypt verify, единый 401, rate-limit username+IP → 429, события login_success/login_failed в user_events, httponly+secure+lax cookie), `POST /logout` - `app/core/rbac.py`: dual-mode — session-cookie приоритетен, legacy `X-Authenticated-User`+roles.yaml без изменений при `AUTH_MODE=dual`; `/auth/login|logout` исключены из auth-гейта - `app/api/v1/me.py`: session-first; `app/main.py`: mount + non-fatal warning при пустом SESSION_SECRET - Прокидка имени в middleware: rbac_guard инжектит `X-Authenticated-User` в `request.scope["headers"]` для session-юзеров → RequestAudit (читает после call_next) видит корректное имя; RateLimit (читает до) для session-запросов остаётся по-IP — задокументированный trade-off, не регрессия Тесты: +34 (session unit + API integration: 401-кейсы, 429, logout, dual/db_only, expired session, legacy regression). Полный `uv run pytest -q`: 1 failed — `test_search_cache_hit`, **pre-existing** (идентично падает на чистом forgejo/main, проверено stash-rerun'ом; на CI main зелёный — локальный env). Деплой безопасен без SESSION_SECRET (opaque-токены не требуют подписи, только warning).
lekss361 added 1 commit 2026-07-30 07:50:16 +00:00
feat(tradein/auth): auth-core — login/logout, sessions, dual-mode rbac (#2552)
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 1m19s
0835266516
Foundation для эпика #2549: session-cookie auth поверх legacy Caddy
trusted-header. app.services.auth_session — CRUD для tradein_sessions
(create/get/revoke) + get_user_by_username для password-логина; opaque
secrets.token_urlsafe токены, sliding last_seen_at/expires_at refresh
(не чаще раза в 5 минут).

POST /api/v1/auth/login проверяет password_hash (bcrypt) через
app.core.password, ставит httponly+secure cookie, пишет
login_success/login_failed в user_events; per-username+IP rate-limit
(SlidingWindowLimiter) отдельно от общего RateLimitMiddleware. POST
/logout ревокает сессию и чистит cookie. Оба пути exempt из rbac_guard's
auth-required gate (иначе логин сам себя не пропустил бы).

rbac_guard теперь dual-mode: session-cookie резолвится первым (DB-роль
employee/manager/admin -> paths как у pilot/+team/admin), fallback на
legacy X-Authenticated-User + roles.yaml БЕЗ ИЗМЕНЕНИЙ когда auth_mode
== "dual"; auth_mode == "db_only" отключает legacy header полностью.
Резолвленный сессией username инжектится в ASGI scope headers (до
call_next) — RequestAuditMiddleware и downstream route-хендлеры видят
его прозрачно; RateLimitMiddleware (внешний относительно rbac_guard)
для session-запросов лимитирует по IP, не по username — документированный
trade-off, не регрессия.

GET /me — session-first: валидная cookie отдаёт scope из tradein_users
без похода в roles.yaml; без cookie — прежний legacy путь. session_secret
остаётся опциональным (opaque-токены не требуют подписи) — пустое
значение только logger.warning на старте, не startup-fail.

Полный набор тестов (tests/test_rbac.py, test_internal_auth_secret.py,
test_account_quota.py) проходит без правок — regression-safe.
Author
Owner

Deep review — 🔴 BLOCK (1 critical)

CI CI Trade-In / backend-tests = success на 08352665, test_search_cache_hit на CI не падает (pre-existing локальный — подтверждено). Легаси-путь rbac.py сверен построчно: бит-в-бит, только обёрнут в if not from_session:. Сессии/логин/rate-limit — в целом добротно. Блокирует одна находка.

🔴 app/core/rbac.py:_propagate_authenticated_user — клиент управляет downstream-identity

if request.headers.get("X-Authenticated-User"):
    return   # <-- клиентский заголовок ВЫИГРЫВАЕТ у сессии

Воспроизведено локально поверх вашего же fake-DB харнесса (tests/test_auth_api.py), эндпоинт-эхо читает Header(alias="X-Authenticated-User") — ровно как все хендлеры trade_in.py:

[без клиентского заголовка]      downstream saw: {'seen': 'alice'}          # ок
[dual + подделанный заголовок]    200, downstream saw: {'seen': 'victim_or_admin'}
[db_only + подделанный заголовок] 200, downstream saw: {'seen': 'victim_or_admin'}

Последствия: _assert_estimate_access / GET /trade-in/history / account_quota / support.py::_username берут identity из сырого заголовка и резолвят роль через legacy get_role() из roles.yaml → сессионный employee, подставив имя roles.yaml-админа, читает оценки всех аккаунтов, чужие support-треды и списывает квоту на жертву. rbac_guard при этом авторизует его как employee — гейт /api/v1/admin/* держится, но данные утекают мимо.

Почему это не только теория:

  • Сессионная ветка целиком пропускает #2213 X-Internal-Auth-Secret — то есть ровно ту защиту, которая ставилась против запросов мимо Caddy изнутри gendesign_shared. С валидной сессией этот контроль обходится.
  • Снаружи сегодня прикрыто Caddy (header_up X-Authenticated-User {http.auth.user.id} перезаписывает клиентское значение). Но эпик #2549 как раз снимает basic_auth — мина взводится на шаги 4-8.
  • Побочно: раз Caddy ставит заголовок ВСЕГДА, в проде early-return срабатывает на каждом запросе → прокидка session-identity сейчас мёртвый код, а хендлеры действуют от имени basic_auth-юзера, пока rbac_guard авторизует сессионного. Расхождение identity между гейтом и данными.
  • Утверждение в описании PR «в db_only клиентский заголовок игнорируется полностью» верно только для rbac_guard, не для хендлеров.

Фикс (перезапись вместо skip, ASGI-имена всегда lowercase bytes):

def _propagate_authenticated_user(request: Request, username: str) -> None:
    request.scope["headers"] = [
        *(
            (k, v)
            for k, v in request.scope.get("headers", [])
            if k != b"x-authenticated-user"
        ),
        (b"x-authenticated-user", username.encode("latin-1", "replace")),
    ]
  • тест: валидная сессия + подделанный X-Authenticated-User → downstream видит сессионного юзера (сейчас такого кейса в 34 новых тестах нет — это и есть пробел покрытия).

🟡 Medium

  1. rbac.pyusername.encode("latin-1") на кириллическом username даёт UnicodeEncodeError → 500 на КАЖДОМ запросе такого юзера. tradein_users.usernametext NOT NULL UNIQUE без ASCII-ограничения, seed #2557 такое имя вполне заведёт. Лечится "replace" в фиксе выше (или CHECK на колонке).
  2. api/v1/auth.py:login — таймингового выравнивания нет: credentials_ok короткозамыкается до verify_password, поэтому неизвестный юзер / NULL-хеш отвечают за ~1 мс, а существующий с паролем — за ~100-300 мс (bcrypt). Единый 401 по тексту есть, но user-enumeration остаётся по времени (rate-limit смягчает, не закрывает). Фикс: прогонять verify_password против фиксированного dummy-хеша, когда юзер не найден/hash NULL.

🟢 Low / follow-up

  • tradein_sessions — нет джоба уборки протухших строк (индекс на expires_at есть, метельщика нет); токен лежит плейнтекстом (хеш токена был бы аккуратнее).
  • Ключ login-лимитера f"{username}:{ip}" — разделитель : не экранирован, теоретические коллизии; username не ограничен по длине.
  • test_session_user_can_reach_tradein_but_not_adminassert status in (401, 403, 404) пропустит и случайный 404 от отсутствующего роута.

Проверено и в порядке

  • Легаси-путь при AUTH_MODE=dual без cookie — бит-в-бит (порядок: нет заголовка → 401, #2213 secret → 401, get_role KeyError → 403, admin-гейт, scope с fail-open); tests/test_rbac.py и test_internal_auth_secret.py не тронуты и зелёные.
  • Без cookie — ноль новых походов в БД (request.cookies.get до SessionLocal()); коннект в rbac_guard берётся и отдаётся ДО call_next, с пулом не конфликтует.
  • secrets.token_urlsafe(32), cookie httponly+secure+samesite=lax, expired/is_active проверки, revoke, sliding refresh ≤1/5 мин (нет UPDATE на каждый запрос).
  • SQL — только text() с bind-параметрами и CAST(:x AS ...), f-string'ов нет; tradein_sessions.token = PK (индекс есть).
  • Raw-пароль не уходит в логи / user_events / Sentry (send_default_pii=False, include_local_variables=False).
  • Порядок middleware подтверждает комментарий: RequestAudit → RateLimit → CORS → rbac_guard, аудит читает заголовки после call_next и видит инъекцию.
  • 401 при NULL password_hash без вызова bcrypt на None; лимитер пустые корзины подчищает при >10k ключей.

После фикса 🔴 + теста на спуфинг — готов к повторному ревью и мержу.

## Deep review — 🔴 BLOCK (1 critical) CI `CI Trade-In / backend-tests` = success на 08352665, `test_search_cache_hit` на CI не падает (pre-existing локальный — подтверждено). Легаси-путь rbac.py сверен построчно: бит-в-бит, только обёрнут в `if not from_session:`. Сессии/логин/rate-limit — в целом добротно. Блокирует одна находка. ### 🔴 `app/core/rbac.py:_propagate_authenticated_user` — клиент управляет downstream-identity ```python if request.headers.get("X-Authenticated-User"): return # <-- клиентский заголовок ВЫИГРЫВАЕТ у сессии ``` Воспроизведено локально поверх вашего же fake-DB харнесса (`tests/test_auth_api.py`), эндпоинт-эхо читает `Header(alias="X-Authenticated-User")` — ровно как все хендлеры `trade_in.py`: ``` [без клиентского заголовка] downstream saw: {'seen': 'alice'} # ок [dual + подделанный заголовок] 200, downstream saw: {'seen': 'victim_or_admin'} [db_only + подделанный заголовок] 200, downstream saw: {'seen': 'victim_or_admin'} ``` Последствия: `_assert_estimate_access` / `GET /trade-in/history` / `account_quota` / `support.py::_username` берут identity из сырого заголовка и резолвят роль через legacy `get_role()` из roles.yaml → сессионный employee, подставив имя roles.yaml-админа, читает оценки всех аккаунтов, чужие support-треды и списывает квоту на жертву. `rbac_guard` при этом авторизует его как employee — гейт `/api/v1/admin/*` держится, но данные утекают мимо. Почему это не только теория: - Сессионная ветка целиком пропускает #2213 `X-Internal-Auth-Secret` — то есть ровно ту защиту, которая ставилась против запросов мимо Caddy изнутри `gendesign_shared`. С валидной сессией этот контроль обходится. - Снаружи сегодня прикрыто Caddy (`header_up X-Authenticated-User {http.auth.user.id}` перезаписывает клиентское значение). Но эпик #2549 как раз снимает basic_auth — мина взводится на шаги 4-8. - Побочно: раз Caddy ставит заголовок ВСЕГДА, в проде early-return срабатывает на каждом запросе → прокидка session-identity сейчас мёртвый код, а хендлеры действуют от имени basic_auth-юзера, пока rbac_guard авторизует сессионного. Расхождение identity между гейтом и данными. - Утверждение в описании PR «в db_only клиентский заголовок игнорируется полностью» верно только для rbac_guard, не для хендлеров. Фикс (перезапись вместо skip, ASGI-имена всегда lowercase bytes): ```python def _propagate_authenticated_user(request: Request, username: str) -> None: request.scope["headers"] = [ *( (k, v) for k, v in request.scope.get("headers", []) if k != b"x-authenticated-user" ), (b"x-authenticated-user", username.encode("latin-1", "replace")), ] ``` + тест: валидная сессия + подделанный `X-Authenticated-User` → downstream видит сессионного юзера (сейчас такого кейса в 34 новых тестах нет — это и есть пробел покрытия). ### 🟡 Medium 1. `rbac.py` — `username.encode("latin-1")` на кириллическом username даёт `UnicodeEncodeError` → 500 на КАЖДОМ запросе такого юзера. `tradein_users.username` — `text NOT NULL UNIQUE` без ASCII-ограничения, seed #2557 такое имя вполне заведёт. Лечится `"replace"` в фиксе выше (или CHECK на колонке). 2. `api/v1/auth.py:login` — таймингового выравнивания нет: `credentials_ok` короткозамыкается до `verify_password`, поэтому неизвестный юзер / NULL-хеш отвечают за ~1 мс, а существующий с паролем — за ~100-300 мс (bcrypt). Единый 401 по тексту есть, но user-enumeration остаётся по времени (rate-limit смягчает, не закрывает). Фикс: прогонять `verify_password` против фиксированного dummy-хеша, когда юзер не найден/hash NULL. ### 🟢 Low / follow-up - `tradein_sessions` — нет джоба уборки протухших строк (индекс на `expires_at` есть, метельщика нет); токен лежит плейнтекстом (хеш токена был бы аккуратнее). - Ключ login-лимитера `f"{username}:{ip}"` — разделитель `:` не экранирован, теоретические коллизии; username не ограничен по длине. - `test_session_user_can_reach_tradein_but_not_admin` — `assert status in (401, 403, 404)` пропустит и случайный 404 от отсутствующего роута. ### ✅ Проверено и в порядке - Легаси-путь при `AUTH_MODE=dual` без cookie — бит-в-бит (порядок: нет заголовка → 401, #2213 secret → 401, `get_role` KeyError → 403, admin-гейт, scope с fail-open); `tests/test_rbac.py` и `test_internal_auth_secret.py` не тронуты и зелёные. - Без cookie — ноль новых походов в БД (`request.cookies.get` до `SessionLocal()`); коннект в `rbac_guard` берётся и отдаётся ДО `call_next`, с пулом не конфликтует. - `secrets.token_urlsafe(32)`, cookie httponly+secure+samesite=lax, expired/`is_active` проверки, revoke, sliding refresh ≤1/5 мин (нет UPDATE на каждый запрос). - SQL — только `text()` с bind-параметрами и `CAST(:x AS ...)`, f-string'ов нет; `tradein_sessions.token` = PK (индекс есть). - Raw-пароль не уходит в логи / user_events / Sentry (`send_default_pii=False`, `include_local_variables=False`). - Порядок middleware подтверждает комментарий: RequestAudit → RateLimit → CORS → rbac_guard, аудит читает заголовки после `call_next` и видит инъекцию. - 401 при NULL `password_hash` без вызова bcrypt на `None`; лимитер пустые корзины подчищает при >10k ключей. После фикса 🔴 + теста на спуфинг — готов к повторному ревью и мержу.
bot-backend added 1 commit 2026-07-30 17:15:47 +00:00
fix(tradein/auth): предотвратить подмену X-Authenticated-User при session-auth (#2552)
All checks were successful
CI / changes (pull_request) Successful in 10s
CI Trade-In / 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 1m25s
4ecc3d689c
CRITICAL: _propagate_authenticated_user делала skip-if-present вместо
перезаписи — клиент-контролируемый X-Authenticated-User (Caddy шлёт его
на КАЖДЫЙ прод-запрос) выигрывал у резолвленной сессии для всего
downstream-трафика, читающего заголовок напрямую (_assert_estimate_access*,
account_quota, /trade-in/history, support.py) — в обоих auth_mode
(dual и db_only). Теперь заголовок безусловно перезаписывается сессионным
username (ASGI header-имена всегда lowercase bytes).

Medium: .encode("latin-1") без errors="replace" крашил бы 500-кой каждый
запрос кириллического username. Login timing-oracle — verify_password
короткозамыкалась на unknown-username/NULL-hash (~1мс vs ~100-300мс bcrypt)
→ теперь всегда сверяется против dummy-хеша при отсутствующем юзере/хеше.
Login rate-limit key length-prefixed — username с ':' (или IPv6 IP) больше
не может схлопнуть чужой бюджет.

Новые тесты подтверждают регрессию: прогнаны на старом коде (до фикса)
через временный откат rbac.py — все три (spoof dual-mode, spoof db_only,
кириллица) падали с 'victim' == 'alice' / UnicodeEncodeError; после
фикса — зелёные. test_rbac.py/test_internal_auth_secret.py без изменений.
lekss361 merged commit d3e0aa296c into main 2026-07-30 17:19:45 +00:00
Author
Owner

Re-review фикса 4ecc3d68 APPROVE, смержено

Проверял независимо: свой репро-набор поверх вашего fake-DB харнесса (_Store/_FakeDB), новые тесты воркера не переиспользовал.

🔴 закрыт — подтверждено

сценарий downstream saw
dual + X-Authenticated-User: victim_or_admin alice
db_only + подделанный заголовок alice
без клиентского заголовка alice
X-AUTHENTICATED-USER (верхний регистр) alice
два дублирующихся заголовка сразу alice

Регистр и дубли — сверх того, что покрыл фикс-коммит: ASGI нормализует имена в lowercase bytes до middleware, фильтр по b"x-authenticated-user" ловит все варианты.

(б) Legacy-путь не сломан — ключевой риск фикса, проверен отдельно

_propagate_authenticated_user вызывается ровно в одном месте (rbac.py:174) под if session_user is not None, поэтому фильтрация не может задеть не-session-трафик. Эмпирически:

сценарий downstream saw
без cookie + X-Authenticated-User: kopylov kopylov нетронут
протухшая cookie + тот же заголовок kopylov
несуществующая cookie + тот же заголовок kopylov
без cookie и без заголовка 401

tests/test_rbac.py, test_internal_auth_secret.py, test_auth_api.py, test_auth_session.py, test_password.py, test_ratelimit.py — 91 passed на фикс-коммите.

(в) Echo-роут не протёк

grep -rn whoami app/ → пусто; встречается только в tests/test_auth_api.py. Роутер прода чист.

(г) Dummy-хеш

_DUMMY_PASSWORD_HASH = 184 мс единожды на импорте (bcrypt rounds=12), импорт модуля целиком 877 мс — для старта контейнера незаметно. Не логируется, случайный на процесс, в репозиторий не попадает. verify_password теперь вызывается ровно один раз на попытку → таймингового оракула нет. Length-prefixed ключ лимитера корректно разводит и : в username, и IPv6 в IP.

🟡 Остаётся — follow-up на seed #2557 (не блокер)

encode("latin-1", "replace") убирает 500, но кириллические username одинаковой длины схлопываются в одну строку:

'иванов' -> '??????'
'петров' -> '??????'
'alice'  -> 'alice'

То есть двое таких юзеров получают ОДИН downstream-identity → общий created_by, общая квота, взаимный IDOR, общий support-тред. Тихая коллизия неприятнее прежнего громкого 500.

Не блокирую, потому что недостижимо сегодня: tradein_users едет пустой (миграция 192 явно без seed, INSERT INTO tradein_users в ветке нет) → сессия в проде пока не может существовать вовсе. Просьба закрыть в #2557: CHECK (username ~ '^[ -~]+$') на колонке, либо fail-closed ветка в _propagate_authenticated_user на не-кодируемом username (лучше 401, чем молчаливая склейка).

🟢 Прежние low остаются как есть

Нет уборки протухших tradein_sessions; токен в БД плейнтекстом.

Merge / deploy

  • CI Trade-In / backend-tests — success, 1m25s на 4ecc3d68 (явный, не skipped). test_search_cache_hit на CI не падает.
  • Смержено: d3e0aa29.
  • deploy-tradein run 6052 — test / build-backend / deploy success, cleanup 6053 success.
  • Пост-деплой смоук: /trade-in/api/v1/health и /trade-in/ отдают 401 от Caddy basic_auth (не 502) за ~0.1с — стек поднялся.
## Re-review фикса `4ecc3d68` — ✅ APPROVE, смержено Проверял независимо: свой репро-набор поверх вашего fake-DB харнесса (`_Store`/`_FakeDB`), новые тесты воркера не переиспользовал. ### 🔴 закрыт — подтверждено | сценарий | downstream saw | |---|---| | dual + `X-Authenticated-User: victim_or_admin` | `alice` ✅ | | db_only + подделанный заголовок | `alice` ✅ | | без клиентского заголовка | `alice` ✅ | | `X-AUTHENTICATED-USER` (верхний регистр) | `alice` ✅ | | два дублирующихся заголовка сразу | `alice` ✅ | Регистр и дубли — сверх того, что покрыл фикс-коммит: ASGI нормализует имена в lowercase bytes до middleware, фильтр по `b"x-authenticated-user"` ловит все варианты. ### (б) Legacy-путь не сломан — ключевой риск фикса, проверен отдельно `_propagate_authenticated_user` вызывается ровно в одном месте (`rbac.py:174`) под `if session_user is not None`, поэтому фильтрация не может задеть не-session-трафик. Эмпирически: | сценарий | downstream saw | |---|---| | без cookie + `X-Authenticated-User: kopylov` | `kopylov` ✅ нетронут | | протухшая cookie + тот же заголовок | `kopylov` ✅ | | несуществующая cookie + тот же заголовок | `kopylov` ✅ | | без cookie и без заголовка | `401` ✅ | `tests/test_rbac.py`, `test_internal_auth_secret.py`, `test_auth_api.py`, `test_auth_session.py`, `test_password.py`, `test_ratelimit.py` — 91 passed на фикс-коммите. ### (в) Echo-роут не протёк `grep -rn whoami app/` → пусто; встречается только в `tests/test_auth_api.py`. Роутер прода чист. ### (г) Dummy-хеш `_DUMMY_PASSWORD_HASH` = 184 мс единожды на импорте (bcrypt rounds=12), импорт модуля целиком 877 мс — для старта контейнера незаметно. Не логируется, случайный на процесс, в репозиторий не попадает. `verify_password` теперь вызывается ровно один раз на попытку → таймингового оракула нет. Length-prefixed ключ лимитера корректно разводит и `:` в username, и IPv6 в IP. ### 🟡 Остаётся — follow-up на seed #2557 (не блокер) `encode("latin-1", "replace")` убирает 500, но кириллические username одинаковой длины схлопываются в одну строку: ``` 'иванов' -> '??????' 'петров' -> '??????' 'alice' -> 'alice' ``` То есть двое таких юзеров получают ОДИН downstream-identity → общий `created_by`, общая квота, взаимный IDOR, общий support-тред. Тихая коллизия неприятнее прежнего громкого 500. Не блокирую, потому что недостижимо сегодня: `tradein_users` едет пустой (миграция 192 явно без seed, `INSERT INTO tradein_users` в ветке нет) → сессия в проде пока не может существовать вовсе. Просьба закрыть в #2557: `CHECK (username ~ '^[ -~]+$')` на колонке, либо fail-closed ветка в `_propagate_authenticated_user` на не-кодируемом username (лучше 401, чем молчаливая склейка). ### 🟢 Прежние low остаются как есть Нет уборки протухших `tradein_sessions`; токен в БД плейнтекстом. ### Merge / deploy - `CI Trade-In / backend-tests` — success, 1m25s на `4ecc3d68` (явный, не skipped). `test_search_cache_hit` на CI не падает. - Смержено: `d3e0aa29`. - `deploy-tradein` run 6052 — `test` / `build-backend` / `deploy` success, cleanup 6053 success. - Пост-деплой смоук: `/trade-in/api/v1/health` и `/trade-in/` отдают 401 от Caddy basic_auth (не 502) за ~0.1с — стек поднялся.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2561
No description provided.