fix(tradein/rbac): скрыть «Доля в продаже» и «Кэш» от клиентских аккаунтов #2587

Merged
lekss361 merged 1 commit from fix/tradein-hide-saleshare-cache-from-clients into main 2026-07-31 15:39:57 +00:00
Owner

Проблема

Аккаунт praktika (DB-роль manager) видел в топбаре на /trade-in/team пункты «Доля в продаже» и «Кэш». Это внутренние инструменты — аналитика рынка и состояние кэшей/скраперов, — клиентские аккаунты их видеть не должны.

Замерено на проде до правки (/me для praktika):

allowed_paths: ['/trade-in/**', '/trade-in/api/v1/**', '/api/v1/team/**']
deny_paths   : ['/admin/**', '/api/v1/admin/**', '/trade-in/api/v1/admin/**']
→ ВИДНО: Оценка · Доля в продаже · История · Кэш · Команда

scopePath этих пунктов (/trade-in/sale-share, /trade-in/cache) матчится /trade-in/** и проходит фильтр. Скрыты только те пункты, что намеренно замаплены на admin-путь (скрапперы/аудит/активность).

Почему правка в backend, а не в Topbar

Один и тот же матчер обслуживает три места: пункт меню (Topbar по scopePath из /me), сама страница (RouteGuard) и серверные ручки (rbac_guard). Правка только фронта спрятала бы пункт, оставив прямой URL и API открытыми — ровно тот caveat, что уже задокументирован для скрапперов. Поэтому deny заводится один раз в роли и закрывает всё сразу.

Закрыто для employee/manager (DB_ROLE_PATHS) и для legacy pilot (auth/roles.yaml):

/trade-in/sale-share/**
/trade-in/cache/**
/trade-in/api/v1/buildings/**
/trade-in/api/v1/trade-in/cache-stats/**

У cache-stats глоб, а не точный путь — намеренно: точный паттерн это строгое равенство, и его обходит обычный трейлинг-слэш (…/cache-stats/allowed=True). Спасал бы только Starlette redirect_slashes, т.е. защита висела бы на роутере, а не на RBAC. После правки замерено: /, //, %2f, ./, ../ — везде 403, утечек нет.

Основной продукт не задет

  • app/api/v1/buildings.py обслуживает только sale-share (3 ручки), BuildingListingsDrawer импортируется единственной страницей /sale-share.
  • Секция «ПРОДАЖИ В ДОМЕ» на экране оценки питается estimate-хендлерами (useEstimatePlacementHistory / useSalesVsListings), а не buildings-API.
  • Свип всех 92 путей OpenAPI: под новый deny попали ровно 4 ожидаемых, остальные (estimate, history, quota, team, brand, me, support, lead) не задеты.
  • admin и analyst сознательно вне deny — запиннено тестом, иначе «синхронизация» deny-списков между ролями закрыла бы их молча.

Заодно: починен красный тест на main

backend/tests/test_rbac.py::test_get_role_known_users падал на main — ждал pilot у всех user1..user10, но user2 («Брусника») стал expired 2026-07-30. CI это пропустил: auth/roles.yaml не входит в paths-filter backend/**, поэтому сьют Site Finder не бежал. Тест приведён к реальности с явной константой и объяснением, что он ловит именно рассинхрон roles.yaml.

Проверено

  • pytest tradein (rbac + auth_session + auth_api + team_api + support) → 153 passed
  • pytest site-finder (backend/tests/test_rbac.py) → 24 passed (было 23 passed + 1 failed)
  • Тесты не тавтологии: снятие новых deny-строк роняет 7 тестов (проверено экспериментом, файл восстановлен со сверкой md5)
  • Новое покрытие: e2e через реальный rbac_guard по session-ветке (именно ею ходит praktika — раньше через guard тестировалась только легаси trusted-header ветка), пин deny_paths в выдаче /me, границы глоба (соседи cache-statistics / cache-stats / sale-share-report остаются доступны), regression-guard на core-пути
  • ruff check + format чисто по изменённым файлам (3 RUF059 в test_estimator_pure_units.py — pre-existing)
  • frontend tsc --noEmit + next build — зелёные

Требует внимания

  1. /sale-share — отдельный продукт «Поиск домов» с vanity-URL gendsgn.ru/sale-share и своим <title>. После этого PR он admin-only, и механизма выдать его конкретному клиенту нет — скоуп только ролевой. Если раздел остаётся продаваемым, нужен per-account carve-out. Отметил это прямо в докстринге страницы.
  2. auth/roles.yaml не входит ни в один CI paths-filter — правка этого файла не запускает ни backend-сьют, ни tradein-сьют. Именно так на main уехал красный тест. Фикс (добавить auth/** в фильтры) — изменение правил пайплайна, не делаю автономно; выношу отдельно.
  3. Комментарий в Caddyfile про /sale-share («Доступ: pilot + admin») стал неточным. Не трогал: правка Caddyfile триггерит полный деплой Site Finder с пересозданием caddy — несоразмерно ради комментария, лучше приложить к следующей правке того файла.
## Проблема Аккаунт `praktika` (DB-роль `manager`) видел в топбаре на `/trade-in/team` пункты **«Доля в продаже»** и **«Кэш»**. Это внутренние инструменты — аналитика рынка и состояние кэшей/скраперов, — клиентские аккаунты их видеть не должны. Замерено на проде до правки (`/me` для praktika): ``` allowed_paths: ['/trade-in/**', '/trade-in/api/v1/**', '/api/v1/team/**'] deny_paths : ['/admin/**', '/api/v1/admin/**', '/trade-in/api/v1/admin/**'] → ВИДНО: Оценка · Доля в продаже · История · Кэш · Команда ``` scopePath этих пунктов (`/trade-in/sale-share`, `/trade-in/cache`) матчится `/trade-in/**` и проходит фильтр. Скрыты только те пункты, что намеренно замаплены на admin-путь (скрапперы/аудит/активность). ## Почему правка в backend, а не в Topbar Один и тот же матчер обслуживает **три** места: пункт меню (Topbar по scopePath из `/me`), сама страница (RouteGuard) и серверные ручки (`rbac_guard`). Правка только фронта спрятала бы пункт, оставив прямой URL и API открытыми — ровно тот caveat, что уже задокументирован для скрапперов. Поэтому deny заводится один раз в роли и закрывает всё сразу. Закрыто для `employee`/`manager` (`DB_ROLE_PATHS`) и для legacy `pilot` (`auth/roles.yaml`): ``` /trade-in/sale-share/** /trade-in/cache/** /trade-in/api/v1/buildings/** /trade-in/api/v1/trade-in/cache-stats/** ``` **У cache-stats глоб, а не точный путь** — намеренно: точный паттерн это строгое равенство, и его обходит обычный трейлинг-слэш (`…/cache-stats/` → `allowed=True`). Спасал бы только Starlette `redirect_slashes`, т.е. защита висела бы на роутере, а не на RBAC. После правки замерено: `/`, `//`, `%2f`, `./`, `../` — везде **403**, утечек нет. ## Основной продукт не задет - `app/api/v1/buildings.py` обслуживает **только** sale-share (3 ручки), `BuildingListingsDrawer` импортируется единственной страницей `/sale-share`. - Секция «ПРОДАЖИ В ДОМЕ» на экране оценки питается estimate-хендлерами (`useEstimatePlacementHistory` / `useSalesVsListings`), а не buildings-API. - Свип всех 92 путей OpenAPI: под новый deny попали ровно 4 ожидаемых, остальные (estimate, history, quota, team, brand, me, support, lead) не задеты. - `admin` и `analyst` сознательно вне deny — **запиннено тестом**, иначе «синхронизация» deny-списков между ролями закрыла бы их молча. ## Заодно: починен красный тест на main `backend/tests/test_rbac.py::test_get_role_known_users` падал на main — ждал `pilot` у всех `user1..user10`, но `user2` («Брусника») стал `expired` 2026-07-30. CI это пропустил: `auth/roles.yaml` не входит в paths-filter `backend/**`, поэтому сьют Site Finder не бежал. Тест приведён к реальности с явной константой и объяснением, что он ловит именно рассинхрон roles.yaml. ## Проверено - `pytest` tradein (rbac + auth_session + auth_api + team_api + support) → **153 passed** - `pytest` site-finder (`backend/tests/test_rbac.py`) → **24 passed** (было 23 passed + 1 failed) - Тесты не тавтологии: снятие новых deny-строк роняет **7** тестов (проверено экспериментом, файл восстановлен со сверкой md5) - Новое покрытие: e2e через реальный `rbac_guard` по **session-ветке** (именно ею ходит praktika — раньше через guard тестировалась только легаси trusted-header ветка), пин `deny_paths` в выдаче `/me`, границы глоба (соседи `cache-statistics` / `cache-stats` / `sale-share-report` остаются доступны), regression-guard на core-пути - `ruff check` + `format` чисто по изменённым файлам (3 RUF059 в `test_estimator_pure_units.py` — pre-existing) - frontend `tsc --noEmit` + `next build` — зелёные ## Требует внимания 1. **`/sale-share` — отдельный продукт «Поиск домов»** с vanity-URL `gendsgn.ru/sale-share` и своим `<title>`. После этого PR он admin-only, и **механизма выдать его конкретному клиенту нет** — скоуп только ролевой. Если раздел остаётся продаваемым, нужен per-account carve-out. Отметил это прямо в докстринге страницы. 2. **`auth/roles.yaml` не входит ни в один CI paths-filter** — правка этого файла не запускает ни backend-сьют, ни tradein-сьют. Именно так на main уехал красный тест. Фикс (добавить `auth/**` в фильтры) — изменение правил пайплайна, не делаю автономно; выношу отдельно. 3. Комментарий в `Caddyfile` про `/sale-share` («Доступ: pilot + admin») стал неточным. Не трогал: правка Caddyfile триггерит полный деплой Site Finder с пересозданием caddy — несоразмерно ради комментария, лучше приложить к следующей правке того файла.
lekss361 added 1 commit 2026-07-31 15:20:54 +00:00
fix(tradein/rbac): скрыть «Доля в продаже» и «Кэш» от клиентских аккаунтов
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI / changes (pull_request) Successful in 11s
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 2m19s
CI Trade-In / backend-tests (pull_request) Successful in 3m13s
CI / openapi-codegen-check (pull_request) Successful in 3m34s
CI / backend-tests (pull_request) Successful in 16m7s
abb9398f3f
Аккаунт praktika (DB-роль manager) видел оба пункта в топбаре на /trade-in/team.
Это внутренние инструменты — аналитика рынка и состояние кэшей/скраперов, —
клиентские аккаунты их видеть не должны (решение владельца продукта).

Гейт один — deny-список роли, потому что все три места сверяются с ним через
общий матчер: пункт меню (Topbar по scopePath из /me), страница (RouteGuard) и
серверные ручки (rbac_guard). Правка только фронта спрятала бы пункт, оставив
прямой URL и API открытыми.

Закрыто для employee/manager (DB_ROLE_PATHS) и для legacy pilot (roles.yaml):
  /trade-in/sale-share/**
  /trade-in/cache/**
  /trade-in/api/v1/buildings/**
  /trade-in/api/v1/trade-in/cache-stats/**

У cache-stats ГЛОБ, а не точный путь: точный паттерн — строгое равенство, его
обходит трейлинг-слэш ('…/cache-stats/' → allowed=True), и защита держалась бы
на Starlette redirect_slashes, а не на RBAC. Замерено после правки: все варианты
(слэш, %2f, ./, ../) дают 403, утечек нет.

Основной продукт не задет: buildings.py обслуживает ТОЛЬКО sale-share, секция
«Продажи в доме» на экране оценки питается estimate-хендлерами. admin и analyst
сознательно вне deny — запиннено тестом, иначе «синхронизация» списков закрыла
бы их молча.

Заодно починен КРАСНЫЙ pre-existing тест главного бэкенда:
backend/tests/test_rbac.py::test_get_role_known_users ждал pilot у всех
user1..user10, но user2 («Брусника») стал expired 2026-07-30. CI это пропустил —
auth/roles.yaml не входит в paths-filter backend/**, из-за чего сьют не бежал.

Тесты: 153 passed (tradein) + 24 passed (site-finder, было 23+1 failed).
Новые — e2e через реальный rbac_guard по session-ветке (именно ею ходит
praktika), пин deny_paths в выдаче /me, границы глоба и regression-guard'ы.
Проверены снятием deny: 7 тестов краснеют, т.е. не тавтологии.
lekss361 merged commit 790f69ee01 into main 2026-07-31 15:39:57 +00:00
lekss361 deleted branch fix/tradein-hide-saleshare-cache-from-clients 2026-07-31 15:39:58 +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#2587
No description provided.