chore(ci+rbac): auth/** в paths-фильтры + «Поиск домов» только у админа #2592
6 changed files with 94 additions and 21 deletions
|
|
@ -43,6 +43,13 @@ jobs:
|
|||
# [tool.uv.workspace] меняют реальные зависимости → гейт обязан бежать.
|
||||
- 'tradein-mvp/uv.lock'
|
||||
- 'tradein-mvp/pyproject.toml'
|
||||
# auth/roles.yaml — общий RBAC-конфиг обоих стеков, лежит В КОРНЕ
|
||||
# репы и монтируется в tradein-backend (/app/auth/roles.yaml).
|
||||
# tests/test_rbac.py читает именно его, поэтому правка ролей обязана
|
||||
# гонять и этот гейт. Без строки правка roles.yaml не запускала НИ
|
||||
# ОДИН сьют (та же дыра закрыта симметрично в ci.yml) — так на main
|
||||
# уехал красный test_get_role_known_users (2026-07-30 → PR #2587).
|
||||
- 'auth/**'
|
||||
- '.forgejo/workflows/ci-tradein.yml'
|
||||
frontend:
|
||||
- 'tradein-mvp/frontend/**'
|
||||
|
|
|
|||
|
|
@ -52,6 +52,14 @@ jobs:
|
|||
backend:
|
||||
- 'backend/**'
|
||||
- 'data/sql/**'
|
||||
# auth/roles.yaml — общий RBAC-конфиг ОБОИХ стеков (bind-mount в
|
||||
# backend и в tradein-backend). Правка ролей/пользователей меняет
|
||||
# поведение backend/tests/test_rbac.py, но сам файл лежит вне
|
||||
# 'backend/**' → без этой строки сьют no-op'ился, и правка уезжала
|
||||
# в main без единого прогона. Так и случилось 2026-07-30: user2
|
||||
# переведён в expired, test_get_role_known_users стал красным и
|
||||
# доехал до main незамеченным (починен в PR #2587).
|
||||
- 'auth/**'
|
||||
- '.forgejo/workflows/ci.yml'
|
||||
frontend:
|
||||
- 'frontend/**'
|
||||
|
|
|
|||
17
Caddyfile
17
Caddyfile
|
|
@ -139,12 +139,19 @@ gendsgn.ru {
|
|||
# /trade-in/sale-share, для которого теперь нет Caddy basic_auth (как и
|
||||
# для остального /trade-in). Это НЕ делает страницу публичной: она всё
|
||||
# ещё за собственной авторизацией trade-in — `RouteGuard` во фронте
|
||||
# (`app/layout.tsx`, «Доступ: pilot + admin») и сессия для
|
||||
# `/api/v1/buildings/sale-share*` на бэке; без валидной сессии юзер
|
||||
# получит редирект на /login, а не контент. Смысл переноса — не открыть
|
||||
# страницу всем, а убрать несогласованность: короткий URL не должен быть
|
||||
# строже (Caddy basic_auth) целевого адреса, к которому и так уже нет
|
||||
# (`app/layout.tsx`) и сессия для `/api/v1/buildings/sale-share*` на
|
||||
# бэке; без валидной сессии юзер получит редирект на /login, а не
|
||||
# контент. Смысл переноса — не открыть страницу всем, а убрать
|
||||
# несогласованность: короткий URL не должен быть строже (Caddy
|
||||
# basic_auth) целевого адреса, к которому и так уже нет
|
||||
# basic_auth-барьера (только собственный login trade-in).
|
||||
#
|
||||
# ОБНОВЛЕНО 2026-07-31: доступ к разделу сузился с «pilot + admin» до
|
||||
# ТОЛЬКО admin — «Поиск домов» признан тестовым продуктом, клиентам не
|
||||
# показывается (deny в auth/roles.yaml для pilot и analyst + в
|
||||
# DB_ROLE_PATHS для employee/manager). Сам редирект не трогаем: он ведёт
|
||||
# на страницу, а гейт стоит на роли — для всех, кроме admin, короткий
|
||||
# адрес приведёт на NoAccessScreen.
|
||||
@saleshare path /sale-share /sale-share/
|
||||
handle @saleshare {
|
||||
redir /trade-in/sale-share permanent
|
||||
|
|
|
|||
|
|
@ -81,15 +81,28 @@ roles:
|
|||
# для любого role != "admin" → analyst авто-403 на admin-API без доп. кода.
|
||||
# deny ниже драйвит фронтовый RouteGuard (deny_paths из /me) для UI-gating
|
||||
# /admin/** страниц.
|
||||
# NB: клиентский deny 2026-07-31 («Доля в продаже» / «Кэш», см. pilot выше)
|
||||
# на analyst СОЗНАТЕЛЬНО не распространён — analyst внутренняя роль и оба
|
||||
# раздела для неё рабочий инструмент. Это не забытая дыра.
|
||||
# Клиентский 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), чтобы дойти до фронта и увидеть
|
||||
|
|
|
|||
|
|
@ -395,6 +395,27 @@ _INTERNAL_TOOL_PATHS = (
|
|||
"/trade-in/api/v1/trade-in/cache-stats/",
|
||||
)
|
||||
|
||||
# Разрез тех же путей по ПРОДУКТАМ — нужен для ролей, у которых доступ
|
||||
# асимметричен (см. test_yaml_roles_deliberately_outside_client_deny).
|
||||
# «Поиск домов» — тестовый продукт, admin-only (решение владельца 2026-07-31).
|
||||
_SALE_SHARE_PATHS = (
|
||||
"/trade-in/sale-share",
|
||||
"/trade-in/api/v1/buildings/sale-share",
|
||||
"/trade-in/api/v1/buildings/sale-share/summary",
|
||||
"/trade-in/api/v1/buildings/123/listings",
|
||||
)
|
||||
# «Кэш» — не продукт, а диагностика состояния кэшей/скраперов.
|
||||
_CACHE_TOOL_PATHS = (
|
||||
"/trade-in/cache",
|
||||
"/trade-in/api/v1/trade-in/cache-stats",
|
||||
"/trade-in/api/v1/trade-in/cache-stats/",
|
||||
)
|
||||
|
||||
# Разрез обязан покрывать исходный набор целиком — иначе новый внутренний путь
|
||||
# добавят в _INTERNAL_TOOL_PATHS, забудут отнести к продукту, и роль analyst
|
||||
# останется непроверенной на нём.
|
||||
assert set(_SALE_SHARE_PATHS) | set(_CACHE_TOOL_PATHS) == set(_INTERNAL_TOOL_PATHS)
|
||||
|
||||
# Основной продукт — не должен быть задет deny выше.
|
||||
_CORE_PRODUCT_PATHS = ("/trade-in/", "/trade-in/api/v1/trade-in/estimate")
|
||||
|
||||
|
|
@ -422,14 +443,29 @@ def test_yaml_roles_deliberately_outside_client_deny() -> None:
|
|||
ровно тот класс рассинхрона, что уже случился с user2 (см.
|
||||
backend/tests/test_rbac.py::test_get_role_known_users).
|
||||
|
||||
`analyst` — внутренняя роль (paths "/**", deny только admin-управление);
|
||||
решение не распространять на неё клиентский deny осознанное, а не забытое.
|
||||
`analyst` (внутренняя роль, paths "/**") попадает под клиентский deny
|
||||
ЧАСТИЧНО, и обе стороны асимметрии здесь запиннены намеренно:
|
||||
- «Поиск домов» ЗАКРЫТ — тестовый продукт, доступ только у admin
|
||||
(решение владельца 2026-07-31; «только у админа» включает и внутренние
|
||||
роли, поэтому analyst тоже в deny);
|
||||
- «Кэш» ОТКРЫТ — это не продукт, а диагностика кэшей/скраперов, ровно тот
|
||||
инструмент, ради которого роль analyst и заведена.
|
||||
Если решение поменяется — упадёт этот тест, а не пользователь на проде.
|
||||
"""
|
||||
for path in _INTERNAL_TOOL_PATHS:
|
||||
assert auth_mod.is_path_allowed("admin", path), f"admin lost access to {path}"
|
||||
|
||||
for path in _SALE_SHARE_PATHS:
|
||||
assert not auth_mod.is_path_allowed("analyst", path), (
|
||||
f"analyst не должен видеть «Поиск домов» ({path}) — тестовый продукт, "
|
||||
f"admin-only; если решение изменилось, обнови тест И комментарий у роли "
|
||||
f"analyst в auth/roles.yaml"
|
||||
)
|
||||
|
||||
for path in _CACHE_TOOL_PATHS:
|
||||
assert auth_mod.is_path_allowed("analyst", path), (
|
||||
f"analyst lost access to {path} — если это намеренно, обнови этот тест "
|
||||
f"и комментарий у роли analyst в auth/roles.yaml"
|
||||
f"analyst потерял «Кэш» ({path}) — это его рабочий инструмент; если "
|
||||
f"закрыли намеренно, обнови тест И комментарий у роли analyst"
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,16 +5,18 @@
|
|||
* Порог % → дома вторички, где доля квартир, выставленных на продажу, ≥ порога.
|
||||
* Сигнал для девелопера: расселение / инвест-выход / проблемный дом.
|
||||
*
|
||||
* Доступ: ТОЛЬКО admin (с 2026-07-31). Раздел признан внутренним инструментом —
|
||||
* клиентские аккаунты его не видят: явный deny `/trade-in/sale-share/**` +
|
||||
* `/trade-in/api/v1/buildings/**` заведён для DB-ролей employee/manager
|
||||
* (`app/services/auth_session.py: DB_ROLE_PATHS`) и для legacy `pilot`
|
||||
* (`auth/roles.yaml`). Роль `analyst` сознательно не в deny — внутренняя.
|
||||
* Доступ: ТОЛЬКО admin (с 2026-07-31). Это ТЕСТОВЫЙ продукт — решение владельца
|
||||
* продукта: клиентам не показывать, доступ только у админа. Явный deny
|
||||
* `/trade-in/sale-share/**` + `/trade-in/api/v1/buildings/**` заведён для
|
||||
* DB-ролей employee/manager (`app/services/auth_session.py: DB_ROLE_PATHS`),
|
||||
* для legacy `pilot` И для внутренней `analyst` (`auth/roles.yaml`) — «только у
|
||||
* админа» включает и внутренние роли. Ср. соседний «Кэш»: он закрыт от клиентов,
|
||||
* но analyst'у оставлен, т.к. это диагностика, а не продукт.
|
||||
*
|
||||
* NB: короткий адрес `gendsgn.ru/sale-share` (301 → сюда, см. Caddyfile) после
|
||||
* этого ведёт на NoAccessScreen для всех, кроме admin. Если раздел снова станет
|
||||
* продаваемым продуктом, одним снятием deny не обойтись: нужен per-account
|
||||
* carve-out — сейчас скоуп только ролевой, выдать его отдельному клиенту нечем.
|
||||
* NB: короткий адрес `gendsgn.ru/sale-share` (301 → сюда, см. Caddyfile) ведёт
|
||||
* на NoAccessScreen для всех, кроме admin. Если раздел когда-нибудь станет
|
||||
* продаваемым, одним снятием deny не обойтись: нужен per-account carve-out —
|
||||
* скоуп сейчас только ролевой, выдать раздел отдельному клиенту нечем.
|
||||
*/
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue