fix(tradein/team): admin управляет менеджерами — сброс пароля и блокировка (#2556) #2575

Merged
lekss361 merged 1 commit from fix/tradein-team-admin-manages-managers into main 2026-07-31 13:08:14 +00:00
Owner

Проблема (инцидент 2026-07-31)

После cutover'а на DB-auth (#2558) аккаунты kopylov и praktika сидят с role='manager', а team-API жёстко фильтровал role='employee' (_fetch_employee_row, _LIST_EMPLOYEES_*_SQL). Следствие: сбросить менеджеру пароль или заблокировать его через UI было нечем — только ручной psql на проде.

Всплыло сегодня: «Практика» весь день билась в форму входа (5 login_failed с трёх разных IP, ни одного успеха), при этом в поле «Логин» вводили старый basic_auth-пароль. Восстановить им доступ админ из интерфейса не мог.

Сам инцидент с доступом разрешён отдельно (пароли из волта оказались валидны, сверены с прод-хешами) — этот PR закрывает то, что сделало ситуацию неразрешимой из UI.

Что меняется

  • _fetch_employee_row принимает actor: adminrole IN ('employee','manager'), manager → как было, только role='employee' + свои по manager_id. Два статических SQL-варианта, без динамической сборки WHERE (конвенция файла).
  • GET /employees без фильтра отдаёт admin'у и менеджеров. ?manager_id= не меняется — только сотрудники указанного менеджера.
  • EmployeeOut.role ("employee" | "manager") — новое поле. UI: бейдж «менеджер» рядом с логином + склонение в текстах действий («Заблокировать менеджера «praktika»?» вместо «сотрудника»).

Безопасность

role='admin' не входит ни в один набор — это несущий инвариант, а не экономия: он один держит невозможность self-lockout'а. Manager до admin-строки не дотягивается по своей ветке фильтра, admin не дотягивается до admin-строки вообще, включая собственную. Поэтому ни block, ни смена пароля с revoke_user_sessions не могут вырубить действующего админа или разжаловать другого. Раздача/отзыв роли admin остаётся операцией уровня миграции/psql — сознательно вне API.

Org-изоляция manager'ов не ослаблена: расширение прав дано только актору-admin.

Проверено

  • pytest tests/test_team_api.py tests/test_rbac.py — 66 passed
  • 6 новых тестов: список с менеджерами · сброс пароля менеджеру + отзыв его сессий + старый пароль перестаёт работать · блокировка менеджера · manager не достаёт до чужого менеджера (404) · admin не достаёт до admin-строки (404) · manager не видит менеджеров в своём списке
  • 3 существующих теста обновлены под новое ожидание admin-списка (test_admin_sees_all_employees_and_filters_by_manager, N+1-guard, pagination-tiebreak)
  • ruff check — чисто по изменённым файлам (3 оставшихся RUF059 — pre-existing в test_estimator_pure_units.py, не тронут)
  • frontend tsc --noEmit + next build — зелёные
## Проблема (инцидент 2026-07-31) После cutover'а на DB-auth (#2558) аккаунты `kopylov` и `praktika` сидят с `role='manager'`, а team-API жёстко фильтровал `role='employee'` (`_fetch_employee_row`, `_LIST_EMPLOYEES_*_SQL`). Следствие: **сбросить менеджеру пароль или заблокировать его через UI было нечем** — только ручной psql на проде. Всплыло сегодня: «Практика» весь день билась в форму входа (5 `login_failed` с трёх разных IP, ни одного успеха), при этом в поле «Логин» вводили старый basic_auth-пароль. Восстановить им доступ админ из интерфейса не мог. Сам инцидент с доступом разрешён отдельно (пароли из волта оказались валидны, сверены с прод-хешами) — этот PR закрывает то, что сделало ситуацию неразрешимой из UI. ## Что меняется - `_fetch_employee_row` принимает `actor`: **admin** → `role IN ('employee','manager')`, **manager** → как было, только `role='employee'` + свои по `manager_id`. Два статических SQL-варианта, без динамической сборки WHERE (конвенция файла). - `GET /employees` без фильтра отдаёт admin'у и менеджеров. `?manager_id=` не меняется — только сотрудники указанного менеджера. - `EmployeeOut.role` (`"employee" | "manager"`) — новое поле. UI: бейдж «менеджер» рядом с логином + склонение в текстах действий («Заблокировать **менеджера** «praktika»?» вместо «сотрудника»). ## Безопасность `role='admin'` не входит ни в один набор — это несущий инвариант, а не экономия: он один держит невозможность self-lockout'а. Manager до admin-строки не дотягивается по своей ветке фильтра, admin не дотягивается до admin-строки вообще, включая собственную. Поэтому ни block, ни смена пароля с `revoke_user_sessions` не могут вырубить действующего админа или разжаловать другого. Раздача/отзыв роли admin остаётся операцией уровня миграции/psql — сознательно вне API. Org-изоляция manager'ов не ослаблена: расширение прав дано **только** актору-admin. ## Проверено - `pytest tests/test_team_api.py tests/test_rbac.py` — 66 passed - 6 новых тестов: список с менеджерами · сброс пароля менеджеру + отзыв его сессий + старый пароль перестаёт работать · блокировка менеджера · manager не достаёт до чужого менеджера (404) · admin не достаёт до admin-строки (404) · manager не видит менеджеров в своём списке - 3 существующих теста обновлены под новое ожидание admin-списка (`test_admin_sees_all_employees_and_filters_by_manager`, N+1-guard, pagination-tiebreak) - `ruff check` — чисто по изменённым файлам (3 оставшихся RUF059 — pre-existing в `test_estimator_pure_units.py`, не тронут) - frontend `tsc --noEmit` + `next build` — зелёные
lekss361 added 1 commit 2026-07-31 12:52:31 +00:00
fix(tradein/team): admin управляет менеджерами — сброс пароля и блокировка (#2556)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 12s
CI / changes (pull_request) Successful in 12s
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 / frontend-checks (pull_request) Successful in 1m28s
CI Trade-In / backend-tests (pull_request) Successful in 2m27s
5817e0b31a
После cutover'а на DB-auth (#2558) аккаунты `kopylov` и `praktika` живут с
`role='manager'`, а team-API жёстко фильтровал `role='employee'` — сбросить
менеджеру пароль или заблокировать его было НЕЧЕМ, кроме ручного psql на проде.
Всплыло 2026-07-31: «Практика» весь день билась в логин (5 failed, 0 успешных),
а восстановить доступ через UI админ не мог.

Что меняется:
- `_fetch_employee_row` берёт actor: admin → `role IN ('employee','manager')`,
  manager → по-прежнему только `role='employee'` + свои по `manager_id`.
- `GET /employees` без фильтра отдаёт admin'у и менеджеров (`?manager_id=` —
  без изменений, только сотрудники этого менеджера).
- `EmployeeOut.role` — новое поле, UI показывает бейдж «менеджер» и
  склоняет тексты («Заблокировать менеджера ...» вместо «сотрудника»).

Инвариант self-lockout сохранён и усилен тестом: строки `role='admin'`
недостижимы через этот роутер ни для кого, включая самого админа, поэтому
ни block, ни смена пароля с `revoke_user_sessions` не могут вырубить
действующего админа. Раздача роли admin остаётся вне API.

Тесты: 6 новых (список с менеджерами, сброс пароля менеджеру + отзыв сессий,
блокировка, manager не достаёт до чужого менеджера, admin не достаёт до
admin-строки), 3 существующих обновлены под новое ожидание списка.
lekss361 merged commit 910ef2503c into main 2026-07-31 13:08:14 +00:00
lekss361 deleted branch fix/tradein-team-admin-manages-managers 2026-07-31 13:08:15 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2575
No description provided.