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'ом, деплой не падает.
50 lines
3.2 KiB
SQL
50 lines
3.2 KiB
SQL
-- Set auth_app password from env.
|
||
-- Applied by .forgejo/workflows/deploy.yml after auth DB migrations:
|
||
-- psql -v pw="$AUTH_DB_PASSWORD" < ops/db-bootstrap/set_auth_app_password.sql
|
||
-- Источник переменной: AUTH_DB_PASSWORD из /opt/gendesign/backend/.env.runtime (chmod 600,
|
||
-- вне git). Зеркало паттерна ops/db-bootstrap/set_tradein_fdw_password.sql и
|
||
-- tradein-mvp/ops/db-bootstrap/set_gendesign_reader_password.sql.
|
||
--
|
||
-- Idempotent: ALTER если роль существует, NOTICE и продолжает если нет (миграция
|
||
-- data/sql/auth/002_auth_app_role.sql могла ещё не примениться на первом деплое).
|
||
-- Пароль НИКОГДА не хранится в этом файле или в git — только имя переменной.
|
||
--
|
||
-- Format %L экранирует пароль как SQL string literal — безопасно даже с кавычками.
|
||
--
|
||
-- psql variable substitution (:'pw') НЕ интерполируется внутри dollar-quoted блока ($$...$$)
|
||
-- — это правило psql, не bug. Поэтому password передаём в DO через сессионный GUC
|
||
-- (set_config), который psql интерполирует ВНЕ dollar quote, и читаем внутри через
|
||
-- current_setting(). По той же причине файл подаётся через stdin, а НЕ через `psql -c`.
|
||
-- Reference incident: deploy 2026-05-24 (post-merge PR #503) упал на
|
||
-- "syntax error at or near ':'" именно на этом.
|
||
--
|
||
-- ⚠️ `set_config(name, value, is_local) -> text` ВОЗВРАЩАЕТ установленное значение. Без
|
||
-- `\o /dev/null` psql напечатал бы пароль на stdout → leak в Forgejo Actions deploy logs
|
||
-- (retained, visible всем с repo read access). Поэтому оба set_config обёрнуты в
|
||
-- `\o /dev/null` / `\o` — глушится только их вывод, NOTICE из DO block (сигнал
|
||
-- идемпотентности) остаётся видимым.
|
||
--
|
||
-- Rollback path: НЕ revert этого файла (вернёт сломанный :'pw' внутри $$). Корректный
|
||
-- rollback — unset AUTH_DB_PASSWORD в /opt/gendesign/backend/.env.runtime на VPS, deploy.yml
|
||
-- тогда пропустит этот шаг полностью (роль останется без пароля = логин по паролю невозможен).
|
||
|
||
\o /dev/null
|
||
SELECT set_config('app.auth_pw', :'pw', false);
|
||
\o
|
||
|
||
DO $$
|
||
BEGIN
|
||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'auth_app') THEN
|
||
EXECUTE format('ALTER ROLE auth_app WITH PASSWORD %L', current_setting('app.auth_pw'));
|
||
RAISE NOTICE 'auth_app password set';
|
||
ELSE
|
||
RAISE NOTICE 'auth_app role missing — migration data/sql/auth/002_auth_app_role.sql not applied yet';
|
||
END IF;
|
||
END $$;
|
||
|
||
-- Clear GUC after use (defense-in-depth — не оставляем password в session state даже на
|
||
-- short connection). Same \o trick — set_config return value is empty string here, но лишний
|
||
-- row в stdout всё равно не нужен.
|
||
\o /dev/null
|
||
SELECT set_config('app.auth_pw', '', false);
|
||
\o
|