fix(tradein/rbac): энфорсить roles.yaml scope на всех non-admin путях (R2 H3 security) #2498

Merged
bot-backend merged 1 commit from fix/tradein-rbac-scope into main 2026-07-12 19:28:39 +00:00
Collaborator

R2 HIGH #3 (security) — RBAC scope не энфорсился

rbac_guard (main.py) гейтил только /api/v1/admin/*. is_path_allowed (roles.yaml paths/deny) существовал, но имел 0 вызовов. → revoked аккаунт (role=expired, roles.yaml paths:[] deny:["/**"] — напр. user2 «Брусника» trial отозван 09.07, praktika) сохранял полный non-admin API-доступ: POST /api/v1/search (экспорт листингов), estimate-quota и т.д.

Fix

После admin-гейта энфорсим is_path_allowed(role, external_path) для всех non-bootstrap путей.

  • Внешний путь: roles.yaml globs внешние (/trade-in/api/v1/**), а Caddy срезает /trade-in перед backend → восстанавливаем _EXTERNAL_PREFIX + path.
  • Bootstrap-исключение /me + /brand/*: expired ДОЛЖЕН получить role=expired через /me (→ trial-экран) и брендинг; иначе paths:[] → 403 на /me сломал бы trial-UX. Проверено: frontend бутстрапит именно useMe+useBrand.
  • fail-open на сбой парса + громкий лог — не лочим платящего pilot из-за конфиг-бага (admin-гейт остаётся).

roles.yaml НЕ менял — только энфорс уже задекларированной политики. pilot/admin/analyst доступ сохранён.

Тесты

+5 scope-кейсов: expired → /search,/trade-in/* = 403; expired → /me,/brand/* = 200 (trial-экран жив); pilot/admin → trade-in/search = 200; analyst → search 200 / admin 403. 31 passed, ruff clean. Тест-harness middleware (mirror main.py) синхронизирован.

⚠️ Post-merge: живьём проверю, что pilot оценивает как раньше и expired видит trial-экран.

## R2 HIGH #3 (security) — RBAC scope не энфорсился `rbac_guard` (main.py) гейтил **только** `/api/v1/admin/*`. `is_path_allowed` (roles.yaml paths/deny) существовал, но имел **0 вызовов**. → revoked аккаунт (role=expired, roles.yaml `paths:[] deny:["/**"]` — напр. user2 «Брусника» trial отозван 09.07, praktika) сохранял **полный non-admin API-доступ**: `POST /api/v1/search` (экспорт листингов), estimate-quota и т.д. ## Fix После admin-гейта энфорсим `is_path_allowed(role, external_path)` для всех non-bootstrap путей. - **Внешний путь:** roles.yaml globs внешние (`/trade-in/api/v1/**`), а Caddy срезает `/trade-in` перед backend → восстанавливаем `_EXTERNAL_PREFIX + path`. - **Bootstrap-исключение** `/me` + `/brand/*`: expired ДОЛЖЕН получить `role=expired` через `/me` (→ trial-экран) и брендинг; иначе `paths:[]` → 403 на `/me` сломал бы trial-UX. Проверено: frontend бутстрапит именно `useMe`+`useBrand`. - **fail-open** на сбой парса + громкий лог — не лочим платящего pilot из-за конфиг-бага (admin-гейт остаётся). **roles.yaml НЕ менял** — только энфорс уже задекларированной политики. pilot/admin/analyst доступ сохранён. ## Тесты +5 scope-кейсов: expired → `/search`,`/trade-in/*` = **403**; expired → `/me`,`/brand/*` = **200** (trial-экран жив); pilot/admin → trade-in/search = 200; analyst → search 200 / admin 403. **31 passed**, ruff clean. Тест-harness middleware (mirror main.py) синхронизирован. ⚠️ Post-merge: живьём проверю, что pilot оценивает как раньше и expired видит trial-экран.
bot-backend added 1 commit 2026-07-12 19:26:40 +00:00
fix(tradein/rbac): энфорсить roles.yaml scope на всех non-admin путях (R2 H3)
All checks were successful
CI / changes (pull_request) Successful in 8s
CI Trade-In / changes (pull_request) Successful in 8s
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 52s
85321d1d1a
rbac_guard гейтил только /api/v1/admin/* → revoked (role=expired, roles.yaml
paths:[] deny:/**) и узко-скоупленные аккаунты сохраняли полный non-admin
API-доступ (напр. POST /api/v1/search — экспорт листингов + estimate-quota).
is_path_allowed (roles.yaml paths/deny) существовал, но НЕ вызывался (0 callers).

Fix: после admin-гейта вызываем is_path_allowed(role, external_path) для всех
non-bootstrap путей. roles.yaml globs — внешние (Caddy срезал /trade-in), поэтому
восстанавливаем внешний путь (_EXTERNAL_PREFIX + path). Bootstrap-пути /me и
/brand/* исключены — expired ДОЛЖЕН получить role=expired через /me (trial-экран)
и брендинг; без исключения trial-UX сломался бы (expired paths:[] → 403 на /me).
На сбой парса — fail-open + громкий лог (не лочим платящего pilot из-за конфиг-бага).

roles.yaml НЕ меняю — только энфорсю уже задекларированную политику. pilot/admin/
analyst доступ сохранён (verified), expired теперь 403 на non-admin API.
Тесты: +5 scope-кейсов (expired denied search/trade-in, allowed me/brand; pilot/
admin/analyst preserved) — 31 passed. Тест-harness middleware — mirror, обновлён.
bot-backend merged commit 519b482cdd into main 2026-07-12 19:28:39 +00:00
bot-backend deleted branch fix/tradein-rbac-scope 2026-07-12 19:28:39 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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#2498
No description provided.