Перейти к содержанию

Row-Level Security как дополнительная защита

До этого изоляция зависела от правильных SQL-условий в каждом обработчике. Они остаются нужны: по ним человек видит смысл запроса. Теперь добавим защиту PostgreSQL, которая ограничит строки для роли приложения даже при забытом условии. Этот механизм называется Row-Level Security, сокращённо RLS.

В «Пространстве» политика сравнивает организацию строки с параметром текущей транзакции. Параметр назначает сервер после проверки членства. Политика не читает произвольный HTTP-заголовок. SQL расположен в sql/03-security.sql архива; его действие при подготовке не проверялось. Особенно важно отличать ожидаемое поведение роли приложения от поведения владельца схемы.

Отбор существующих и проверка новых строк

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

Ниже приведён полный фрагмент политики таблицы проектов. Аналогичные политики создаются для задач, приглашений, аудита и результатов экспорта. current_setting с аргументом true допускает отсутствие параметра, а nullif учитывает пустое значение после окончания локального состояния.

ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
ALTER TABLE projects FORCE ROW LEVEL SECURITY;
CREATE POLICY projects_tenant ON projects
FOR ALL TO space_app,space_worker
USING (organization_id = nullif(current_setting('app.organization_id',true),'')::uuid)
WITH CHECK (organization_id = nullif(current_setting('app.organization_id',true),'')::uuid);

Когда параметр отсутствует, сравнение с NULL не даёт разрешения. Ожидаемый SELECT роли приложения вернёт ноль строк, а INSERT будет отклонён политикой. Это полезнее значения «все компании по умолчанию». Если разработчик забыл открыть ограниченную транзакцию, он увидит отказ либо пустое чтение, а не общий массив клиентов.

Политика применяется к роли соединения базы. Она не различает Анну и Бориса сама по себе, поскольку оба используют space_app. Проверку личности и членства выполняет ASP.NET Core, а PostgreSQL проверяет область, которую сервер передал соединению. Это две согласованные части защиты, а не два независимых способа входа.

Кто может обходить политику

Согласно документации PostgreSQL 18 о RLS, суперпользователь и роль с BYPASSRLS обходят политики; владелец таблицы обычно тоже имеет особое поведение. FORCE ROW LEVEL SECURITY включает применение политики для владельца, но не превращает суперпользователя в обычного клиента.

Поэтому API не подключается как postgres и не владеет таблицами. Роль владельца применяется только для схемы. Если вручную выполнить запрос от суперпользователя и увидеть оба проекта, это не доказательство неработающей политики приложения. Если выполнить от суперпользователя и увидеть только один проект из-за собственного WHERE, это не доказательство работающей RLS.

Правильное наблюдение требует тех же прав, которые получает сервис. В пределах локальной учебной базы можно открыть транзакцию ролью space_app, назначить «Север» и выполнить SELECT без WHERE. Ожидаемо видны только северные строки. Затем завершить транзакцию и начать другую для «Юга»: результат должен измениться, не сохранив старый параметр.

Не выдавайте роли приложения разрешение изменять политику или владение таблицей ради устранения ошибки доступа. Такой «ремонт» отменяет защиту. Ошибка часто означает, что не назначен параметр, неверно открыта транзакция или отсутствуют обычные GRANT. RLS дополняет SQL-права и не заменяет разрешение SELECT либо INSERT.

Какие таблицы относятся к компании

Политика нужна не только основной таблице проектов. Аудит содержит сведения о действиях участников, экспорт — копию данных, приглашение — адрес человека. Если защитить проекты, но оставить общий список аудита, можно раскрыть названия объектов и состав клиентов через вспомогательную страницу.

Однако пользователи, организации и таблица разрешения членства имеют другой контракт. Сервер должен найти членство до назначения tenant-параметра. В учебном проекте разрешающий запрос читает минимальные данные этих таблиц явно, без общей выгрузки. Технический реестр токенов приглашения также содержит только маршрут к записи. Его нельзя превращать в новый публичный список компаний.

Фоновая очередь имеет две политики: приложение видит задания своей компании, worker может получить очередное задание всех компаний. После получения задания worker открывает отдельную ограниченную транзакцию для прикладных данных. Политика очереди не даёт ему безусловно читать проекты всех компаний. Такая разница между очередью и полезной нагрузкой будет разобрана отдельно.

Границы дополнительной защиты

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

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

Операции TRUNCATE не являются обычным отбором строк и не должны разрешаться роли приложения. Наши GRANT ограничиваются необходимыми действиями. Резервное копирование и обслуживание схемы используют отдельные права и отдельный процесс. Политика HTTP-данных не заменяет правила доступа к резервной копии, где находятся все компании.

Политики могут складываться шире ожидаемого

При добавлении второй permissive-политики PostgreSQL объединяет разрешения соответствующим способом, поэтому новая политика с USING(true) может расширить доступ роли. Нельзя добавлять её к projects только ради удобства worker. В нашем проекте такое исключение существует у очереди, где межорганизационный поиск необходим, и отсутствует у полезных данных. Проверяйте роли в TO каждого определения: политика для технической роли не должна случайно перечислять обычное приложение. Защита также не распространяется автоматически на представление или функцию с особыми правами. Если вы добавите такой объект, отдельно разберите, от чьего имени он читает таблицу и сохраняет ли ограничение. Отдельная функция SECURITY DEFINER без аккуратного контракта способна сделать новый путь обхода даже при правильно написанном tenant-условии основной таблицы.

Теперь забытый WHERE получает дополнительную защиту базы, а явные условия остаются читаемым контрактом. Следующий урок показывает параметр организации в транзакции и пуле соединений. Без правильного времени жизни параметра сама политика могла бы применить чужую область к следующему запросу.

Оглавление серии