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
Аккаунт 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 тестов краснеют, т.е. не тавтологии.
128 lines
7.8 KiB
YAML
128 lines
7.8 KiB
YAML
# 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. '<prefix>/**' → '^<prefix>(?:/.*)?$': сам путь + слэш + подпути,
|
||
# но НЕ соседи по префиксу ('…/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/** страниц.
|
||
# NB: клиентский deny 2026-07-31 («Доля в продаже» / «Кэш», см. pilot выше)
|
||
# на analyst СОЗНАТЕЛЬНО не распространён — analyst внутренняя роль и оба
|
||
# раздела для неё рабочий инструмент. Это не забытая дыра.
|
||
paths:
|
||
- "/**"
|
||
deny:
|
||
- "/admin/**"
|
||
- "/api/v1/admin/**"
|
||
- "/trade-in/api/v1/admin/**"
|
||
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.
|
||
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-фикстурой
|
||
# после восстановления доступа)
|