Почему не отдельная база на тенанта
Когда у LogiFlow стало 38 перевозчиков на платформе, база-на-тенанта означала бы 38 миграций на каждый релиз и пул соединений, который невозможно держать. Мы положили всех в один Postgres и изолировали через row-level security. Одна роль приложения, отдельные роли для миграций и админки — и никаких per-tenant пользователей в базе, они плодятся и ломают пулинг.
Грабли №1: владелец таблицы обходит RLS
RLS не применяется к суперюзеру и к владельцу таблицы. Если приложение ходит в базу под той же ролью, что владеет таблицами, — политики молча не работают. Мы поймали это на staging, когда один перевозчик увидел рейсы другого. Лечится так: приложение коннектится отдельной не-владельческой ролью, миграции — владельцем, и на каждую таблицу включён FORCE ROW LEVEL SECURITY.
ALTER TABLE loads ENABLE ROW LEVEL SECURITY;
ALTER TABLE loads FORCE ROW LEVEL SECURITY; -- иначе владелец обойдёт
CREATE POLICY tenant_isolation ON loads
USING (tenant_id = current_setting('app.tenant_id')::uuid);
-- приложение на каждый запрос:
SET app.tenant_id = '…'; -- НЕ владелец таблицыГрабли №2: политика без индекса — это скан
RLS меняет планы запросов: Postgres иначе оценивает число строк. Если на колонке из политики (`tenant_id`) нет индекса — получаете seq scan на каждый запрос. Мы прогнали EXPLAIN ANALYZE с включённым RLS, добавили индексы на колонки политик и заменили подзапросы в политиках на helper-функции. p95 упал с 340мс до 90мс — без единой смены железа.
- ▍Коннектись не-владельческой ролью, иначе политики молчат
- ▍FORCE ROW LEVEL SECURITY на каждой таблице
- ▍Индекс на каждой колонке, которая участвует в политике
- ▍Helper-функция вместо подзапроса в USING(...)
- ▍EXPLAIN ANALYZE на каждую политику до прода
«RLS требует тех же тестов, что и код: не проверил политику — она будет молчать ровно тогда, когда должна кричать.»
Что бы сделали иначе
Добавили бы в CI red-team тест с первого дня: запрос, который утверждает, что тенант A физически не может прочитать данные тенанта B. Мы дописали его после случая на staging — а должны были до первой строки кода.