6 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| eccb895db1 |
feat(tradein): переключаемый реестр людей — подготовка переезда «Меры» в БД auth [PR-2b/6]
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 7s
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 1m3s
CI Trade-In / backend-tests (pull_request) Successful in 2m40s
Дефолт не меняет ничего: IDENTITY_STORE="tradein" — это сегодняшний прод, tradein_users/tradein_sessions, соединение с БД auth не открывается вообще. Переключение делается одной переменной окружения ПОСЛЕ того, как на проде появится пароль auth_app и будут скопированы данные. Так сделано намеренно: мерж, который зависит от невыполненного ручного шага, — это мерж, который ломает прод в момент невнимательности. Ядро. app/services/identity_store.py — единственное место, знающее, в какой БД и в каких таблицах живёт реестр. Имена таблиц берутся из фиксированного словаря по значению флага, не конкатенацией с вводом. app/core/auth_db.py — ЛЕНИВЫЙ engine БД auth (core/db.py создаёт свой на импорте; такое же для auth роняло бы старт без DSN). Одно понятие состояния доступа вместо двух. В tradein_users состояние — булев is_active, в auth.users — access_state из трёх значений. Конверсия живёт в одной функции to_access_state(): True→active, False→disabled, а неизвестная строка, NULL или чужой тип → disabled с WARNING. Fail-closed выбран сознательно: если следующая миграция добавит четвёртое состояние, оно по умолчанию НЕ будет пускать. Проверка доступа — свойство can_sign_in, а не сравнение со строкой. Логин в режиме auth. Пароль проверяется ВСЕГДА и ДО ветвления по состоянию — иначе появляется timing-oracle и перечисление логинов. Верный пароль + trial_expired → 403 с машиночитаемым code="access_expired", сессия НЕ создаётся. Верный пароль + disabled → тот же generic 401, что и при неверном пароле. Резолв уже выданной сессии пропускает только active — блокировка обрывает сессию немедленно, а не по истечении sliding-refresh. Старт падает явно, если IDENTITY_STORE=auth, а DSN не задан. Без этого ошибка конфигурации не похожа на аварию: продуктовая БД жива, приложение работает, а rbac_guard ловит исключение резолва вместе с любым другим сбоем и падает в legacy trusted-header ветку — то есть сутками раздаёт права из roles.yaml мимо реестра, включая аккаунты с disabled. Форма входа понимает новый код ответа. Ветвление по detail.code, а не по тексту: текст бэк вправе менять, код — нет. Гранты соблюдены, а не обойдены: auth_app не имеет UPDATE на role/manager_id и не имеет DELETE на users (миграция 004, column-level). Тесты: 2996 passed (+59). Единственный красный — test_search_cache_hit — предсуществующий: проверен контрольным полным прогоном на чистом main (2937 passed, тот же красный). |
|||
|
|
5817e0b31a |
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
После 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 существующих обновлены под новое ожидание списка. |
||
|
|
df943ea1c7 |
fix(tradein/team): \Z вместо $ в username-regex — 422 вместо 500 на trailing newline (#2554)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 13s
CI / changes (pull_request) Successful in 13s
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) Successful in 2m18s
Deep-review seed'а (#2564) нашёл смежный дефект в уже смерженном коде (#2563): Python `$` матчит перед trailing newline (re.match(r'...\$', 'admin\n') -> True), а Postgres `~` в CHECK tradein_users_username_ascii_ck (миграция 193) - False. username="admin\n" проходил Pydantic-валидацию и падал уже в БД -> 500 вместо честного 422. `\Z` - конец строки без поблажки на trailing newline, совпадает с семантикой Postgres `~`. Grep по app/schemas/ (pattern=/regex=/re.compile/re.match/re.fullmatch) - других regex-валидаторов с `$` в схемах trade-in нет, team.py - единственный файл с regex-based полем. |
||
|
|
2e05a16a14 |
fix(tradein/team): устойчивая сортировка списка + семантика unlimited в батч-квотах (#2554)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 11s
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) Successful in 2m0s
Deep-review PR #2563 follow-up (после merge+deploy): 1. ORDER BY created_at DESC, id DESC в _LIST_EMPLOYEES_BY_MANAGER_SQL / _LIST_EMPLOYEES_ALL_SQL. created_at DEFAULT now() — время транзакции, bulk-seed (#2557) вставляет много юзеров одной транзакцией -> идентичный timestamp у N+ строк -> без тай-брейкера порядок между LIMIT/OFFSET страницами на PostgreSQL для строк-близнецов не гарантирован (сотрудники пропадали/дублировались бы при листании). id (BIGINT IDENTITY, монотонный) — детерминированный tie-break. 2. _batch_quota_status: unlimited теперь честно совпадает с account_quota.is_unlimited — override.unlimited=true честится ТОЛЬКО для username, присутствующего в roles.yaml (KeyError -> unlimited=False всегда, override даже не читается). Раньше батч всегда читал override независимо от roles.yaml -> список мог показать "unlimited" для квоты, которую реальный enforcement (check_and_raise/increment, тот же is_unlimited) не признаёт. Сегодня недостижимо (unlimited есть только у kopylov/praktika, оба в roles.yaml), но станет достижимым при расширении ролевки. |
||
|
|
4ac3971061 |
fix(tradein/team): revoke сессий при смене пароля + Origin-check + батч-квоты (#2554)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 11s
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) Successful in 1m56s
Deep-review PR #2563: - HIGH: PATCH new_password не ревокал старые сессии сотрудника — sliding-refresh продлевал скомпрометированную/чужую сессию бесконечно. Теперь revoke_user_sessions вызывается при is_active=False ИЛИ new_password (self-lockout невозможен — _fetch_employee_row фильтрует role='employee'). - Medium1: Origin/Referer-check (защита CSRF) на POST/PATCH team-роутах против settings.cors_origins; оба заголовка отсутствуют (curl-смоук внутри контейнера) — пропускаем, не ломаем прод-смоук. - Medium2: батч-квота для GET /employees (_batch_quota_status) — было 2N+3 SQL-запросов на N сотрудников, стало 5 плоско (замерено: N=10 -> 5 запросов). Добавлена пагинация limit(default 50, max 200)/offset. - Заодно: статические WHERE-ветки вместо f-string-сборки в list_employees; monthly_limit теперь сбрасывает account_quota_overrides.unlimited=false и не затирает существующий note. |
||
|
|
712c56f456 |
feat(tradein/team): team-management API — employees CRUD, quotas, stats (#2554)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 8s
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) Successful in 1m47s
Session-only identity (current_team_actor, admin|manager) поверх tradein_users/ tradein_sessions (#2552 foundation). Org-изоляция manager <-> employee через manager_id: чужой/несуществующий employee_id -> 404 (не 403 — не палим существование), POST с чужим manager_id в теле от manager игнорируется (принудительно свой id). Квота — upsert в account_quota_overrides (существующий паттерн, без правки account_quota.py). История оценок — user_events LEFT JOIN trade_in_estimates. Team-события (employee_created/blocked/unblocked/ password_reset/quota_changed) без пароля в payload. |