test(tradein): прод-конфигурация ролей в приёмке #3316 + компромисс фолбэка
Some checks failed
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Failing after 5m8s

Review PR #3331: приёмка «роли не изменились» гонялась с ПУСТЫМ реестром, а в
проде строка в БД есть у 12 из 13 юзеров и DB-роль ИНАЯ (kopylov: manager при
YAML pilot, user1: employee при YAML pilot). Добавлены два кейса именно этой
конфигурации:

  * YAML pilot + реестр employee → employee, и scope не поехал: allow/deny
    DB_ROLE_PATHS['employee'] сверяются со списками роли pilot из roles.yaml
    целиком — дрейф ЛЮБОГО из двух списков теперь красный тест, а не тихо
    потерянный/выданный раздел в проде;
  * YAML pilot + реестр manager → manager, и лишних путей на tradein-периметре
    нет: manager отличается от employee ровно префиксом /api/v1/team/** (вне
    /trade-in/**), deny-списки совпадают.

Докстринг `_registry_role`: зафиксирован компромисс — при недоступном реестре
фолбэк временно возвращает авторитетность roles.yaml, то есть состояние, которое
фикс и лечит. Сегодня безопасно (прод-коллизий имён нет, новые закрыты
409-гвардом create_employee); появится коллизия — ветку менять на fail-closed.
This commit is contained in:
bot-backend 2026-09-02 14:59:14 +05:00
parent 4feb61c006
commit cde95aa9f6
2 changed files with 55 additions and 0 deletions

View file

@ -180,6 +180,15 @@ def _registry_role(username: str) -> str | None:
пустая, либо реестр вообще недоступен. Во всех трёх случаях решение
остаётся за roles.yaml падение БД не имеет права выключить legacy-вход.
Осознанный компромисс (#3316 review): последняя ветка — недоступный
реестр на время сбоя ВОЗВРАЩАЕТ авторитетность roles.yaml, то есть ровно
то состояние, которое этот фикс и лечит. Сегодня это безопасно: коллизий
имён между реестром и YAML на проде нет, а новые закрыты 409-гвардом в
`team.create_employee`. Если коллизия всё же появится (ручной INSERT в
реестр, расширение roles.yaml) сбой БД станет окном эскалации, и тогда
эту ветку надо менять на fail-closed (отказ вместо YAML-роли), а не
дописывать проверки у вызывающих.
Имя таблицы берётся из фиксированного словаря `identity_schema()`, значение
едет bind-параметром: снаружи в SQL не попадает ничего.
"""

View file

@ -129,6 +129,52 @@ def test_legacy_yaml_users_keep_their_roles(registry: Callable[..., Any]) -> Non
assert {username: auth.get_role(username) for username in users} == users
def _yaml_pilot() -> str:
for username, role in _yaml_users().items():
if role == "pilot":
return username
pytest.skip("в auth/roles.yaml нет ни одного pilot — тест неприменим")
def test_prod_config_pilot_in_yaml_employee_in_registry(registry: Callable[..., Any]) -> None:
"""Прод-конфигурация 12 из 13 аккаунтов: строка в реестре ЕСТЬ и роль там иная.
Реестр главнее (`employee`), а объём прав от этого не меняется: scope
DB-роли `employee` обязан совпадать с вчерашним YAML-scope роли `pilot`.
Списки сверяются целиком дрейф ЛЮБОГО из двух ловится здесь, а не
тихой потерей/выдачей раздела в проде.
"""
from app.services.auth_session import DB_ROLE_PATHS
registry("employee")
assert auth.get_role(_yaml_pilot()) == "employee"
pilot = auth._load_roles_config()["roles"]["pilot"]
allow, deny = DB_ROLE_PATHS["employee"]
assert sorted(allow) == sorted(pilot["paths"])
assert sorted(deny) == sorted(pilot["deny"] or [])
def test_prod_config_pilot_in_yaml_manager_in_registry(registry: Callable[..., Any]) -> None:
"""Конфигурация kopylov: YAML pilot + реестр manager → manager.
Лишних путей на tradein-периметре это не даёт: manager отличается от
employee ровно одним префиксом `/api/v1/team/**` (дашборд «Команды»,
ВНЕ `/trade-in/**`), а deny-списки совпадают.
"""
from app.services.auth_session import DB_ROLE_PATHS
registry("manager")
assert auth.get_role(_yaml_pilot()) == "manager"
emp_allow, emp_deny = DB_ROLE_PATHS["employee"]
mgr_allow, mgr_deny = DB_ROLE_PATHS["manager"]
extra = set(mgr_allow) - set(emp_allow)
assert extra == {"/api/v1/team/**"}
assert not any(p.startswith("/trade-in") for p in extra)
assert sorted(mgr_deny) == sorted(emp_deny)
def test_registry_failure_falls_back_to_yaml(registry: Callable[..., Any]) -> None:
"""Реестр недоступен → legacy-вход продолжает работать по YAML."""
registry(_MISSING)