Комментарии 25
Спасибо!
Рад, что материал оказался полезен. Если будете внедрять RLS у себя, делитесь фидбеком. Будет интересно обсудить, как решение покажет себя в ваших условиях
Нам только предстоит миграция АБС с Oracle на Postgres, и разработчик АБС уже предупреждает, что будет просадка производительности на 30%. Поэтому сейчас для нас любая информация об особенностях работы postgre цена :)
Postgres RLS, про который написл, - это решение скорее для динамичных SaaS-систем, где нужно масштабироваться на тысячи мелких клиентов без раздувания количества объектов в БД. В вашем случае на Postgres основная сложность будет даже не в безопасности, а в том, как оптимизатор справится со сложными планами запросов после Oracle. Просадка в 30% - это еще оптимистичный прогноз от разработчиков. Часто в таких кейсах приходится пересматривать индексы и структуру запросов с нуля. Будет интересно узнать, на какой архитектуре вы в итоге остановитесь. Удачи с миграцией!
А если в таблице сделать индекс по tenant_id, разве планировщик при такой политике RLS не станет его использовать при поиске по LIKE?
Как упоминал в разделе "Грабли №2", планировщик Postgres - параноик. Даже если есть индекс, без метки LEAKPROOF на операторе поиска он может выбрать Seq Scan, чтобы не "протечь" данными через индекс до проверки прав. Поэтому индекс на tenant_id - это база, но для LIKE/ILIKE часто нужен составной индекс или обертка в LEAKPROOF-функцию, чтобы заставить Postgres поверить, что поиск безопасен
По идее tenant_id должен быть в любом индексе
Вы имеете в виду обязательные составные индексы (tenant_id, column) для стабильного Index Scan? Или какой-то более хитрый кейс? На самом деле, тема индексов - это только вершина айсберга. Когда данных становится слишком много, приходится смотреть в сторону партиционирования по tenant_id, но это тянет на отдельный большой разбор (который, кстати, у меня почти уже готов)
вроде в цитусе (ставится как расширение) есть мультитенантность
Citus - это отличное решение для горизонтального масштабирования, когда данные измеряются терабайтами и шардинг становится неизбежен. Но это инфраструктурное усложнение, которое оправдано только при высоких нагрузках. В своей статье делал упор на логическую изоляцию в рамках стандартного Postgres. RLS позволяет гибко настраивать права доступа (например, разные роли внутри одного тенанта) без раздувания инфраструктуры и лишних затрат на поддержку кластера. Для большинства SaaS-задач на старте и этапе роста RLS выглядит, как более прагматичный выбор
Как теперь вывести данные по разным tenant_id в админке для менеджера?
Столько нюансов по настройке, которые легко пропустить. Проще было бы сделать обертку в коде, которая сама добавляет условие по tenant_id для заданных таблиц в from. Для этого лучше использовать ORM и QueryBuilder, а не сырые SQL-запросы. Или хотя бы ищет это условие в тексте запроса и бросает исключение если его нет.
3. Отдельная БД на клиента
Не знаю конечно ваши объемы , но если брать общий случай , то RLS, там где можно без него - это 100% ошибка проектирования.
Если у вас реально логически клиенты никак не связаны, и могут быть помещены в отдельную бд , то так и надо делать, это гораздо лучше чем RLS. Keep it simple , bro )
Производительность , возможности масштабирования и настройки - все в 100 раз лучше.
Легкие БД - в рамках одного инстанса , стали тяжелей - можно размазать на сколько нужно инстансов, на какую нужно инфру. Возможности масштабирования огромны.
Ваш подход идеально вписывается, когда есть клиенты с хорошими чеками, где затраты на клиента не превышают доходы от него. Это золотой стандарт индустрии, тут спорить никто не станет. А если у вас массовый SaaS с тысячами мелких клиентов, то проблемы изолированного подхода накатываются, как снежный ком. Раскатка миграций на 10 000 баз, управление пулами соединений и сбор сквозной аналитики превращаются в DevOps-ад, который стоит дороже, чем сэкономленные ресурсы. Считаю, что все зависит от бизнес-требований
все зависит от бизнес-требований
Логично, может это и оправданно в вашем случае, поэтому и про объемы упомянул.
Но в любом случае надо держать в голове , ваш подход - это пороховая бочка, которая к каждому(!) обращению добавляет index range scan по сути, и рано или поздно, это убъет производительность. Притом какой-нибудь самый важный и есс-но "жирный" по данным клиент - он будет ждать дольше всех.
Согласен, этот риск нужно держать в голове. RLS - это не "серебряная пуля". Но архитектура не статична. RLS позволяет нам эффективно жить на одной машине на старте и в фазе активного роста. А когда упрешься в этот потолок (или появится тот самый "жирный" клиент), то ваше решение подойдет идеально. У любого подхода есть свои ограничения
1. Логическая изоляция (WHERE в коде)?
Делал почти так же как у тебя. tenant_id хранился в таблице, а все запросы шли через вьюхи с таким же фильтром tenant_id = current_setting('app.current_tenant')::uuid. Все индексы были вида (tenant_id, id), а внешние ключи обязательно содержали tenant_id.
если на таблицы в отлельной схеме создать представления с select * from schem.tab where tenant_id = current_setting('app.current_tenant')::uuid, не давать привилегии на таблицы, дать на представления. Есть ли у RLS преимущества по сравнению с представлениями?
Представления работают, но требуют ручного обновления при изменении схемы таблиц, усложняют запись (триггеры) и оставляют риск утечки через забытую View. С RLS таких проблем не будет
цель - разобраться. Вы отлично описали RLS, статья хорошая. Вопросы по нужности использования самого RLS на практике. Может быть у view есть недостатки, о которых я не знаю и вы или кто-то подскажет. Возражения (не чтобы возразить, а чтобы выяснить - может я что-то не так понял):
требуют ручного обновления при изменении схемы таблиц
при изменении структуры таблицы менять view не надо, в нём “select *”.
усложняют запись (триггеры)
триггеры создавать не нужно - view простое.
оставляют риск утечки через забытую View
Если забыть создать view - доступа не будет, так как на схему хранения таблиц и на сами таблицы доступ не даётся. Если забыть удалить view - тоже не будет, так как если в таблице нет столбца tenant_id, то доступа и не будет. А если столбец в новой таблице есть, то значит строки предназначены для просмотра и чужих строк view не выдаст.
С RLS наоборот - создал таблицу, забыл добавить политику и всё - доступ есть ко всем строкам.
Postgres (и Linux) сходят с ума от такого количества открытых дескрипторов, а
VACUUMпревращается в ад
для точности: по умолчанию в Postgres max_files_per_processes=1000, в linux max_open_files=1024. Если файлов окрыто 1000, а процессу нужен доступ еще к одному файлу, процесс закрывает открытый файл. Это создает задержку и даже увеличивают лимиты, но при 50 таблицах не слишком критичную, так как таблицы небольшие, то fsync по файлам выполняется быстро.
Если забыть создать view - доступа не будет, так как на схему хранения таблиц и на сами таблицы доступ не даётся. Если забыть удалить view - тоже не будет, так как если в таблице нет столбца tenant_id, то доступа и не будет. А если столбец в новой таблице есть, то значит строки предназначены для просмотра и чужих строк view не выдаст.
С RLS наоборот - создал таблицу, забыл добавить политику и всё - доступ есть ко всем строкам.
Соглашусь с вашим тезисом, схема "нет View - нет доступа" действительно надежна. Тут я, пожалуй, погорячился, когда назвал это прямой утечкой. Основной риск здесь скорее в человеческом факторе при масштабировании. Если таблиц станет 100+, кто-то из разработчиков может допустить ошибку. RLS в этом плане кажется более монолитным решением. Но вы правы, убрать полностью человеческий фактор невозможно. Все же, проблема человеческого фактора с RLS легко решается одной проверкой в CI. А вот проверить View на корректность будет сложнее
триггеры создавать не нужно - view простое.
По поводу записи через триггеры, то да, для простых View они не нужны, но если логика усложнится, поддержка триггеров может добавить головной боли.
при изменении структуры таблицы менять view не надо, в нём “select *”.
По поводу SELECT *. Если правильно помню, звездочка раскрывается в список колонок именно в момент создания представления. То есть, если мы добавим колонку в таблицу позже, во View она автоматически не появится, и его придется пересоздавать. Но тут лучше сверится с докой Postgres
для точности: по умолчанию в Postgres max_files_per_processes=1000, в linux max_open_files=1024. Если файлов окрыто 1000, а процессу нужен доступ еще к одному файлу, процесс закрывает открытый файл. Это создает задержку и даже увеличивают лимиты, но при 50 таблицах не слишком критичную, так как таблицы небольшие, то fsync по файлам выполняется быстро
Кажется, вы упустили множитель в моем примере. Речь не о 50 таблицах, а о 500 000 (10к клиентов × 50 таблиц). Если всего 50 таблиц, то проблем не будет
По поводу SELECT *. Если правильно помню, звездочка раскрывается в список колонок именно в момент создания представления. То есть, если мы добавим колонку в таблицу позже, во View она автоматически не появится, и его придется пересоздавать. Но тут лучше сверится с докой Postgres
да, точно! это неудобство, хотя может это правильно с точки зрения безопасности или ещё по какой-то причине
Кажется, вы упустили множитель в моем примере. Речь не о 50 таблицах, а о 500 000 (10к клиентов × 50 таблиц). Если всего 50 таблиц, то проблем не будет
каждый клиент работает с 50 таблицами, то есть запрос одновременно обращается максимум к 50 таблицам. Если используется пулер, то для запроса другого прользователя серверный процесс часть файлов закроет, часть откроет. Проблема была бы, если одному запросу понадобилось бы больше 1000 файлов. Но понятно, что огромное число файлов в кластере баз данных хуже и решение с RLS в этом имеет плюсы, если размер таблиц получится не слишком большим.
То есть, в целом, можно рассматривать использование и RLS и view под конкретную задачу и выбрать то, что больше подходит

Как перестать писать WHERE tenant_id и отдать безопасность базе (PostgreSQL RLS в Go)?