Две связанные вещи, обе — по решению владельца продукта.
1. auth/** в paths-фильтры обоих CI (ci.yml, ci-tradein.yml).
auth/roles.yaml — общий RBAC-конфиг двух стеков, но лежит в корне репы и не
попадал НИ В ОДИН фильтр: правка ролей/пользователей не запускала ни backend-,
ни tradein-сьют. Так 2026-07-30 в main уехал красный test_get_role_known_users
(user2 переведён в expired, тест ждал pilot) — обнаружен только вручную и
починен в PR #2587. Теперь правка roles.yaml гоняет оба гейта.
2. «Поиск домов» (/trade-in/sale-share) — ТЕСТОВЫЙ продукт, доступ только у
админа. Раньше он был закрыт от клиентских ролей (employee/manager/pilot), но
оставался открыт внутренней роли analyst. «Только у админа» включает и
внутренние роли → analyst добавлен в deny по sale-share.
Асимметрия с «Кэшем» намеренная и запиннена тестом: Кэш — не продукт, а
диагностика кэшей/скраперов, т.е. ровно тот инструмент, ради которого роль
analyst заведена; ему он оставлен.
Замеры после правки (реальный is_path_allowed поверх roles.yaml):
роль | Поиск домов | Кэш | ядро продукта
admin | True | True | True
analyst | False | True | True
pilot | False | False| True
Тест test_yaml_roles_deliberately_outside_client_deny переписан: пиннит ОБЕ
стороны асимметрии, а не только «analyst видит всё». Набор внутренних путей
разрезан на _SALE_SHARE_PATHS / _CACHE_TOOL_PATHS с assert'ом, что разрез
покрывает исходный набор целиком — иначе новый путь добавят и забудут отнести
к продукту, оставив analyst непроверенным.
Заодно поправлены устаревшие комментарии «Доступ: pilot + admin» в Caddyfile
(vanity-редирект gendsgn.ru/sale-share) и в докстринге самой страницы.
Тесты: 77 passed (tradein rbac/auth_session/auth_api) + 24 passed (site-finder).
tsc --noEmit + next build — зелёные. YAML обоих workflow провалидирован.
Аккаунт 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 тестов краснеют, т.е. не тавтологии.
Аналогично praktika (#1977): user2 pilot→expired в roles.yaml. RouteGuard
уже short-circuit'ит на role=expired → NoAccessScreen variant="trial"
(«Пробный доступ закончился», контакт @ArtemKopylov87). Логин остаётся в
caddy/users.caddy.snippet, чтобы дойти до фронта и увидеть экран. Инфра
expired-роли + trial-экран уже в проде — здесь только смена роли.
praktika pilot→expired role in roles.yaml (paths:[], deny:/**); RouteGuard
short-circuits on role=expired with a dedicated trial-ended screen (not the
generic path-deny path). NoAccessScreen gains variant="trial" with title
«Пробный доступ закончился» and a Telegram link to @ArtemKopylov87. Backend
Role Literal and frontend Role type extended to include "expired". kopylov
(pilot) unaffected.
EPIC18/§19. analyst sees everything (deals, insights, exports, site-finder,
analytics, concept) EXCEPT admin/data-management. Enforcement is backend-hard
(the existing rbac_guard already 403s any non-admin role on /api/v1/admin/*, so
adding the role auto-blocks it) + frontend (deny_paths via /me) + audit.
§19 audit: new best-effort HTTP middleware logs the sensitive actions
(analyze / forecast / forecast-export) to a new audit_log table after the
response. Audit failures never break or delay-fail the request (2-layer
try/except + finally close). Registered INNER to rbac_guard so only authorized
requests are audited (a 403 short-circuits before audit). classify_path matches
export before forecast (anchored).
- auth/roles.yaml: analyst role (paths /**, deny admin-mgmt) + analysttest QA user
- core/auth.py (+ tradein mirror): Role Literal += analyst
- core/audit_middleware.py (new) + main.py registration
- data/sql/144_audit_log.sql (idempotent; auto-applies)
- tests: analyst rbac (403 admin / 200 parcels) + 11 audit cases
No data-level ACL (analyst sees data per policy) -> no #948 dependency. No new
deps. parcels.py untouched. Real analyst logins still need adding to
caddy/users.caddy.snippet (devops).
Refs #962.