fix(tradein/auth): единый источник ролей — БД first, YAML только legacy-fallback (закрывает эскалацию до admin и 403 своим) #3331
2 changed files with 55 additions and 0 deletions
|
|
@ -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 не попадает ничего.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue