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'ом, деплой не падает.
82 lines
6.8 KiB
PL/PgSQL
82 lines
6.8 KiB
PL/PgSQL
-- auth/002: роль приложения auth_app + гранты (least privilege).
|
||
--
|
||
-- WHY:
|
||
-- Миграции этой БД прогоняются суперюзером кластера ($POSTGRES_USER), он же владелец
|
||
-- таблиц. Бэкенды «Меры» и «Птицы» ходить под суперюзером не должны: скомпрометированный
|
||
-- бэкенд не обязан уметь DROP TABLE users. Поэтому отдельная login-роль с точечными
|
||
-- грантами. БД `auth` НЕ принадлежит auth_app (владелец — суперюзер): владелец таблицы
|
||
-- имеет на неё все права независимо от GRANT'ов, и разграничение ниже стало бы фикцией.
|
||
--
|
||
-- Пароль роли здесь НЕ задаётся — роль создаётся passwordless, пароль ставится отдельным
|
||
-- bootstrap-шагом деплоя из env (AUTH_DB_PASSWORD в /opt/gendesign/backend/.env.runtime,
|
||
-- см. ops/db-bootstrap/set_auth_app_password.sql). Ровно тот же паттерн, что у
|
||
-- gendesign_reader (tradein м.101 + set_gendesign_reader_password.sql) и tradein_fdw_reader
|
||
-- (data/sql/100_tradein_fdw_role.sql). Пароль в git не попадает ни при каких условиях.
|
||
--
|
||
-- Периметр прав (обосновано по-операционно):
|
||
-- sessions — SELECT/INSERT/UPDATE/DELETE. Полный набор: выдать токен (INSERT), проверить
|
||
-- на каждом запросе (SELECT), обновить last_seen_at (UPDATE), разлогинить и вычистить
|
||
-- протухшие (DELETE).
|
||
-- users — SELECT (найти по username, прочитать hash и is_active) + UPDATE (смена пароля
|
||
-- самим пользователем и проставление хеша админом).
|
||
-- users — INSERT/DELETE НЕ выдаются, сознательно:
|
||
-- * INSERT — создание аккаунтов в PR-1 не существует ни как код, ни как UI. Выдать грант
|
||
-- «на будущее» = держать открытой операцию, которой никто не пользуется и которую никто
|
||
-- не тестирует. Когда появится админский путь создания пользователей, грант добавляется
|
||
-- новой миграцией в одну строку (плюс GRANT USAGE на sequence, идентичность требует
|
||
-- nextval). Обратная ошибка дороже: снять грант, на который уже опирается прод-код,
|
||
-- нельзя без синхронного релиза.
|
||
-- * DELETE — не выдаётся и дальше: закрытие доступа делается через is_active = false
|
||
-- (см. комментарий к колонке в 001). Физическое удаление каскадом сносит сессии и
|
||
-- обрывает связь с историей действий пользователя в продуктовых БД, где user_id/username
|
||
-- остаются висеть; это операция уровня «руками через psql с осознанием последствий»,
|
||
-- а не то, что должен уметь HTTP-хендлер.
|
||
--
|
||
-- IDEMPOTENCY:
|
||
-- CREATE ROLE через DO-блок с проверкой pg_roles (нет ADD ROLE IF NOT EXISTS), GRANT/REVOKE
|
||
-- идемпотентны по определению. Повторный прогон — no-op. Роли в PostgreSQL общие на кластер,
|
||
-- поэтому DO-блок отработает корректно, даже если роль уже создана из другой БД.
|
||
--
|
||
-- Dependencies: 001_identity_schema.sql (гранты ссылаются на users/sessions).
|
||
|
||
BEGIN;
|
||
|
||
DO $$
|
||
BEGIN
|
||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'auth_app') THEN
|
||
CREATE ROLE auth_app LOGIN;
|
||
END IF;
|
||
END$$;
|
||
|
||
COMMENT ON ROLE auth_app IS
|
||
'Прикладная роль единой формы входа («Мера» + «Птица»). Пароль ставится '
|
||
'.forgejo/workflows/deploy.yml из env AUTH_DB_PASSWORD (backend/.env.runtime) через '
|
||
'ops/db-bootstrap/set_auth_app_password.sql. Пароль никогда не хранится в SQL-миграциях.';
|
||
|
||
-- Никто, кроме владельца БД и явно поименованных ролей, не должен даже подключаться:
|
||
-- по умолчанию PostgreSQL даёт CONNECT роли PUBLIC, то есть любая login-роль кластера
|
||
-- (glitchtip, tradein_fdw_reader, gendesign_reader) может открыть сессию в `auth`.
|
||
-- Хранилище паролей — не то место, где стоит полагаться на «а таблицы им всё равно не видны».
|
||
--
|
||
-- ЭТА СТРОКА ПРОДУБЛИРОВАНА в ops/db-bootstrap/create_auth_db.sql — намеренно, инвариант
|
||
-- держится в двух местах. Здесь — ради самодостаточности миграции: применённая на пустую БД
|
||
-- (scratch/staging, ручной psql -f) она обязана давать полный периметр прав, не полагаясь на
|
||
-- то, что кто-то отдельно прогнал bootstrap. В bootstrap — ради переприменяемости: миграция
|
||
-- выполняется РОВНО ОДИН РАЗ (трекинг в _schema_migrations), а БД может быть пересоздана из
|
||
-- дампа в обход миграций, и тогда дефолтный PUBLIC-CONNECT вернулся бы молча. Не «сокращай»
|
||
-- дубль — ни одна из копий не покрывает сценарий другой.
|
||
REVOKE ALL ON DATABASE auth FROM PUBLIC;
|
||
|
||
-- Defense-in-depth: явный REVOKE-периметр перед точечными грантами — любые унаследованные
|
||
-- или PUBLIC-гранты на существующих объектах обнуляются (паттерн из 100_tradein_fdw_role.sql).
|
||
REVOKE ALL ON ALL TABLES IN SCHEMA public FROM auth_app;
|
||
REVOKE ALL ON ALL SEQUENCES IN SCHEMA public FROM auth_app;
|
||
REVOKE ALL ON ALL FUNCTIONS IN SCHEMA public FROM auth_app;
|
||
|
||
GRANT CONNECT ON DATABASE auth TO auth_app;
|
||
GRANT USAGE ON SCHEMA public TO auth_app;
|
||
|
||
GRANT SELECT, INSERT, UPDATE, DELETE ON sessions TO auth_app;
|
||
GRANT SELECT, UPDATE ON users TO auth_app;
|
||
|
||
COMMIT;
|