fix(tradein/rbac): 401-оракул закрыт — несуществующий путь под публичным префиксом отвечает так же, как любой другой мусор #3352

Merged
bot-backend merged 2 commits from fix/3324-rbac-401-oracle into main 2026-09-05 18:19:29 +00:00
Collaborator

Refs #3324, периметр п.2 (последний пункт периметра; issue остаётся до решения по п.1 zero-downtime — нет, п.1 в #3274; здесь закрыт п.2). Аноним, дёрнув /trade-in/api/public/whatever, получал 401 с rbac-текстом, а мусор в другом месте — 404: выключенная ручка отличима от несуществующей.

Механизм

rbac_guardBaseHTTPMiddleware (app/main.py:236), отрабатывает ДО роутинга: пути без хендлера получали 401 от guard'а, а такой же мусор под _PUBLIC_PATH_PREFIXES уходил в роутер и получал 404.

Фикс (rbac.py, +73/−3 с тестами)

_path_is_routed(): route.matches(request.scope)[0] != Match.NONE по router.routes; в rbac_guard сразу после public-проверки: не роутится → call_next → 404 отдаёт тот же роутер, что и всем. Ослабления нет: Match.NONE у всех = хендлера не существует; Match.PARTIAL (путь есть, метод другой) считается маршрутом и идёт в guard как раньше; нет scope["app"] — прежнее поведение (fail-closed).

Тесты

Новый тест сравнивает три пары мусорных путей ответ-к-ответу (status+json), затем «защита цела»: защищённые → 401, публичный → 200. Полный suite: 5340 passed, 35 skipped. Фальсификация (откат только rbac.py): assert (401, …) == (404, …) — по значению.

Одна правка чужого теста: test_session_user_can_reach_tradein_but_not_admin опирался ровно на дефект (/api/v1/admin/dummy не был зарегистрирован, 403 приходил за несуществующий путь) — зарегистрирована реальная ручка по образцу соседних (комментарий «чтобы 403 приходил от scope-чека, а не от отсутствия роута»), ассерт 403 остался.

Прод-приёмка

Снаружи Caddy срезает /trade-in: docker exec tradein-backend curl -s -o /dev/null -w '%{http_code}' localhost:8000/api/public/zzz → 404 (было 401); /api/v1/me без cookie → 401 как раньше.

Refs #3324, периметр п.2 (последний пункт периметра; issue остаётся до решения по п.1 zero-downtime — нет, п.1 в #3274; здесь закрыт п.2). Аноним, дёрнув `/trade-in/api/public/whatever`, получал 401 с rbac-текстом, а мусор в другом месте — 404: выключенная ручка отличима от несуществующей. ## Механизм `rbac_guard` — `BaseHTTPMiddleware` (`app/main.py:236`), отрабатывает ДО роутинга: пути без хендлера получали 401 от guard'а, а такой же мусор под `_PUBLIC_PATH_PREFIXES` уходил в роутер и получал 404. ## Фикс (`rbac.py`, +73/−3 с тестами) `_path_is_routed()`: `route.matches(request.scope)[0] != Match.NONE` по `router.routes`; в `rbac_guard` сразу после public-проверки: не роутится → `call_next` → 404 отдаёт тот же роутер, что и всем. Ослабления нет: `Match.NONE` у всех = хендлера не существует; `Match.PARTIAL` (путь есть, метод другой) считается маршрутом и идёт в guard как раньше; нет `scope["app"]` — прежнее поведение (fail-closed). ## Тесты Новый тест сравнивает три пары мусорных путей **ответ-к-ответу** (status+json), затем «защита цела»: защищённые → 401, публичный → 200. Полный suite: `5340 passed, 35 skipped`. Фальсификация (откат только rbac.py): `assert (401, …) == (404, …)` — по значению. Одна правка чужого теста: `test_session_user_can_reach_tradein_but_not_admin` опирался ровно на дефект (`/api/v1/admin/dummy` не был зарегистрирован, 403 приходил за несуществующий путь) — зарегистрирована реальная ручка по образцу соседних (комментарий «чтобы 403 приходил от scope-чека, а не от отсутствия роута»), ассерт 403 остался. ## Прод-приёмка Снаружи Caddy срезает `/trade-in`: `docker exec tradein-backend curl -s -o /dev/null -w '%{http_code}' localhost:8000/api/public/zzz` → 404 (было 401); `/api/v1/me` без cookie → 401 как раньше.
bot-backend added 1 commit 2026-09-05 17:53:10 +00:00
fix(rbac): не отвечать 401 за несуществующий путь (#3324)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 15s
CI / changes (pull_request) Successful in 18s
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 5m36s
0ef1880e55
rbac_guard — HTTP-middleware, он отрабатывает до роутинга и потому отвечал
401 с rbac-текстом даже на пути, которых в приложении нет. Аноним получал
бесплатный оракул периметра: мусор под «интересным» префиксом давал 401, а
такой же мусор под публичным префиксом — 404 роутера, то есть выключенная
ручка была отличима от несуществующей.

Guard теперь пропускает запрос дальше, если ни один маршрут роутера не
матчится (Match.NONE у всех) — 404 отдаёт тот же роутер, что и на любой
другой мусор. Существующие маршруты не затронуты: Match.PARTIAL (путь есть,
метод другой) по-прежнему идёт в guard, реальный закрытый маршрут анониму
даёт 401, публичный — работает без идентичности.
Light1YT added 1 commit 2026-09-05 18:12:31 +00:00
fix(rbac): честный докстринг про размен оракулов + трейлинг-слэш больше не выдаёт маршрут
All checks were successful
CI Trade-In / changes (pull_request) Successful in 16s
CI / changes (pull_request) Successful in 19s
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 5m51s
8c4be70193
Ревью #3352: комментарий у _path_is_routed утверждал «ослабления нет» — неверно.
Раньше 401 был ПРЕФИКСНЫМ оракулом (таблицу маршрутов по нему не перечислить),
теперь 401/404 — оракул СУЩЕСТВОВАНИЯ маршрута, включая имена admin-ручек.
Докстринг переписан честно, с проверенным по caddy/sites/apps.caddy фактом:
блок handle /trade-in/api/* стоит выше import users.caddy.snippet, внешнего
basic_auth у trade-in нет — значит перебор имён выполним и снаружи.

Второй канал того же оракула закрыт: /api/v1/me/ не матчил ни один маршрут,
guard пропускал, а роутер отвечал 307 на существующий путь. FastAPI получил
redirect_slashes=False (проверено: ни одного route с трейлинг-слэшем, ни одного
такого вызова во фронте; deny-правила уже на глоб-форме).

Тесты: PARTIAL-кейс (POST на GET-путь анониму → 401, не 404/405), трейлинг-слэш
на РЕАЛЬНОМ app.main (тест на копии был бы тавтологией), admin-гейт сверяется по
тексту 'admin only' — scope-ветка отвечает тем же 403, но 'forbidden for role'.
bot-backend merged commit bab4b4f8ff into main 2026-09-05 18:19:29 +00:00
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#3352
No description provided.