feat(auth): отдельная БД auth — фундамент единого входа «Меры» и «Птицы» [PR-1/6] #2597

Merged
lekss361 merged 1 commit from feat/auth-db-foundation into main 2026-07-31 19:06:42 +00:00
Owner

Первый PR эпика: вся авторизация переезжает на одну нейтральную форму входа для «Меры» (/trade-in) и «Птицы» (раздел Site Finder), браузерный popup (Caddy basic_auth) убирается.

Этот PR — только фундамент. Прод работает ровно как сейчас: в БД gendesign ничего не меняется, новая БД создаётся и наполняется логинами без паролей, читать её пока некому.

Почему отдельная БД

Хранилище доступов не должно принадлежать продукту, из которого аккаунты выносятся. Сервер — существующий gendesign-postgres, новый контейнер не заводим. Замерено на проде: оба бэкенда сидят в сети gendesign_shared и TCP-достают до него.

Состав

  • data/sql/auth/001-003 — схема (users, sessions), роль приложения, сид 13 логинов.
    password_hash = NULL у всех — plaintext и bcrypt-хеши в git запрещены (hard-tripwire), пароли проставляются отдельно на проде. Прецедент — tradein м.193.
  • ops/db-bootstrap/create_auth_db.sqlCREATE DATABASE через \gexec. Не миграцией: CREATE DATABASE запрещён в транзакции, а миграции обязаны быть транзакционными.
  • ops/db-bootstrap/set_auth_app_password.sql — пароль роли из env, зеркало set_tradein_fdw_password.sql (GUC + \o /dev/null + %L, строго через stdin: :'pw' не интерполируется внутри $$...$$, на этом падал деплой 2026-05-24).

Схема в подкаталоге data/sql/auth/ намеренно: основной цикл деплоя использует ls -1 data/sql/*.sql, который в подкаталоги не рекурсирует → эти файлы физически не могут примениться в БД gendesign. Защита не на дисциплине, а на глобе. Триггер data/sql/** подкаталог при этом покрывает.

Права

Владелец БД — суперюзер, а не auth_app (иначе гранты были бы декорацией). users — только SELECT+UPDATE: INSERT не выдан, потому что создания аккаунтов в этом PR нет, а снять грант, на который уже опирается прод-код, сложнее, чем выдать. REVOKE ALL ON DATABASE FROM PUBLIC продублирован в bootstrap и в миграции намеренно — bootstrap гоняется каждый деплой (переприменяемость), миграция однократно (самодостаточность); перекрёстные комментарии в обоих файлах.

Проверено исполнением

Не по комментариям, а прогоном на postgis/postgis:16-3.4 — том же образе, что на проде:

  • двойной прогон всех файлов идемпотентен (второй даёт только «already exists»);
  • COALESCE-защита сида не затирает вручную проставленные пароль/имя — проверено живьём;
  • ASCII-CHECK отклоняет кириллицу и слишком короткий логин;
  • auth_app коннектится и не может INSERT/DELETE в users и CREATE TABLE; посторонняя login-роль → permission denied for database "auth";
  • битая миграция даёт exit 1 и НЕ пишется в _schema_migrations — деплой прервётся до подъёма контейнеров (блок стоит перед compose up -d);
  • пароль с кавычками и бэкслешем не ломает %L и не печатается в stdout;
  • смоделирован целевой регресс: GRANT ALL ... TO PUBLIC (эквивалент восстановления из дампа мимо миграций) → повторный bootstrap возвращает public_connect = f.

Тест

backend/tests/sql/test_auth_sql_migrations.py — имена, транзакционность, наличие wiring в deploy.yml, и детектор паролей/хешей, покрывающий и data/sql/auth, и ops/db-bootstrap (единственное место в репе с ALTER ROLE ... PASSWORD; detect-private-key bcrypt не ловит, а репо-wide grep невозможен — caddy/users.caddy.snippet легально содержит хеши). Детектор проверен на живучесть: подложенный bcrypt-хеш роняет тест, при этом проверки имён/BEGIN-COMMIT на bootstrap-файлы не распространяются (они намеренно без номеров и без транзакций).

Открытые развилки (зафиксированы комментариями в коде, решаются в PR-2/3)

  1. Судьба tradein_users/tradein_sessions — после этого PR в проде два одинаковых по схеме хранилища сессий в разных БД. Пока никто не читает новое — безопасно; но до того, как в новую БД проставят первые хеши, надо решить: read-through или вывод из эксплуатации.
  2. expired != disabled — сид кодирует user2 как is_active=false, но сегодня в roles.yaml у него role=expired, и семантика другая: такой юзер доходит до фронта и видит trial-экран. Булев флаг это состояние не выражает; при переключении trial-экран пропадёт молча, без падения тестов.

Требует ручного шага на VPS

Переменную AUTH_DB_PASSWORD завести в runtime-env бэкенда. Пока пусто — шаг ALTER ROLE пропускается с warning'ом, деплой не падает.

Дальше по эпику

PR-2 backend (сервис сессий + dual-mode guard в обоих бэкендах) → PR-3 нейтральная форма входа → PR-4 canary → PR-5 cutover (снятие popup) → PR-6 уборка. Инвариант: гейт снимается последним, только после того, как замена доказана в проде.

Первый PR эпика: вся авторизация переезжает на **одну нейтральную форму входа** для «Меры» (`/trade-in`) и «Птицы» (раздел Site Finder), браузерный popup (Caddy basic_auth) убирается. **Этот PR — только фундамент. Прод работает ровно как сейчас:** в БД `gendesign` ничего не меняется, новая БД создаётся и наполняется логинами без паролей, читать её пока некому. ## Почему отдельная БД Хранилище доступов не должно принадлежать продукту, из которого аккаунты выносятся. Сервер — существующий `gendesign-postgres`, новый контейнер не заводим. Замерено на проде: оба бэкенда сидят в сети `gendesign_shared` и TCP-достают до него. ## Состав - `data/sql/auth/001-003` — схема (`users`, `sessions`), роль приложения, сид 13 логинов. **`password_hash = NULL` у всех** — plaintext и bcrypt-хеши в git запрещены (hard-tripwire), пароли проставляются отдельно на проде. Прецедент — tradein м.193. - `ops/db-bootstrap/create_auth_db.sql` — `CREATE DATABASE` через `\gexec`. Не миграцией: `CREATE DATABASE` запрещён в транзакции, а миграции обязаны быть транзакционными. - `ops/db-bootstrap/set_auth_app_password.sql` — пароль роли из env, зеркало `set_tradein_fdw_password.sql` (GUC + `\o /dev/null` + `%L`, строго через stdin: `:'pw'` не интерполируется внутри `$$...$$`, на этом падал деплой 2026-05-24). **Схема в подкаталоге `data/sql/auth/` намеренно:** основной цикл деплоя использует `ls -1 data/sql/*.sql`, который в подкаталоги не рекурсирует → эти файлы физически не могут примениться в БД `gendesign`. Защита не на дисциплине, а на глобе. Триггер `data/sql/**` подкаталог при этом покрывает. ## Права Владелец БД — суперюзер, а не `auth_app` (иначе гранты были бы декорацией). `users` — только SELECT+UPDATE: INSERT не выдан, потому что создания аккаунтов в этом PR нет, а снять грант, на который уже опирается прод-код, сложнее, чем выдать. `REVOKE ALL ON DATABASE FROM PUBLIC` продублирован в bootstrap и в миграции намеренно — bootstrap гоняется каждый деплой (переприменяемость), миграция однократно (самодостаточность); перекрёстные комментарии в обоих файлах. ## Проверено исполнением Не по комментариям, а прогоном на `postgis/postgis:16-3.4` — том же образе, что на проде: - двойной прогон всех файлов идемпотентен (второй даёт только «already exists»); - COALESCE-защита сида не затирает вручную проставленные пароль/имя — проверено живьём; - ASCII-CHECK отклоняет кириллицу и слишком короткий логин; - `auth_app` коннектится и не может INSERT/DELETE в `users` и `CREATE TABLE`; посторонняя login-роль → `permission denied for database "auth"`; - **битая миграция даёт exit 1 и НЕ пишется в `_schema_migrations`** — деплой прервётся до подъёма контейнеров (блок стоит перед `compose up -d`); - пароль с кавычками и бэкслешем не ломает `%L` и не печатается в stdout; - смоделирован целевой регресс: `GRANT ALL ... TO PUBLIC` (эквивалент восстановления из дампа мимо миграций) → повторный bootstrap возвращает `public_connect = f`. ## Тест `backend/tests/sql/test_auth_sql_migrations.py` — имена, транзакционность, наличие wiring в `deploy.yml`, и детектор паролей/хешей, покрывающий **и** `data/sql/auth`, **и** `ops/db-bootstrap` (единственное место в репе с `ALTER ROLE ... PASSWORD`; `detect-private-key` bcrypt не ловит, а репо-wide grep невозможен — `caddy/users.caddy.snippet` легально содержит хеши). Детектор проверен на живучесть: подложенный bcrypt-хеш роняет тест, при этом проверки имён/BEGIN-COMMIT на bootstrap-файлы не распространяются (они намеренно без номеров и без транзакций). ## Открытые развилки (зафиксированы комментариями в коде, решаются в PR-2/3) 1. **Судьба `tradein_users`/`tradein_sessions`** — после этого PR в проде два одинаковых по схеме хранилища сессий в разных БД. Пока никто не читает новое — безопасно; но до того, как в новую БД проставят первые хеши, надо решить: read-through или вывод из эксплуатации. 2. **`expired` != `disabled`** — сид кодирует `user2` как `is_active=false`, но сегодня в `roles.yaml` у него `role=expired`, и семантика другая: такой юзер доходит до фронта и видит trial-экран. Булев флаг это состояние не выражает; при переключении trial-экран пропадёт молча, без падения тестов. ## Требует ручного шага на VPS Переменную `AUTH_DB_PASSWORD` завести в runtime-env бэкенда. Пока пусто — шаг `ALTER ROLE` пропускается с warning'ом, деплой не падает. ## Дальше по эпику PR-2 backend (сервис сессий + dual-mode guard в обоих бэкендах) → PR-3 нейтральная форма входа → PR-4 canary → PR-5 cutover (снятие popup) → PR-6 уборка. Инвариант: **гейт снимается последним**, только после того, как замена доказана в проде.
lekss361 added 1 commit 2026-07-31 18:48:28 +00:00
feat(auth): отдельная БД auth — фундамент единого входа «Меры» и «Птицы»
All checks were successful
CI Trade-In / changes (pull_request) Successful in 14s
CI / changes (pull_request) Successful in 14s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 3m0s
CI / backend-tests (pull_request) Successful in 15m35s
bea61f6cd9
PR-1 эпика: вся авторизация переезжает на одну нейтральную форму входа, браузерный
popup (Caddy basic_auth) убирается. Этот PR — ТОЛЬКО фундамент, прод работает как
сейчас: в БД gendesign ничего не меняется, новая БД создаётся и наполняется
логинами без паролей, читать её пока некому.

Почему отдельная БД, а не таблица в существующей: хранилище доступов не должно
принадлежать продукту, из которого аккаунты выносятся. Сервер — существующий
gendesign-postgres (новый контейнер не заводим); проверено, что оба бэкенда
сидят в сети gendesign_shared и TCP-достают до него.

Состав:
- data/sql/auth/001-003 — схема (users, sessions), роль приложения, сид 13 логинов.
  password_hash = NULL у ВСЕХ: plaintext и bcrypt-хеши в git запрещены, пароли
  проставляются отдельно на проде (конвенция репы, прецедент tradein м.193).
- ops/db-bootstrap/create_auth_db.sql — CREATE DATABASE через \gexec. Не миграцией:
  CREATE DATABASE запрещён в транзакции, а миграции обязаны быть транзакционными.
- ops/db-bootstrap/set_auth_app_password.sql — пароль роли из env, зеркало
  set_tradein_fdw_password.sql (GUC + \o /dev/null + %L, строго через stdin —
  :'pw' не интерполируется внутри $$...$$, на этом падал деплой 2026-05-24).

Схема лежит в ПОДКАТАЛОГЕ data/sql/auth/ намеренно: основной цикл деплоя использует
`ls -1 data/sql/*.sql`, который в подкаталоги не рекурсирует → эти файлы физически
не могут примениться в БД gendesign. Защита не на дисциплине, а на глобе. Триггер
`data/sql/**` подкаталог при этом покрывает.

Права: владелец БД — суперюзер, а не auth_app (иначе гранты были бы декорацией).
users — только SELECT+UPDATE (INSERT не выдан: создания аккаунтов в этом PR нет, а
снять грант, на который уже опирается прод-код, сложнее чем выдать). REVOKE ALL ON
DATABASE FROM PUBLIC продублирован в bootstrap и в миграции намеренно: bootstrap
гоняется каждый деплой (переприменяемость), миграция — однократно (самодостаточность).

Проверено ИСПОЛНЕНИЕМ на postgis/postgis:16-3.4 (тот же образ, что на проде):
двойной прогон всех файлов идемпотентен; COALESCE-защита сида не затирает вручную
проставленные пароль/имя (проверено живьём); ASCII-CHECK отклоняет кириллицу;
auth_app коннектится, посторонняя роль → permission denied; битая миграция даёт
exit 1 и НЕ пишется в _schema_migrations, т.е. деплой прервётся до подъёма кода;
пароль с кавычками и бэкслешем не ломает %L и не печатается в stdout.

Тест backend/tests/sql/test_auth_sql_migrations.py: имена, транзакционность, наличие
wiring в deploy.yml и детектор паролей/хешей, покрывающий И data/sql/auth, И
ops/db-bootstrap — единственное место в репе с ALTER ROLE ... PASSWORD.
Детектор проверен на живучесть: подложенный bcrypt-хеш роняет тест.

Открытые развилки зафиксированы комментариями в коде, решаются в PR-2/3:
судьба tradein_users/tradein_sessions (два одинаковых по схеме хранилища) и
expired != disabled (trial-экран не выражается булевым is_active).

NB: переменную AUTH_DB_PASSWORD нужно завести вручную в runtime-env бэкенда на VPS.
Пока пусто — шаг ALTER ROLE пропускается с warning'ом, деплой не падает.
lekss361 merged commit a596caf590 into main 2026-07-31 19:06:42 +00:00
lekss361 deleted branch feat/auth-db-foundation 2026-07-31 19:06:42 +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#2597
No description provided.