gendesign/ops/db-bootstrap/create_auth_db.sql
bot-backend bea61f6cd9
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
feat(auth): отдельная БД auth — фундамент единого входа «Меры» и «Птицы»
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'ом, деплой не падает.
2026-07-31 21:47:33 +03:00

67 lines
6.5 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- Создание БД `auth` — единого хранилища доступов «Меры» и «Птицы» (идемпотентно).
--
-- Applied by .forgejo/workflows/deploy.yml ПЕРЕД миграциями data/sql/auth/*.sql:
-- docker compose ... exec -T postgres psql -U "$POSTGRES_USER" -d postgres \
-- -v ON_ERROR_STOP=on < ops/db-bootstrap/create_auth_db.sql
-- Подключение обязательно к БД `postgres`: нельзя создать базу, находясь в ней самой.
--
-- ПОЧЕМУ ЭТО НЕ МИГРАЦИЯ:
-- CREATE DATABASE запрещён внутри транзакционного блока, а .claude/rules/sql.md требует
-- от каждого файла в data/sql обёртки BEGIN/COMMIT. Плюс миграции `auth` по определению
-- выполняются уже ВНУТРИ БД `auth` — то есть создать её собой они не могут. Отсюда
-- отдельный bootstrap-шаг, по образцу scripts/bootstrap_glitchtip.sh (там так же
-- заводится вторая БД на этом же сервере).
--
-- ПОЧЕМУ \gexec, А НЕ DO-БЛОК:
-- DO-блок — это функция, она выполняется внутри транзакции, значит CREATE DATABASE в ней
-- недопустим. \gexec строит текст команды на стороне клиента и отправляет её отдельным
-- стейтментом. Если WHERE NOT EXISTS отфильтровал строку, \gexec не получает ничего и
-- молча ничего не делает — это и даёт идемпотентность без ошибки на повторном прогоне.
-- ON_ERROR_STOP=on распространяется и на команды, выполненные через \gexec.
--
-- ВЛАДЕЛЕЦ БД — $POSTGRES_USER (суперюзер кластера), НЕ auth_app. Владелец объекта имеет на
-- него все права в обход GRANT'ов; если бы БД и таблицы принадлежали прикладной роли,
-- точечные гранты в data/sql/auth/002_auth_app_role.sql были бы декорацией. Роль auth_app
-- создаётся миграцией 002 и получает только нужные DML-права.
--
-- TEMPLATE template0 — сознательно, а не template1 (шаблон по умолчанию): template0
-- гарантированно пуст и неизменяем, а в template1 любой может доустановить расширения или
-- объекты, и они молча окажутся в хранилище паролей. На образе postgis:16-3.4 сегодня
-- postgis лежит в template_postgis, а template1 чист (проверено локально на том же образе),
-- но полагаться на это как на инвариант незачем — template0 снимает вопрос навсегда.
-- ENCODING 'UTF8' указан явно (кластер и так UTF8 — вся кириллица gendesign лежит в нём),
-- чтобы кодировка хранилища логинов не зависела от того, с какими аргументами когда-нибудь
-- пересоздадут кластер.
--
-- Пароля в этом файле нет и быть не может: роль создаётся passwordless в миграции 002,
-- пароль ставится отдельным шагом из env (ops/db-bootstrap/set_auth_app_password.sql).
SELECT 'CREATE DATABASE auth TEMPLATE template0 ENCODING ''UTF8'';'
WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'auth')
\gexec
-- Единственная преграда для «любая login-роль кластера (glitchtip, tradein_fdw_reader,
-- gendesign_reader) открывает сессию в хранилище паролей»: по умолчанию PostgreSQL выдаёт
-- CONNECT роли PUBLIC при создании БД.
--
-- ДУБЛЬ С data/sql/auth/002_auth_app_role.sql — НАМЕРЕННЫЙ, не копипаста. Инвариант держится
-- в двух местах, потому что у файлов разный жизненный цикл:
-- * здесь (bootstrap) — ради ПЕРЕПРИМЕНЯЕМОСТИ: этот файл гоняется на КАЖДОМ деплое, там же,
-- где создаётся БД. Если `auth` восстановят из дампа или пересоздадут в обход миграций,
-- база появится с дефолтным PUBLIC-CONNECT, а 002 уже числится применённой в
-- _schema_migrations и второй раз не выполнится — REVOKE молча не вернётся.
-- * в 002 — ради САМОДОСТАТОЧНОСТИ миграции: применённая на пустую БД (scratch/staging,
-- ручной psql -f) она обязана давать полный периметр прав без чтения bootstrap-файлов.
-- Удалять любую из двух копий нельзя: каждая закрывает сценарий, который другая не покрывает.
--
-- Выполнимо из подключения к БД `postgres` (мы именно в ней): права на объект DATABASE живут
-- в pg_database.datacl — это общий на кластер каталог, не локальный для БД, в отличие от
-- грантов на таблицы/схемы. Проверено эмпирически на postgis:16-3.4 (REVOKE из сессии в
-- `postgres` по другой БД убирает `=Tc/` из datacl, has_database_privilege('public', …,
-- 'CONNECT') → false). Команда идемпотентна — повторный прогон бесплатен.
REVOKE ALL ON DATABASE auth FROM PUBLIC;
COMMENT ON DATABASE auth IS
'Единое хранилище доступов: «Мера» (trade-in) и «Птица» (Site Finder). Схема — '
'data/sql/auth/*.sql, применяется отдельным циклом миграций в .forgejo/workflows/deploy.yml '
'(таблица _schema_migrations живёт внутри этой же БД).';