feat(auth): роли и трёхзначное состояние доступа в БД auth [PR-2a/6] #2602
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#2602
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/auth-schema-roles-access-state"
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 эпика «единый вход». Только схема — Python-кода нет, прод работает ровно как сейчас: в БД
authпока никто не ходит, аauth_appна проде до сих пор без пароля.Закрывает обе развилки, оставленные открытыми в #2597 (решения владельца от 31.07).
Развилка А → полный переезд
tradein_users(БДtradein) в итоге удаляется,auth.usersстановится единственным реестром людей. Значитroleиmanager_idпереезжают сюда.Это отменяет решение 001:15-19 («здесь НЕТ колонки role — сознательно»). Применённую миграцию править нельзя, поэтому актуальная правда живёт в 004: шапка адресно перечисляет, какие формулировки 001/002/003 больше не действуют, и
COMMENT ON TABLE usersпереписан — старый утверждал, что роли остаются в продуктовых БД, и это стало ложью.Колонки зеркалят м.192 побуквенно (CHECK ролей, иерархический CHECK, partial index, self-FK
ON DELETE SET NULL) — чтобы код «Меры» переехал наauth.usersбез правок. Сверено с живой схемой прода, не с файлом.Добавлен
users_manager_not_self_ck: на уровне БД самоназначение менеджером сейчас проходит, а валидация есть только в «Мере» (team.py) — у второго потребителя, «Птицы», её нет, и любой будущийWITH RECURSIVEпоmanager_idна такой строке зациклится.Развилка Б → три состояния вместо булева флага
is_activeзаменён наaccess_state ∈ (active, trial_expired, disabled).activetrial_expireddisabledСмысл разделения: булев флаг схлопывал «пускаем внутрь, но объясняем» и «не пускаем вовсе» в одно значение — trial-экран исчез бы молча, без падения тестов (ровно это предсказывал комментарий 003:78-90). Честный текст показывается только после верного пароля, поэтому защита от перечисления логинов сохраняется.
user2(«Брусника») →trial_expired.Семантика зафиксирована в
COMMENT ON COLUMN, чтобы автор PR-2b читал её из БД, а не угадывал.Гранты
INSERT выдан — без него переезд не состоится: создание сотрудника из «Команды» сегодня пишет в
tradein_users, а её не станет.DELETE НЕ выдан. Первая редакция его содержала; убрано после ревью. Потребителя нет (в
team.pyтолько POST и PATCH), а 002:22-33 отклоняла ровно такие гранты-на-будущее.DELETEпоusersкаскадит наsessions, то есть цена ошибки выше обычной, а строкаGRANTв миграции того PR, где появится хендлер, стоит столько же. Закрытие доступа — этоaccess_state, а не удаление строки.Табличный
UPDATEиз 002:80 сужен до column-level. Он автоматически распространялся бы на всё, что добавила эта миграция:auth_appмолча получил бы право писатьroleиaccess_state. Тогда ошибка в единственном UPDATE-эндпоинте (team.pyPATCH) превращалась бы из «испортил профиль» вSET role='admin' WHERE id=<свой>или тихое снятие блокировки — без смены пароля, то есть без внешнего признака компрометации. Выданы ровно поля сегодняшнего PATCH;roleиmanager_idне включены — их не пишет никто.Гранта на
users_id_seqнет намеренно. 002:26-27 записала как факт, что «идентичность требует nextval». ДляGENERATED ALWAYS AS IDENTITYэто неверно: PostgreSQL подставляет неnextval('...'), аNextValueExpr→nextval_internal(seqid, check_permissions := false), ACL последовательности не проверяется вовсе. Проверено обратным экспериментом (REVOKE, затем INSERT — прошёл; контрольная таблица сbigserialв тех же условиях упала). Разбор оставлен в файле: «auth_app сделал INSERT, значит грант рабочий» ничего не доказывает.Проверено исполнением на
postgres:16Не по комментариям — прогоном:
admin/manager×2/employee×10,user2 = trial_expired,is_activeотсутствует, все 6 констрейнтов на месте;user2 → activeиuser3 → managerостались после третьего прогона — backfill не затирает решения владельца, принятые между деплоями;auth_app:INSERT users✓,UPDATE access_state✓,INSERT sessions✓ ·UPDATE role✗,UPDATE manager_id✗,DELETE✗,CREATE TABLE✗;adminсmanager_id, self-manager,access_stateвне списка,roleвне списка, INSERT безrole.Ревью — три независимых скептика в отдельных контекстах, 8 находок, все не блокирующие; две (ложное обоснование гранта на sequence, молчаливое расширение UPDATE) нашлись только потому, что ревьюеры ставили эксперименты, а не читали комментарии.
Известные ограничения, записанные в файл
is_active). Прогон каталога целиком на пустой БД чист — это и есть штатный recovery-путь; проверено.COMMENT ON COLUMN access_stateкак требование к API: PR-2b обязан отклонять такой переход. Сегодня путь закрыт тем, что «Команда» не отдаёт строки сrole='admin'.manager_idуказывает на строку сrole='manager'» строчным CHECK не выражается — зафиксировано COMMENT'ом как инвариант приложения, адресно для второго потребителя.Блокер деплоя (не этого PR, но следующего)
На проде
auth_appбез пароля (rolpassword IS NULL— проверено), потому чтоAUTH_DB_PASSWORDне заведён в.env.runtime. Эту миграцию отсутствие пароля не ломает (она идёт под суперюзером), но PR-2b без него не подключится. Переменную нужно завести в двух файлах:/opt/gendesign/backend/.env.runtimeи/opt/gendesign/tradein-mvp/backend/.env.runtime— у стеков разныеenv_file.Test plan
uv run pytest tests/sql/test_auth_sql_migrations.py -q— 6 passed (добавлена проверка запретаCREATE INDEX CONCURRENTLY: в связке с обязательной обёрткой BEGIN/COMMIT это невыполнимая на проде комбинация, а отдельной проверки не было)postgres:16— см. вышеdata/sql/auth/*.sqlвdeploy.ymlДальше
PR-2b — «Мера» переезжает на БД
auth(второй engine, ~14 SQL-запросов,team.py, ~2100 строк тестов с fake-DB по тексту SQL). PR-2c — guard «Птицы». Перед деплоем 2b на прод нужно скопировать вauth.usersbcrypt-хеши и роли изtradein_users, а вauth.sessions— 8 живых сессий, иначе пилоты разлогинятся.