feat(tradein): переключаемый реестр людей — подготовка переезда «Меры» в БД auth [PR-2b/6] #2608
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
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#2608
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/auth-mera-identity-store"
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?
Третий PR эпика «единый вход». Продолжение #2597 (БД
auth) и #2602 (роли +access_state).Прод после мержа работает ровно как сейчас
IDENTITY_STORE="tradein"— дефолт и сегодняшнее поведение:tradein_users/tradein_sessions, соединение с БДauthне открывается вообще. Переключение — одна переменная окружения, уже после того как на проде появится парольauth_appи будут скопированы данные.Флаг сделан намеренно, а не «на всякий случай»: на проде роль
auth_appдо сих пор без пароля (rolpassword IS NULL— проверено), а хеши, роли и 8 живых сессий вauthещё не перенесены. Мерж, который зависит от невыполненного ручного шага, — это мерж, который ломает прод в момент невнимательности. Здесь порядок обратный: код едет первым и спит, данные и пароль догоняют, переключение — отдельное осознанное действие с мгновенным откатом той же переменной.Ядро
app/services/identity_store.py— единственное место, знающее, в какой БД и в каких таблицах живёт реестр людей. Имена таблиц берутся из фиксированного словаря по значению флага; конкатенации с внешним вводом нет (имя таблицы — не тот параметр, который можно биндить).app/core/auth_db.py— ленивый engine БДauthпод блокировкой. Отдельный модуль именно потому, чтоcore/db.pyсоздаёт engine на импорте: то же самое для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(свойство), а неstate == "active"(сравнение, которое забудут обновить).Логин в режиме
authactivetrial_expireddetail.code="access_expired", сессия не создаётсяdisabledПароль проверяется всегда и до ветвления по состоянию — иначе появляется timing-oracle, и по времени ответа можно отличить существующий логин от несуществующего. Честный текст показывается только после верного пароля, поэтому перечисление логинов остаётся невозможным: заблокированный аккаунт с неверным паролем неотличим от несуществующего (запинено тестом).
Резолв уже выданной сессии пропускает только
active— блокировка обрывает сессию немедленно, а не по истечении sliding-refresh.Старт падает явно при недонастроенном режиме
Если
IDENTITY_STORE=auth, а DSN пуст — приложение не поднимается.Причина не в аккуратности, а в том, как выглядела бы ошибка иначе: продуктовая БД жива, приложение полностью работоспособно, а
rbac_guardловитAuthDatabaseNotConfiguredErrorвместе с любым другим сбоем резолва и падает в legacy trusted-header ветку (auth_mode='dual'). То есть недонастроенный деплой сутками раздавал бы права изroles.yamlмимо реестра — включая аккаунты сdisabled. Это нашло ревью, и это исправлено здесь же.На дефолтный режим не влияет: ветка не выполняется, пустой
AUTH_DATABASE_URLпо-прежнему не ошибка. Проверяется конфигурация, а не доступность БД —create_engineк серверу не ходит, недоступный сервер старту не мешает.Форма входа понимает новый код
Ветвление по
detail.code, а не по тексту сообщения: текст бэкенд вправе менять, код — нет. Без этого 403 показывался бы как «Проверьте подключение» — контракт без потребителя бесполезен (второе замечание ревью).Гранты соблюдены, а не обойдены
auth_appне имеетUPDATEнаrole/manager_idи не имеетDELETEнаusers(миграция 004, column-level). Код этих путей не пишет — блокировка сотрудника идёт черезaccess_state, а не через удаление строки.Тесты
2996 passed (+59 к базе), 9 skipped.
Единственный красный —
tests/test_search_api.py::test_search_cache_hit(assert 401 == 200) — предсуществующий. Проверено не рассуждением, а контрольным полным прогоном на чистомmainв отдельном worktree: 2937 passed, тот же самый красный. Модуль не собирается в изоляции, поэтому одиночный прогон как доказательство не годится — нужен был именно полный.Новые тесты покрывают: оба режима, 403 при
trial_expired, неотличимость неверного пароля от заблокированного аккаунта, отказ сессии для не-active, перезаписьX-Authenticated-Userв ASGI-scope при валидной куке (CRITICAL #2552 — проверено, что подделанный заголовок не выигрывает у куки), и дефолт флага прямым ассертом.Ревью
Три скептика в свежих контекстах, с мутационными проверками (ломали код и смотрели, краснеет ли сьют). Два замечания уровня medium — тихая деградация в legacy-ветку при неверном DSN и контракт 403 без потребителя — закрыты в этом же коммите. Заодно ревью поймало изъян самого процесса: mutation-харнесс правил файлы в общем worktree параллельно с чтением; финальное состояние дерева проверено отдельно, следов мутаций нет.
Test plan
uv run pytest -q— 2996 passed, 1 предсуществующий красный (контроль на main: 2937 passed, тот же)ruff checkпо изменённым файлам — чисто (3 ошибки вtest_estimator_pure_units.pyпредсуществующие, файл не тронут; локальный ruff новее пиннутого)tscфронта — локально нетnode_modules, проверитCI Trade-In / frontend-checksЧто должно случиться на проде ПЕРЕД переключением флага
AUTH_DB_PASSWORDв двух файлах (/opt/gendesign/backend/.env.runtimeи/opt/gendesign/tradein-mvp/backend/.env.runtime) +ALTER ROLE.tradein_users → auth.users:password_hash,role,manager_id,display_name,org_name,email.access_stateне копируется — вauthон уже выставлен верно (user2 = trial_expired, а вtradein_usersто же состояние выражено какis_active=false, что дало быdisabled).tradein_sessions → auth.sessionsс ремаппингомuser_idпоusername— id между базами не гарантированно совпадают. Без этого восемь человек разлогинятся.AUTH_DATABASE_URL(хостgendesign-postgres, неpostgres— внутри стека «Меры» это имя резолвится в её собственный контейнер) +IDENTITY_STORE=auth+ пересоздание контейнера (deploy-tradein.ymlне делает--force-recreateприIMAGE_TAG=latest).Откат на любом шаге — вернуть
IDENTITY_STORE=tradeinи перезапустить.