fix(tradein/rbac): аноним на admin-префиксе получает единый 404 — существование admin-ручек снаружи не перебирается #3371

Merged
bot-backend merged 3 commits from fix/3360-admin-prefix-anon-404 into main 2026-09-05 20:32:06 +00:00
Collaborator

Closes #3360 [SECURITY] (находка доводки #3352). /trade-in/api/v1/admin/* снаружи доходит до guard'а анонимно (Caddy не режет префикс), а после #3352 аноним отличал существующую admin-ручку (401) от несуществующей (404).

Факт: срез в Caddy невозможен

admin-API зовут из браузера: 66 вызовов /api/v1/admin/* в 12 файлах фронта (app/scrapers/**, components/scrapers/*, lib/admin-audit-api.ts, GuardedRoute.tsx, Topbar.tsx). Реализован только guard-вариант.

Механизм (rbac.py)

_unauthenticated(path, detail): на _ADMIN_API_RE404 {"detail": "Not Found"} (+ logger.info о зонде), иначе прежний 401. Через него все три анонимные ветки: auth_mode != dual, нет X-Authenticated-User, невалидный X-Internal-Auth-Secret (#2213). Аутентифицированные не тронуты: admin → 200, не-admin → 403 «admin only».

Смоук периметра: пара /trade-in/api/v1/admin/proxies (реальный) и /admin/users (нет) — обе 404; ПТИЦА gendsgn.ru/api/v1/admin/* оставлен 401 (другой бэкенд за basic_auth).

Тесты

test_rbac.py: существующий …_no_header_on_admin_path_returns_401_404 (он фиксировал старое поведение); +2: сравнение ответ-к-ответу существующей vs несуществующей admin-ручки, admin 200 / analyst 403 / /me анониму 401; покрытие всех трёх анонимных веток. Полный suite 5464 passed. Фальсификация (откат rbac.py): существующая → 401 …, несуществующая → 404 …, подделка мимо Caddy → 401 — по значению.

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

scripts/smoke-mera-perimeter.sh (оба admin-пути 404) + живой админ-UI под admin (/scrapers работает). Внешних интеграторов, ходящих в admin-API анонимно, в коде нет.

Closes #3360 [SECURITY] (находка доводки #3352). `/trade-in/api/v1/admin/*` снаружи доходит до guard'а анонимно (Caddy не режет префикс), а после #3352 аноним отличал существующую admin-ручку (401) от несуществующей (404). ## Факт: срез в Caddy невозможен admin-API зовут из браузера: 66 вызовов `/api/v1/admin/*` в 12 файлах фронта (`app/scrapers/**`, `components/scrapers/*`, `lib/admin-audit-api.ts`, `GuardedRoute.tsx`, `Topbar.tsx`). Реализован только guard-вариант. ## Механизм (`rbac.py`) `_unauthenticated(path, detail)`: на `_ADMIN_API_RE` — `404 {"detail": "Not Found"}` (+ `logger.info` о зонде), иначе прежний 401. Через него все три анонимные ветки: `auth_mode != dual`, нет `X-Authenticated-User`, невалидный `X-Internal-Auth-Secret` (#2213). Аутентифицированные не тронуты: admin → 200, не-admin → 403 «admin only». Смоук периметра: пара `/trade-in/api/v1/admin/proxies` (реальный) и `/admin/users` (нет) — обе 404; ПТИЦА `gendsgn.ru/api/v1/admin/*` оставлен 401 (другой бэкенд за basic_auth). ## Тесты `test_rbac.py`: существующий `…_no_header_on_admin_path_returns_401` → `_404` (он фиксировал старое поведение); +2: сравнение ответ-к-ответу существующей vs несуществующей admin-ручки, admin 200 / analyst 403 / `/me` анониму 401; покрытие всех трёх анонимных веток. **Полный suite `5464 passed`.** Фальсификация (откат rbac.py): `существующая → 401 …, несуществующая → 404 …`, `подделка мимо Caddy → 401` — по значению. ## Прод-приёмка `scripts/smoke-mera-perimeter.sh` (оба admin-пути 404) + живой админ-UI под admin (`/scrapers` работает). Внешних интеграторов, ходящих в admin-API анонимно, в коде нет.
bot-backend added 1 commit 2026-09-05 19:52:02 +00:00
fix(rbac): анониму на admin-префиксе отвечать 404, а не 401
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
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 5m6s
990a59e469
#3360. Периметр /trade-in/api/v1/admin/* снаружи не срезан (caddy/sites/apps.caddy:
блок `handle /trade-in/api/*` стоит выше `import caddy/users.caddy.snippet`), и
срезать его нельзя: admin-UI кабинета зовёт эти пути ИЗ БРАУЗЕРА (12 файлов
tradein-mvp/frontend/src — scrapers/**, components/scrapers/**, admin-audit-api.ts,
GuardedRoute). Значит внешний аноним доходит до rbac_guard, а после #3324
(несуществующий путь → 404 роутера) 401 на существующей ручке стал оракулом:
перебором имён восстанавливался список admin-API.

Все три ветки «личность не установлена» (нет X-Authenticated-User, auth_mode=db_only,
подделанный заголовок без #2213-секрета) на admin-префиксе теперь отдают ровно то же,
что роутер даёт на несуществующий путь. Аутентифицированные не тронуты: admin — 200,
не-admin — 403 «admin only» (прятать наличие ручки от опознанного человека незачем).

Смоук периметра: пара admin-путей — существующий /admin/proxies и несуществующий
/admin/users — оба обязаны быть 404 снаружи.
Author
Collaborator

Deep-ревью ⚠️ approve. Остаточный оракул, названный ревью («подделанный X-Authenticated-User с именем не из roles.yaml → 403 vs 404 — жив внутри docker-сети, если секрет пуст»): проверено на проде — TRADEIN_INTERNAL_AUTH_SECRET в контейнере tradein-backend непустой (64 символа), гейт #2213 действует; снаружи заголовок срезает Caddy (apps.caddy:205, :590).

Единственный minor (ветка db_only в тесте зелёная по построению — сверять detail равенством) — в доводке; мерж после неё и зелёного CI.

Deep-ревью ⚠️ approve. Остаточный оракул, названный ревью («подделанный `X-Authenticated-User` с именем не из roles.yaml → 403 vs 404 — жив внутри docker-сети, если секрет пуст»): проверено на проде — `TRADEIN_INTERNAL_AUTH_SECRET` в контейнере tradein-backend **непустой (64 символа)**, гейт #2213 действует; снаружи заголовок срезает Caddy (`apps.caddy:205, :590`). Единственный minor (ветка `db_only` в тесте зелёная по построению — сверять `detail` равенством) — в доводке; мерж после неё и зелёного CI.
Light1YT added 1 commit 2026-09-05 20:04:49 +00:00
test(rbac): сделать db_only-ветку anon-cloak фальсифицируемой
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
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 / frontend-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m1s
d5fb675858
Ассерт `/api/v1/me → 401` проходил в обоих мирах: если monkeypatch
auth_mode тихо не подействует, запрос без заголовка уходит в ветку «нет
заголовка» и даёт тот же 401 (и тот же 404 на admin-пути). Теперь
сверяется detail РАВЕНСТВОМ со строкой db_only-ветки; `in` не годится —
'valid session required' является подстрокой dual-текста.

Мутация (monkeypatch режима закомментирован) даёт красный:
assert 'no authenticated user (valid session required)' == 'valid session required'
Light1YT added 1 commit 2026-09-05 20:16:38 +00:00
Merge remote-tracking branch 'origin/main' into HEAD
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 11s
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 5m10s
6e0a34a683
# Conflicts:
#	scripts/smoke-mera-perimeter.sh
bot-backend merged commit 357c4348ac into main 2026-09-05 20:32:06 +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#3371
No description provided.