7 мин

Row-level security в PostgreSQL для SaaS: рабочие политики

PostgreSQL RLS для SaaS помогает обеспечить изоляцию тенантов в базе данных. Узнайте, когда его использовать, как писать политики и чего избегать.

Row-level security в PostgreSQL для SaaS: рабочие политики

Настоящая проблема, которую RLS пытается решить в SaaS-приложениях

В SaaS-приложении самая опасная ошибка безопасности — та, которая проявляется после масштабирования. Вы начинаете с простого правила вроде «пользователи могут видеть только данные своего тенанта», затем быстро добавляете новый эндпоинт, добавляете отчётный запрос или вводите join, который незаметно пропускает проверку.

Авторизация только в приложении ломается под нагрузкой, потому что правила оказываются разбросаны по коду. Один контроллер проверяет tenant_id, другой проверяет членство, фоновая задача забывает, а путь «admin export» остаётся «временным» месяцами. Даже внимательные команды упускают место.

PostgreSQL row-level security (RLS) решает конкретную проблему: он заставляет базу данных контролировать, какие строки видимы для данного запроса. Ментальная модель простая: каждый SELECT, UPDATE и DELETE автоматически фильтруется политиками, как каждый запрос фильтруется middleware аутентификации.

Важно, что речь идёт о «строках». RLS не защитит всё:

  • Сам по себе он не скрывает отдельные колонки (используйте представления или привилегии на колонки).
  • Он не делает безопасными опасные функции (функция всё ещё может вытекать данными, если выполняется с повышенными правами).
  • Он не проверяет бизнес-правила (например, «только владельцы могут менять настройки биллинга»).

Конкретный пример: вы добавляете эндпоинт, который перечисляет проекты с join до invoice'ов для дашборда. При авторизации только в приложении легко отфильтровать projects по тенанту, но забыть отфильтровать invoices, либо сделать join по ключу, который пересекает тенанты. С RLS обе таблицы могут обеспечивать изоляцию тенантов, так что запрос безопасно не вернёт данных, вместо того чтобы их раскрыть.

Компромисс реальный. Вы пишете меньше повторяющегося кода авторизации и уменьшаете число мест, где возможна утечка. Но вы берёте на себя новую работу: политики нужно проектировать внимательно, тестировать рано и принимать то, что политика может заблокировать запрос, который вы ожидали работать.

Когда RLS упрощает авторизацию (и когда добавляет боли)

RLS может казаться лишней работой, пока ваше приложение не вырастет больше пары эндпоинтов. Если у вас строгие границы тенантов и много путей запросов (страницы списков, поиск, экспорты, админ-инструменты), запись правила в базе значит, что вам не нужно помнить о добавлении одного и того же фильтра везде.

RLS хорошо подходит, когда правило скучное и универсальное: «пользователь может видеть только строки своего тенанта» или «пользователь может видеть только проекты, в которых он состоит». В таких сценариях политики уменьшают ошибки, потому что каждый SELECT, UPDATE и DELETE проходит через один и тот же шлюз, даже если запрос появится позже.

Он также полезен для приложений с большим объёмом чтений, где логика фильтрации остаётся последовательной. Если ваш API имеет 15 разных способов загрузки инвойсов (по статусу, по дате, по клиенту, по поиску), RLS позволяет перестать переавторизовывать фильтрацию по тенанту в каждом запросе и сосредоточиться на функционале.

RLS добавляет боли, когда правила не основаны на строках. Правила по полям вроде «видно зарплату, но не бонус» или «маскировать колонку, если вы не HR» часто превращаются в неудобный SQL и плохо поддерживаемые исключения.

Это также тяжёлый вариант для обширной отчётности, которой действительно нужен широкий доступ. Команды часто создают обходные роли «только для этой задачи», и именно там накапливаются ошибки.

Прежде чем принимать решение, решите, хотите ли вы, чтобы база была финальным контролёром доступа. Если да — планируйте дисциплину: тестируйте поведение базы (не только ответы API), относитесь к миграциям как к изменениям безопасности, избегайте быстрых обходов, решите, как фоновые задания аутентифицируются, и держите политики маленькими и повторяемыми.

Если вы используете инструменты, генерирующие бэкенд, они могут ускорить доставку, но не снимают необходимости в ясных ролях, тестах и простой модели тенанта. (Например, Koder.ai использует Go и PostgreSQL для сгенерированных бэкендов, и вы всё равно захотите проектировать RLS осознанно, а не «подмешивать его потом».)

Базовые правила моделирования данных, которые упрощают политики RLS

RLS проще всего, когда ваша схема уже чётко говорит, кому что принадлежит. Если вы начинаете с размытой модели и пытаетесь «исправить это политиками», обычно получаете медленные запросы и запутанные баги.

Положите ключ тенанта там, где он нужен

Выберите один ключ тенанта (например, org_id) и используйте его последовательно. Большинство таблиц, принадлежащих тенанту, должны иметь его, даже если они ссылаются на другую таблицу, которая тоже его содержит. Это избегает джоинов внутри политик и держит проверки USING простыми.

Практическое правило: если строка должна исчезать, когда клиент отменяет подписку, ей, скорее всего, нужен org_id.

Моделируйте членства явно

Политики RLS обычно отвечают на вопрос: «Является ли этот пользователь членом этой организации, и что он может делать?» Это тяжело вывести из случайных колонок.

Держите основные таблицы маленькими и простыми:

  • users (по одной записи на человека)
  • orgs (по одной записи на тенант)
  • org_memberships (user_id, org_id, role, status)
  • опционально: project_memberships для доступа на уровне проекта

С этим набором политики могут проверять членство одним индексированным запросом.

Отделяйте общие справочные данные от данных, принадлежащих тенанту

Не всё должно иметь org_id. Справочные таблицы вроде стран, категорий продуктов или типов планов часто общие для всех тенантов. Сделайте их доступными только для чтения для большинства ролей и не привязывайте их к одному org.

Данные, принадлежащие тенанту (проекты, инвойсы, тикеты), должны избегать подтягивания тенант-специфичных деталей через общие таблицы. Делайте справочные таблицы минимальными и стабильными.

Внешние ключи, каскады и индексы

Внешние ключи работают с RLS, но удаления могут удивлять, если роль, выполняющая удаление, не «видит» зависимые строки. Планируйте каскады внимательно и тестируйте реальные сценарии удаления.

Индексируйте колонки, по которым фильтруют политики, особенно org_id и ключи членств. Политика, выглядящая как WHERE org_id = ..., не должна превращаться в полное сканирование таблицы, когда число строк достигает миллионов.

Как работают политики RLS, без страшилок

RLS — это переключатель на уровне таблицы. После включения PostgreSQL перестаёт доверять коду приложения в вопросе фильтрации тенанта. Каждый SELECT, UPDATE и DELETE фильтруется политиками, а каждый INSERT и UPDATE проверяется ими.

Крупный ментальный сдвиг: с включённым RLS запросы, которые раньше возвращали данные, могут начать возвращать ноль строк без ошибок. Это PostgreSQL, выполняющий контроль доступа.

Что политики реально делают

Политики — это небольшие правила, прикреплённые к таблице. Они используют два типа проверок:

  • USING — фильтр для чтения. Если строка не соответствует USING, она невидима для SELECT, и её нельзя целенаправленно обновить или удалить.
  • WITH CHECK — ворота для записи. Решает, какие новые или изменённые строки разрешены при INSERT или UPDATE.

Обычный SaaS-паттерн: USING гарантирует, что вы видите только строки вашего тенанта, а WITH CHECK гарантирует, что вы не сможете вставить строку в чужой тенант, подделав tenant_id.

PERMISSIVE против RESTRICTIVE в одной фразе

Когда вы добавляете больше политик, это важно:

  • PERMISSIVE (по умолчанию): строка разрешена, если любая политика позволяет её.
  • RESTRICTIVE: строка разрешена только если все restrictive-политики позволяют её (в дополнение к permissive-поведению).

Если вы планируете накладывать слой правил вроде соответствия тенанта + проверок ролей + членства в проекте, restrictive-политики могут прояснить намерение, но они также повышают риск «заблокировать себя», если вы забудете одно условие.

Как Postgres узнаёт, кто пользователь

RLS нуждается в надёжном значении «кто делает вызов». Распространённые варианты:

  • Сессионная переменная, установленная для запроса (например, app.user_id и app.tenant_id).
  • JWT-claims, смэппленные в сессионные настройки вашим API-слоем.
  • Смена роли (SET ROLE ...) на запросе, что работает, но добавляет операционных сложностей.

Выберите один подход и применяйте его везде. Смешение источников идентичности между сервисами — быстрый путь к запутанным багам.

Именуйте политики, чтобы потом было проще дебажить

Используйте предсказуемую конвенцию, чтобы дампы схем и логи были читабельны. Например: {table}__{action}__{rule}, вроде projects__select__tenant_match.

Шаг за шагом: добавьте RLS в одну таблицу и докажите, что это работает

Применяйте RLS к общим данным SaaS
Постройте CRM на основе тенантов и примените те же RLS-паттерны ко всем таблицам.

Если вы новичок в RLS, начните с одной таблицы и небольшого proof. Цель не в идеальном покрытии. Цель — заставить базу отказываться от кросс-тенантного доступа, даже когда в приложении есть баг.

Небольшая таблица для практики

Предположим простую таблицу projects. Сначала добавьте tenant_id так, чтобы это не сломало запись.

ALTER TABLE projects ADD COLUMN tenant_id uuid;

-- Backfill existing rows (example: everyone belongs to a default tenant)
UPDATE projects SET tenant_id = '11111111-1111-1111-1111-111111111111'::uuid
WHERE tenant_id IS NULL;

ALTER TABLE projects ALTER COLUMN tenant_id SET NOT NULL;

Далее, отделите собственника от роли API. Частый паттерн: одна роль владеет таблицами (app_owner), другая роль используется API (app_user). Роль API не должна быть владельцем таблиц, иначе она может обходить политики.

ALTER TABLE projects OWNER TO app_owner;
REVOKE ALL ON projects FROM PUBLIC;
GRANT SELECT, INSERT, UPDATE, DELETE ON projects TO app_user;

Теперь решите, как запрос сообщает Postgres, какой тенант он обслуживает. Один простой подход — установка сессионной настройки на протяжении транзакции. Ваше приложение устанавливает её сразу после открытия транзакции.

-- inside the same transaction as the request
SELECT set_config('app.current_tenant', '22222222-2222-2222-2222-222222222222', true);

Включите RLS и начните с правил чтения.

ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

CREATE POLICY projects_tenant_select
ON projects
FOR SELECT
TO app_user
USING (tenant_id = current_setting('app.current_tenant')::uuid);

Доказать это просто: попробуйте два разных тенанта и проверьте, что количество строк меняется.

Добавьте правила записи (WITH CHECK)

Политики чтения не защищают записи. Добавьте WITH CHECK, чтобы вставки и обновления не могли подбросить строки в чужой тенант.

CREATE POLICY projects_tenant_write
ON projects
FOR INSERT, UPDATE
TO app_user
WITH CHECK (tenant_id = current_setting('app.current_tenant')::uuid);

Быстрый способ проверить поведение (включая ошибки) — держать небольшой SQL-скрипт, который можно запускать после каждой миграции:

  • BEGIN; SET LOCAL ROLE app_user;
  • SELECT set_config('app.current_tenant', '\u003ctenant A\u003e', true); SELECT count(*) FROM projects;
  • INSERT INTO projects(id, tenant_id, name) VALUES (gen_random_uuid(), '\u003ctenant B\u003e', 'bad'); (должно упасть)
  • UPDATE projects SET tenant_id = '\u003ctenant B\u003e' WHERE ...; (должно упасть)
  • ROLLBACK;

Если вы можете запускать этот скрипт и получать одинаковые результаты каждый раз, у вас есть надёжная базовая линия перед распространением RLS на другие таблицы.

Шаблоны политик, которые вы будете переиспользовать в большинстве SaaS-приложений

Большинство команд обращаются к RLS, устав повторять одни и те же проверки авторизации в каждом запросе. Хорошая новость: формы политик обычно последовательны.

Строки «владельца» против строк «членства»

Некоторые таблицы естественно принадлежат одному пользователю (заметки, API-токены). Другие — принадлежат организации, где доступ зависит от членства. Обращайтесь с ними как с разными шаблонами.

Для данных, которыми владеет один пользователь, политики часто проверяют created_by = app_user_id(). Для данных организации политики часто проверяют наличие строки членства для этой организации.

Практический способ держать политики читаемыми — централизовать идентичность в небольших SQL-хелперах и переиспользовать их:

-- Example helpers
create function app_user_id() returns uuid
language sql stable as $$
  select current_setting('app.user_id', true)::uuid
$$;

create function app_is_admin() returns boolean
language sql stable as $$
  select current_setting('app.is_admin', true) = 'true'
$$;

Отделяйте правила чтения от правил записи

Чтение часто шире, чем запись. Например, любой член организации может SELECT проекты, но только редакторы могут UPDATE, а только владельцы — DELETE.

Держите это явно: одна политика для SELECT (членство), одна политика для INSERT/UPDATE с WITH CHECK (роль), и одна для DELETE (часто строже, чем update).

Админ-override без отключения RLS

Избегайте «выключить RLS для админов». Вместо этого добавляйте спасательный путь внутри политик, вроде app_is_admin(), чтобы случайно не дать полный доступ общей сервисной роли.

Soft delete и статусные флаги

Если вы используете deleted_at или status, включите это в политику SELECT (deleted_at is null). Иначе кто-то сможет «оживить» строки, меняя флаги, которые приложение считало финальными.

UPSERT: делайте WITH CHECK дружелюбным

INSERT ... ON CONFLICT DO UPDATE должен удовлетворять WITH CHECK для строки после записи. Если политика требует created_by = app_user_id(), убедитесь, что upsert задаёт created_by при вставке и не перезаписывает его при обновлении.

Если вы генерируете бэкенд-код, эти паттерны стоит превратить во внутренние шаблоны, чтобы новые таблицы сразу начинались с безопасных настроек, а не с пустого листа.

Частые подводные камни RLS, которые усложняют отладку

RLS хорош, пока одна мелочь не заставит его выглядеть так, будто PostgreSQL «случайно» прячет или показывает данные. Ошибки ниже тратят больше всего времени.

Ошибки, приводящие к тихим утечкам тенантов

Первая ловушка — забыть WITH CHECK на вставке и обновлении. USING контролирует видимость, а не то, что разрешено создавать. Без WITH CHECK баг в приложении может записать строку в чужой тенант, и вы можете не заметить этого, потому что тот же пользователь не сможет прочитать её.

Другая распространённая утечка — «leaky join». Вы правильно фильтруете projects, затем делаете join к invoices, notes или files, которые не защищены так же. Решение строгое, но прямое: каждая таблица, которая может раскрыть данные тенанта, нуждается в собственной политике, и представления не должны опираться только на одну «безопасную» таблицу.

Распространённые ошибки проявляются рано:

  • Правило чтения есть, но правило записи не содержит WITH CHECK.
  • Условие политики использует join к другой таблице, которая не защищена.
  • Доступ обеспечивается во взгляде (view), но подлежащая таблица остаётся открытой.
  • Вы полагаетесь на то, что «приложение всегда ставит tenant_id», а одна фоновая задача забывает.
  • Вы тестируете под суперпользователем, поэтому никогда не видите реального поведения.

Ошибки, делающие поведение запутанным

Политики, которые ссылаются на ту же таблицу (напрямую или через view), могут создать рекурсивные сюрпризы. Политика может проверять членство, запрашивая view, который читает защищаемую таблицу снова, что ведёт к ошибкам, медленным запросам или политике, которая никогда не матчится.

Настройка ролей — ещё один источник путаницы. Владельцы таблиц и роли с повышенными правами могут обходить RLS, поэтому ваши тесты проходят, а реальные пользователи терпят фейлы (или наоборот). Всегда тестируйте с той же низкоприоритетной ролью, которую использует ваше приложение.

Будьте осторожны с SECURITY DEFINER функциями. Они выполняются с привилегиями владельца функции, поэтому хелпер вроде current_tenant_id() может быть в порядке, но «удобная» функция, читающая данные, может случайно прочитать через тенанты, если её не спроектировать так, чтобы уважать RLS.

Также задайте безопасный search_path внутри security definer функций. Иначе функция может подобрать другой объект с тем же именем, и логика политики может молча указывать не туда в зависимости от сессионного состояния.

Отладка RLS: практические способы увидеть, что происходит

Запускайте основы мультиарендности быстрее
Преобразуйте модель тенанта в реальные таблицы, роли и эндпоинты без старта с нуля.

Баги RLS обычно связаны с отсутствием контекста, а не «плохим SQL». Политика может быть корректной на бумаге и всё равно не работать, потому что сессия использует другую роль, или потому что запрос никогда не установил значения тенанта и пользователя, от которых зависит политика.

Надёжный способ воспроизвести продакшен-репорт — смоделировать ту же сессию локально и выполнить точный запрос. Обычно это значит:

  • SET ROLE app_user; (или реальная роль API)
  • SELECT set_config('app.tenant_id', 't_123', true); и SELECT set_config('app.user_id', 'u_456', true);
  • Выполнить тот же SQL, что и приложение (включая параметры)
  • Подтвердить, что видит Postgres: SELECT current_user, current_setting('app.tenant_id', true), current_setting('app.user_id', true);

Если вы не уверены, какая политика применяется, смотрите в каталог, а не догадывайтесь. pg_policies показывает каждую политику, команду и выражения USING и WITH CHECK. Сопоставьте это с pg_class, чтобы убедиться, что RLS включён на таблице и не обходится.

Проблемы с производительностью могут выглядеть как проблемы авторизации. Политика, которая делает join к таблице членств или вызывает функцию, может быть корректной, но медленной, когда таблица растёт. Используйте EXPLAIN (ANALYZE, BUFFERS) на воспроизведённом запросе и ищите последовательные сканирования, неожиданные вложенные циклы или фильтры, применённые поздно. Отсутствие индексов на (tenant_id, user_id) и таблицах членств — частая причина.

Полезно логировать три значения для каждого запроса на уровне приложения: tenant ID, user ID и роль базы, используемую для запроса. Когда они не совпадают с тем, что вы думаете, RLS будет вести себя «неправильно», потому что входные данные неверны.

Для тестов держите несколько тестовых тенантов и делайте ошибки явными. Небольшой набор тестов обычно включает: «Tenant A не может читать Tenant B», «пользователь без членства не видит проект», «владелец может обновлять, зритель — нет», «вставка блокируется, если tenant_id не совпадает с контекстом», и «админ-овверрайд применяется только там, где задумано».

Быстрая предпродакшен-проверка для RLS

Относитесь к RLS как к ремню безопасности, а не к переключателю фичи. Маленькие промахи превращаются в «все видят данные всех» или «всё возвращает ноль строк».

Форма данных и политики

Убедитесь, что дизайн таблиц и правила политики соответствуют вашей модели тенанта.

  • Каждая таблица, принадлежащая тенанту, должна иметь явный ключ тенанта (обычно tenant_id). Если его нет — зафиксируйте причину (например, глобальные справочные таблицы).
  • Включите RLS на каждой таблице, принадлежащей тенанту, а не только на «главных». Если по каким-то путям нельзя допустить обхода, рассмотрите FORCE ROW LEVEL SECURITY для таких таблиц.
  • Разделите правила чтения и записи. Чтение — USING. Запись обязана иметь WITH CHECK, чтобы вставки и обновления не могли переместить строку в другой тенант.
  • Держите предикаты политик пригодными для индексации. Если политики фильтруют по tenant_id или делают join через таблицы членств, добавьте соответствующие индексы.

Простая проверка здравомыслия: пользователь из tenant A может читать свои инвойсы, может вставить инвойс только для tenant A и не может изменить tenant_id инвойса.

Роли, производительность и тесты

RLS сильна ровно настолько, насколько корректны роли, которыми пользуется приложение.

  • Убедитесь, что приложение никогда не подключается как суперпользователь, владелец таблицы или любая роль с bypassrls.
  • Прогоните несколько реальных запросов на данных, близких к продакшен-объему, и посмотрите планы.
  • Добавьте автоматизированные негативные тесты, которые доказывают, что кросс-тенантный доступ запрещён.

Пример: мультиарендное приложение проектов с доступом на основе членства

Начать с Postgres-ориентированного бэкенда
Получите чистый Go API и схему PostgreSQL, которую можно зафиксировать с помощью RLS.

Представьте B2B-приложение, где компании (orgs) имеют проекты, а проекты — задачи. Пользователи могут принадлежать нескольким организациям, а пользователь может быть участником некоторых проектов, но не всех. Это хороший кейс для RLS, потому что база может обеспечить изоляцию тенантов, даже если API-забывает фильтр.

Простая модель: orgs, users, org_memberships (org_id, user_id, role), projects (id, org_id), project_memberships (project_id, user_id), tasks (id, project_id, org_id, ...). Колонка org_id в tasks намеренна. Она упрощает политики и уменьшает сюрпризы при джоинах.

Классическая утечка происходит, когда у задач есть только project_id, а политика проверяет доступ через join к projects. Одна ошибка (слишком разрешающая политика на projects, join, который теряет условие, или view, меняющее контекст) может открыть задачи другой организации.

Более безопасный путь миграции, чтобы не сломать прод:

  • Сначала выкладывайте изменения схемы (добавьте org_id в tasks, добавьте таблицы членств).
  • Заполните tasks.org_id из projects.org_id, затем сделайте NOT NULL.
  • Добавьте политики, но держите RLS отключённым, пока тестируете в staging.
  • Включите RLS, затем принудите его, и только потом убирайте старые фильтры в приложении.

Доступ поддержки обычно лучше обрабатывать узкой ролью «break-glass», а не отключением RLS. Держите её отдельно от обычных аккаунтов поддержки и фиксируйте её использование.

Документируйте правила, чтобы политики не расходились: какие сессионные переменные должны быть установлены (user_id, org_id), какие таблицы должны нести org_id, что значит «член», и несколько SQL-примеров, которые должны возвращать 0 строк при запуске от имени чужой организации.

Следующие шаги: безопасный rollout RLS и поддерживаемость

RLS проще жить, если относиться к нему как к изменению продукта. Внедряйте по частям, доказывайте поведение тестами и держите чёткую запись причин каждой политики.

План отката, который обычно работает:

  • Начните с одной таблицы с явной принадлежностью (например, projects) и зафиксируйте её.
  • Добавьте тесты, покрывающие разрешённые и запрещённые чтения и записи для нескольких ролей (владелец, участник, посторонний).
  • Расширяйтесь партиями (одна область функционала за раз), которые вы сможете отладить в одной сессии.
  • Мониторьте ошибки прав доступа во время rollout и выкатывайте изменения в низкорискованное окно.

После стабилизации первой таблицы делайте изменения политик осознанно. Добавьте шаг ревью политик в миграции и прикладывайте краткую заметку о назначении (кто должен иметь доступ к чему и почему) плюс соответствующее обновление тестов. Это предотвращает «просто добавь ещё OR» политики, которые постепенно превращаются в дыру.

Если вы двигаетесь быстро, инструменты вроде Koder.ai (koder.ai) могут помочь сгенерировать Go + PostgreSQL стартовую точку через чат, а затем вы сможете наложить политики RLS и тесты с той же дисциплиной, что и в ручном бэкенде.

Наконец, держите страховочные меры при rollout: снимайте снапшоты перед миграциями политик, отрабатывайте откаты, пока это не станет скучным, и держите узкий break-glass путь для поддержки, который не отключает RLS для всей системы.

FAQ

Какую проблему безопасности реально решает RLS в SaaS-приложении?

RLS заставляет PostgreSQL контролировать, какие строки видимы или доступны для записи в рамках запроса, поэтому изоляция тенантов не зависит от того, что каждый эндпоинт помнит фильтр WHERE tenant_id = .... Главное преимущество — уменьшение числа багов «одна пропущенная проверка», которые появляются по мере роста приложения и множества запросов.

Когда RLS стоит потраченных усилий?

Это оправдано, когда правила доступа единообразны и применимы к строкам — например, изоляция по тенанту или доступ на основе членства — и у вас много путей выборки данных (поиск, экспорт, админки, фоновые задания). Обычно это нецелесообразно, если правила в основном поколоночные, сильно исключаемые или доминируют отчёты, которым нужны кросс-тенантные чтения.

От чего RLS меня НЕ защитит?

RLS защищает видимость строк и базовую проверку записей. Конфиденциальность колонок обычно требует представлений и прав на колонки, а сложные бизнес-правила (например, владение биллингом или согласовательные процессы) всё ещё нужно реализовывать в логике приложения или через грамотно спроектированные ограничения БД.

Как безопаснее всего начать использовать RLS, если я новичок?

Создайте роль с низкими привилегиями для API (не владелец таблиц), включите RLS, затем добавьте политику SELECT и политику INSERT/UPDATE с WITH CHECK. Установите сессионную переменную для запроса, например app.current_tenant, и проверьте, что при её переключении меняются видимые и записываемые строки.

Как приложению сообщить Postgres, какой тенант и пользователь выполняют запрос?

Обычный вариант — сессионные переменные, задаваемые на протяжении запроса (например, app.tenant_id и app.user_id). Главное — последовательность: все пути (веб-запросы, фоновые задачи, скрипты) должны устанавливать те же значения, которые ожидают политики, иначе вы получите запутанное поведение «нулевых строк».

В чём разница между USING и WITH CHECK в политике RLS?

USING контролирует, какие существующие строки видимы и могут быть целью для SELECT, UPDATE и DELETE. WITH CHECK контролирует, какие новые или изменённые строки допустимы при INSERT и UPDATE, поэтому он предотвращает «запись в чужой тенант», даже если приложение передало неверный tenant_id.

Почему все повторяют «не забудьте WITH CHECK»?

Если добавить только USING, баг в эндпоинте всё ещё может вставить или обновить строку в чужой тенант, и вы можете этого не заметить, потому что тот же пользователь не сможет прочитать ошибочную строку. Всегда сопоставляйте правило чтения с соответствующим WITH CHECK для записи, чтобы неподходящие данные не могли появиться изначально.

Как спроектировать схему, чтобы политики RLS оставались простыми и быстрыми?

Избегайте джоинов внутри условий политик: положите ключ тенанта (например, org_id) прямо в таблицы, принадлежащие тенанту, даже если у них есть ссылка на другую таблицу с тем же ключом. Добавьте явные таблицы членств (org_memberships, опционально project_memberships), чтобы политики делали один индексированный запрос вместо сложного вывода.

Как отлаживать «RLS прячет мои данные», не гадя?

Сначала воспроизведите тот же сессионный контекст, который использует приложение: установите ту же роль и сессионные настройки, затем выполните точный SQL-запрос. Проверьте, включён ли RLS и загляните в pg_policies, чтобы увидеть USING и WITH CHECK выражения — чаще всего проблема в отсутствии правильно установленных идентификационных значений, а не в «плохом SQL».

Если я генерирую бэкенд (например с помощью Koder.ai), нужно ли всё равно тщательно проектировать RLS?

Да, но относитесь к сгенерированному коду как к отправной точке, а не к системе безопасности. Если вы используете Koder.ai для генерации Go + PostgreSQL бэкенда, вам всё равно нужно определить модель тенанта, последовательно задавать сессионную идентичность и явно добавлять политики и тесты, чтобы новые таблицы не выпускались без нужной защиты.

Похожие статьи