gendesign/auth/roles.yaml
bot-backend 762d1f68c6
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Successful in 1m56s
CI / frontend-tests (pull_request) Successful in 2m14s
CI Trade-In / backend-tests (pull_request) Successful in 3m1s
CI / openapi-codegen-check (pull_request) Successful in 2m15s
CI / backend-tests (pull_request) Successful in 15m48s
chore(ci+rbac): auth/** в paths-фильтры + «Поиск домов» только у админа
Две связанные вещи, обе — по решению владельца продукта.

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 провалидирован.
2026-07-31 20:24:24 +03:00

141 lines
8.9 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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/** страниц.
# Клиентский 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.
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-фикстурой
# после восстановления доступа)