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
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'ом, деплой не падает.
67 lines
6.5 KiB
SQL
67 lines
6.5 KiB
SQL
-- Создание БД `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 живёт внутри этой же БД).';
|