fix(tradein/auth): единый источник ролей — БД first, YAML только legacy-fallback (закрывает эскалацию до admin и 403 своим) #3331
No reviewers
Labels
No labels
Fable 5 ревью
GG-форсайт
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
feedback/max
generative
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
ИРД
вторичка
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#3331
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/3316-role-single-source"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #3316 [SECURITY][P1]. Аудит 01-02.09, линза payments-auth. Двусторонний дефект одного корня: роль жила в roles.yaml, пользователи — в БД, никто не сверял.
Резолвер (
app/core/auth.py::get_role)Один
SELECT role FROM tradein_users WHERE username=:username(имя таблицы из фиксированного словаря, значение — bind): непустая роль из БД — ответ; строки нет / роль пуста / реестр недоступен — прежний путь по roles.yaml, KeyError если нет и там. Вызывающие не тронуты — все уже ходят через эту функцию (rbac,_assert_estimate_access, /history, team, account_quota).Закрывает обе стороны:
admintestиз YAML, резолвится в employee из БД, не в admin из YAML; плюс defense-in-depth —create_employeeотказывает на имена, числящиеся в YAML с ролью ≠ employee;Побочные правки, без которых поведение бы поехало
rbac_guard/get_user_scope: роль из реестра →DB_ROLE_PATHS-матчер (иначе 403 на всё);_batch_quota_status: роли из уже прочитанных строк (иначе N+1 — закреплено существующим тестом);unlimitedосталось за YAML в обоих местах — иначе override начал бы работать там, где раньше молча игнорировался.Почему 13 прод-юзеров не сменят роль
tradein_users.role— NOT NULL CHECK (миграция 192), пустой роли не бывает; строка есть только у заведённых через team-API/сид 193. Чисто-legacy YAML-логины идут прежней веткой (тест сверяет ВЕСЬ маппинг roles.yaml). У praktika DB-роль manager — она и до фикса на реальном периметре шла из БД (сессионная ветка; legacy trusted-header снаружи срезан Caddy с #2558). Прод-SELECT перед мержем — отдельным комментарием ниже.Тесты
Новый
test_role_single_source.py(5), релевантные прогоны:247 passed+255 passed, 3 skipped. Фальсификация (снятие 3 строк DB-first): 2 красных ПО ЗНАЧЕНИЮ —assert 'admin' == 'employee'(эскалация) иHTTPException: 403: user not in roles configна СВОЕЙ оценке.Прод-приёмка после деплоя
create_employeeс именемadmin→ отказ;Роль жила в двух местах сразу: люди заводятся в БД (`tradein_users.role`), а `get_role` читал ТОЛЬКО `auth/roles.yaml` — и никто эти два источника не сверял. Дефект двусторонний: * вверх: менеджер заводил сотрудника с именем, которое уже числится в roles.yaml админом (проверялись лишь regex и уникальность в БД) — на входе тот получал admin из YAML, то есть чтение ЛЮБОЙ чужой оценки (admin проходит мимо ownership-check в trade_in.py) и безлимитную квоту; * вниз: сотрудник, которого в roles.yaml нет, ловил KeyError → 403 на СОБСТВЕННУЮ оценку. Источник теперь один и лечится один раз — в `app.core.auth.get_role`: реестр (`tradein_users.role` / `auth.users.role`) спрашивается первым, roles.yaml остаётся fallback для legacy-юзеров, у которых строки в реестре нет. Реестр недоступен → тоже fallback: падение БД не выключает legacy-вход. Вызывающие (rbac, trade_in, team, account_quota) не менялись. Сопутствующее, чтобы поведение существующих аккаунтов не поехало: * rbac_guard выбирает матчер путей по РОДУ роли (роль реестра → DB_ROLE_PATHS), иначе employee/manager на legacy-пути получил бы 403 на всё; * get_user_scope отдаёт scope роли реестра из того же DB_ROLE_PATHS; * право на персональный `unlimited` осталось за roles.yaml (account_quota + _batch_quota_status) — фикс убирает эскалацию, а не раздаёт новую; * `_batch_quota_status` берёт роли из уже прочитанных строк — иначе список «Команды» снова стал бы N+1. Defense-in-depth: create_employee отдаёт 409 на username, за которым в roles.yaml числится не-employee роль.