Netpulse_SasS/server/migrations/0012_auth.sql
byrsapty 205dd5e079 Пакування: runner міграцій і вшитий у бінарник фронтенд
netpulse-migrate замість PowerShell-скрипта: у контейнері немає ані
psql, ані PowerShell, а тягнути клієнт Postgres в образ заради одного
запуску — це половина дистрибутива на порожньому місці.

Міграції вшиті через embed і переїхали в server/migrations: embed не
бачить нічого за межами кореня свого модуля, а міграції поруч із
бінарником, який їх накочує, не можуть розійтися версіями.

Накочування під advisory-блокуванням: два інстанси при rolling update
інакше застосували б ту саму міграцію двічі. Кожен файл в одній
транзакції разом із записом у schema_migrations; виняток — continuous
aggregates, які TimescaleDB забороняє в транзакції. Змінена вже
застосована міграція зупиняє запуск: у різних інсталяціях інакше
опиниться різна схема під одним номером.

Перевірено на чистій базі: 23 міграції, 101 таблиця, повторний запуск
каже «схема актуальна».

Веб віддає сам API через embed: на self-hosted це прибирає з інструкції
встановлення цілий компонент. Три політики кешування — назавжди для
assets із хешем у імені, ніколи для index.html, коротко для решти.

Знайдено живим прогоном: невідомий шлях під /api/ віддавав 200 з
index.html, і клієнт падав на розборі HTML як JSON замість чесного 404.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 01:01:55 +03:00

94 lines
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.

-- =====================================================================
-- NetPulse :: 0012_auth.sql
-- Автентифікація людей: політики RLS для шляху входу + індекси.
--
-- Схема користувачів, ролей і сесій уже є з 0001. Бракує одного:
-- вхід відбувається ДО того, як відомий тенант. Користувач надсилає
-- лише email і пароль; поки ми не знайшли його membership, виставити
-- app.tenant_id нема з чого. Обидві таблиці, потрібні на цьому шляху,
-- закриті політикою tenant_isolation — тобто логін під роллю з RLS
-- завжди повертав би порожньо.
-- =====================================================================
-- ---------------------------------------------------------------------
-- Виняток для шляху автентифікації
--
-- Політика спрацьовує РІВНО тоді, коли тенант ще не встановлений.
-- Це навмисно вузько: у будь-якому іншому запиті порожній app.tenant_id
-- і далі означає порожній результат (див. 0011), тому забутий SET LOCAL
-- не перетворюється на витік чужих даних — він просто нічого не поверне.
--
-- Компроміс усвідомлений: у вікні «тенант ще невідомий» таблиця
-- core.users читається повністю. Прийнятно, бо запити на цьому шляху
-- пише лише сервер і всі вони одразу звужені за email або token_hash.
-- ---------------------------------------------------------------------
CREATE POLICY users_auth_lookup ON core.users
FOR SELECT
USING (core.current_tenant() IS NULL);
-- Той самий виняток для сесій: обмін refresh-токена теж відбувається
-- до вибору тенанта, бо саме сесія його й називає.
CREATE POLICY sessions_auth_lookup ON core.sessions
FOR SELECT
USING (core.current_tenant() IS NULL);
-- Створення й відкликання сесії — теж поза тенантом: у момент логіну
-- контексту ще немає, а при виході він уже не потрібен.
CREATE POLICY sessions_auth_write ON core.sessions
FOR INSERT
WITH CHECK (core.current_tenant() IS NULL OR tenant_id = core.current_tenant());
CREATE POLICY sessions_auth_revoke ON core.sessions
FOR UPDATE
USING (core.current_tenant() IS NULL OR tenant_id = core.current_tenant());
-- Членства читаються одразу після знаходження користувача, щоб
-- зрозуміти, до якого тенанта його пускати.
CREATE POLICY memberships_auth_lookup ON core.memberships
FOR SELECT
USING (core.current_tenant() IS NULL);
-- ---------------------------------------------------------------------
-- Індекси під шлях автентифікації
-- ---------------------------------------------------------------------
-- Живі сесії користувача — для сторінки «активні входи» й масового
-- відкликання при зміні пароля.
CREATE INDEX IF NOT EXISTS sessions_active_idx
ON core.sessions (user_id, expires_at DESC)
WHERE revoked_at IS NULL;
-- Прибирання протермінованих сесій фоновим завданням.
CREATE INDEX IF NOT EXISTS sessions_expiry_idx
ON core.sessions (expires_at)
WHERE revoked_at IS NULL;
-- ---------------------------------------------------------------------
-- Аудит входів
--
-- Окремо від core.audit_log: невдалі спроби входу треба рахувати ще до
-- того, як відомо, хто саме їх робить, а audit_log вимагає tenant_id.
-- Це ж джерело для блокування перебору.
-- ---------------------------------------------------------------------
CREATE TABLE core.login_attempts (
ts timestamptz NOT NULL DEFAULT now(),
id uuid NOT NULL DEFAULT core.new_id(),
email citext NOT NULL,
ip inet,
user_agent text,
success boolean NOT NULL,
-- bad_password | no_user | locked | mfa_failed
reason text,
user_id uuid,
PRIMARY KEY (ts, id)
);
SELECT create_hypertable('core.login_attempts', 'ts',
chunk_time_interval => INTERVAL '7 days', if_not_exists => TRUE);
CREATE INDEX login_attempts_email_idx ON core.login_attempts (email, ts DESC);
CREATE INDEX login_attempts_failed_idx ON core.login_attempts (ip, ts DESC)
WHERE NOT success;
SELECT add_retention_policy('core.login_attempts', INTERVAL '180 days', if_not_exists => TRUE);