# RBAC roles + user → role mapping. # # Single source of truth for both main backend (backend/app/core/auth.py) and # tradein backend (tradein-mvp/backend/app/core/auth.py). Mounted into both # containers at /app/auth/roles.yaml via docker-compose bind-mount. # # Glob semantics (fnmatch-based, see app.core.auth.is_path_allowed): # - "/**" → matches any path (admin scope). # - "/foo/**" → matches /foo, /foo/, /foo/bar, /foo/bar/baz/... # - "/foo" → matches exactly /foo. # # A path is allowed iff (1) it matches one of `paths` AND (2) it does NOT match # any pattern in `deny`. `deny` overrides `paths` (deny-by-default within # match scope). # # Username list MUST match exactly the entries in caddy/users.caddy.snippet # — Caddy basic_auth sets X-Authenticated-User from {http.auth.user.id}, and # unknown users hit /me with 403 ("user not in roles config"). # # Last updated: 2026-05-26 (pilot scope narrowed to /trade-in/** only — decision # 2026-05-26: pilot аккаунты пилот-программы видят ТОЛЬКО раздел Trade-In; # Analytics / Site Finder / Concept / landing — admin-only). roles: admin: paths: - "/**" deny: [] pilot: # Pilot имеет доступ ТОЛЬКО к разделу Trade-In (оценка вторички). # Landing (/), analytics, site-finder, concept, остальной /api/v1/* — закрыты. # Backend middleware всё равно пропускает known users на все non-admin paths # (path-level enforcement делает frontend RouteGuard через `allowed_paths`), # но для UX/security принципов keep deny список explicit. paths: - "/trade-in/**" - "/trade-in/api/v1/**" deny: - "/admin/**" - "/api/v1/admin/**" - "/trade-in/api/v1/admin/**" # Внутренние разделы, закрытые от клиентских аккаунтов (решение владельца # продукта 2026-07-31): «Доля в продаже» — аналитика рынка, «Кэш» — # состояние кэшей/скраперов. Зеркало deny-списка DB-ролей employee/manager # (tradein-mvp/backend/app/services/auth_session.py: DB_ROLE_PATHS). # # Зачем копия здесь, если клиенты ходят session-cookie'ой: снаружи легаси # trusted-header ветка НЕДОСТИЖИМА — с #2558 Caddy срезает входящий # X-Authenticated-User на всём /trade-in/* (`header_up # -X-Authenticated-User` в handle /trade-in/api/* и в @tradein), так что # ни один клиентский аккаунт по ней не ходит. Паттерны нужны для другого: # 1) ВНУТРИСЕТЕВОЙ dual-mode трафик — запросы изнутри gendesign_shared с # валидным X-Internal-Auth-Secret; ими ходят QA-смоуки вида # `docker exec tradein-backend curl localhost:8000 # -H 'X-Authenticated-User: ...'` — они резолвятся именно через # roles.yaml, и без этих строк смоук показал бы 200 там, где # реальный клиент получает 403; # 2) чтобы legacy-pilot не расходился с DB-employee, если dual-режим # когда-нибудь снова окажется на периметре (откат #2558 / новый # фронт-прокси) — тогда расхождение молча откроет разделы. # НЕ удалять как «мёртвые»: они мёртвые только пока Caddy режет заголовок. # # Страницы + их API вместе: deny гейтит пункт меню (Topbar через /me), # саму страницу (RouteGuard) и серверные ручки (rbac_guard). # # cache-stats закрыт ГЛОБОМ, а не точным путём, намеренно: точный паттерн # обходится трейлинг-слэшем ('…/cache-stats/' не равен '…/cache-stats' → # allowed), и защита повисала бы на Starlette redirect_slashes, а не на # RBAC. '/**' → '^(?:/.*)?$': сам путь + слэш + подпути, # но НЕ соседи по префиксу ('…/cache-statistics' не матчится). - "/trade-in/sale-share/**" - "/trade-in/cache/**" - "/trade-in/api/v1/buildings/**" - "/trade-in/api/v1/trade-in/cache-stats/**" analyst: # #962 (EPIC18, ТЗ §19): analyst видит ВСЁ (deals, insights, exports, # site-finder, analytics, concept) КРОМЕ admin/data-management. # paths "/**" = доступ ко всему; deny = админ-управление (scraper-триггеры, # sync-таски, data-management, user/role management). # Backend hard-gate: app/main.py rbac_guard уже блокирует /api/v1/admin/* # для любого role != "admin" → analyst авто-403 на admin-API без доп. кода. # deny ниже драйвит фронтовый RouteGuard (deny_paths из /me) для UI-gating # /admin/** страниц. # Клиентский deny 2026-07-31 (см. pilot выше) распространён на analyst # ЧАСТИЧНО — асимметрия намеренная, не недосмотр: # «Поиск домов» (/trade-in/sale-share + /api/v1/buildings/**) — ЗАКРЫТ. # Решение владельца продукта 2026-07-31: это ТЕСТОВЫЙ продукт, доступ # только у admin. «Только у админа» = включая внутренние роли, поэтому # analyst тоже в deny. # «Кэш» (/trade-in/cache + cache-stats) — ОСТАВЛЕН открытым: это не # продукт, а диагностика состояния кэшей/скраперов, т.е. ровно тот # рабочий инструмент, ради которого роль analyst и заведена # («видит ВСЁ кроме admin-управления», см. выше). # Обе стороны этой асимметрии запиннены тестом # tradein-mvp/backend/tests/test_rbac.py::test_yaml_roles_deliberately_outside_client_deny # — если решение поменяется, тест упадёт и заставит обновить и его, и этот # комментарий, а не тихо разойтись с реальностью. paths: - "/**" deny: - "/admin/**" - "/api/v1/admin/**" - "/trade-in/api/v1/admin/**" - "/trade-in/sale-share/**" - "/trade-in/api/v1/buildings/**" expired: # Пробный доступ закончился — нет доступа ни к чему. Аккаунт остаётся в # caddy/users.caddy.snippet (basic_auth), чтобы дойти до фронта и увидеть # сообщение; /me отдаёт role=expired → RouteGuard рендерит trial-экран. paths: [] deny: - "/**" # NB: реальные analyst-логины ДОЛЖНЫ быть добавлены и в # caddy/users.caddy.snippet (Caddy basic_auth) силами devops — иначе Caddy не # пропустит юзера и не выставит X-Authenticated-User. Здесь только role-mapping; # Caddy НЕ редактируем из этого таска. users: admin: admin kopylov: pilot user1: pilot user2: expired # «Брусника» — доступ закрыт 2026-07-30 (решение владельца продукта; # ранее: восстановлен 2026-07-13, trial-expire 2026-07-09) user3: pilot user4: pilot user5: pilot user6: pilot user7: pilot user8: pilot user9: pilot user10: pilot praktika: pilot # ГК «Практика» — доступ восстановлен 2026-07-27 (решение владельца # продукта; ранее expired с 2026-06-27). Безлимитная квота оценок # выдана через account_quota_overrides.unlimited (migration 191), # не через код — см. app.services.account_quota.is_unlimited. buyer1: pilot # Тестовый доступ потенциального покупателя — заведён 2026-09-02 по # просьбе владельца. Квота 50 оценок/мес через # account_quota_overrides.monthly_limit (не unlimited). DB-роль # manager (как praktika/kopylov — самостоятельный внешний аккаунт, # не employee под чьим-то manager_id). admintest: admin # temp QA 2026-05-26 pilottest: pilot # temp QA 2026-05-26 analysttest: analyst # temp QA 2026-06-07 (#962) expiredtest: expired # temp QA 2026-07-27 — role=expired regression coverage для # test_rbac.py (praktika перестал быть expired-фикстурой # после восстановления доступа)