-- 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;