RLS работает на таблице, но через view видны чужие строки: security_invoker и права, которые придётся выдать
Прямой SELECT из таблицы честно режет чужие строки, а тот же запрос через представление отдаёт всю базу целиком. Разработчик смотрит на политику RLS, политика правильная, и непонятно, где утечка. Я разберу, почему так устроено в PostgreSQL 18, как это чинится одним параметром view, почему сразу после починки прилетает permission denied на исходную таблицу, и какие права надо выдать, чтобы всё встало на место. Плюс покажу разбор из практики: в танцевальной студии «Танцпол-стиль» такая ошибка полтора месяца показывала администраторам одного зала оплаты и телефоны клиентов других залов.
Кто на самом деле смотрит в таблицу, когда вы делаете SELECT из view
Картина всегда одна и та же. На таблице включён RLS, политика написана аккуратно, разработчик заходит под ролью приложения, делает SELECT напрямую — видит только свои строки. Радуется. Потом тот же самый фильтр уезжает в отчёт, отчёт ходит не в таблицу, а в представление, и представление отдаёт всё. Первая реакция — «RLS сломался». RLS не сломался. Он просто применяется не к тому пользователю, о котором вы думаете.
В PostgreSQL права на базовые отношения обычного view проверяются для владельца представления, а не для того, кто выполняет запрос. Документация формулирует это прямо: «By default, access to the underlying base relations referenced in the view is determined by the permissions of the view owner». То есть view — это легальный, задокументированный и, вообще-то, очень полезный механизм делегирования прав: вы даёте человеку SELECT на представление и не даёте на таблицу. С RLS этот же механизм превращается в дыру, потому что политики берутся тоже владельца представления.
Дальше срабатывает вторая шестерёнка. Владелец view почти всегда совпадает с владельцем таблицы — это та роль, под которой катились миграции. А владелец таблицы штатно обходит row-level security: «Table owners normally bypass row security as well, though a table owner can choose to be subject to row security with ALTER TABLE ... FORCE ROW LEVEL SECURITY». Складываем два факта — и получаем ровно то, что видит разработчик: политики оценены для владельца, владелец их обходит, представление честно отдаёт все 900 тысяч строк.
Отсюда два независимых рычага, и оба надо дёрнуть. Первый — на таблице: FORCE ROW LEVEL SECURITY, чтобы владелец перестал быть исключением. Второй — на представлении: security_invoker = true, чтобы политики и права считались для вызывающего. Одного второго рычага обычно хватает, но первый я всё равно включаю: он закрывает случай, когда кто-то ходит в таблицу напрямую под ролью миграций.
- обычное view: права и политики базовых таблиц — владельца представления;
- security_invoker view: права и политики — вызывающего, «как если бы таблицы были указаны в запросе напрямую»;
- владелец таблицы обходит RLS, пока не включён FORCE ROW LEVEL SECURITY;
- суперпользователь и роль с атрибутом BYPASSRLS обходят RLS всегда — их этим не лечат.
Разбор: «Танцпол-стиль», четыре зала и отчёт, который показывал чужих клиентов
Клиент — танцевальная студия «Танцпол-стиль»: четыре зала в разных районах, 41 рабочее место, из них администраторы на ресепшенах, тренеры и небольшая бухгалтерия. Запись на занятия и продажа абонементов идут через самописную CRM на PostgreSQL 18 (минорные версии накатываем в квартальное окно), данные залов разделены по branch_id. Схема app, таблица app.payments — оплаты и списания абонементов за семь лет, около 900 тыс. строк, app.branches — справочник залов. Пришли с формулировкой «администратор зала на Академической видит в сводном отчёте клиентов других залов, но не всегда». Классика: чужие строки попадали только в отчёты за длинный период, а за день администраторы их просто не замечали.
Роли были разложены нормально: app_owner — владелец схемы и всех объектов, под ним катятся миграции; app_api — роль пула соединений CRM, LOGIN, без BYPASSRLS. Приложение в начале каждой транзакции делает SET LOCAL app.branch_id по залу, к которому привязан администратор. RLS включён, политика на месте, выглядела она так.
ALTER TABLE app.payments ENABLE ROW LEVEL SECURITY;
CREATE POLICY branch_isolation ON app.payments
FOR ALL TO app_api
USING (branch_id = current_setting('app.branch_id', true)::uuid)
WITH CHECK (branch_id = current_setting('app.branch_id', true)::uuid);
GRANT SELECT, INSERT, UPDATE, DELETE ON app.payments TO app_api;Проверка под app_api через прямой SELECT давала честные 230 тыс. строк своего зала вместо 900 тыс. А отчёт ходил в app.v_payments_summary — обычное представление, созданное миграцией, то есть принадлежащее app_owner. Владелец таблицы, FORCE RLS не включён, security_invoker не задан. Представление отдавало всё. Мы прогнали инвентаризацию по базе: из 9 представлений над таблицами с RLS security_invoker стоял ровно у двух (их разработчик делал руками уже на PostgreSQL 15+), у 7 не стоял. Три из этих семи были выведены в кабинет администратора, то есть реально показывали людям ФИО и телефоны клиентов чужих залов — а это уже персональные данные, а не просто неудобство.
Починка заняла минут сорок, и почти всё время ушло не на сам параметр, а на права: после включения security_invoker отчёт лёг с permission denied for table payments, потому что раньше app_api про таблицу вообще ничего не знал — ему хватало SELECT на view. Следом всплыл справочник app.branches, который представление подтягивало JOIN-ом: SELECT на него у app_api тоже не было. По итогу: 7 представлений переведены на security_invoker, выдано 5 GRANT SELECT на базовые таблицы и справочники плюс USAGE на схему, на двух таблицах включён FORCE ROW LEVEL SECURITY с отдельной политикой для app_owner. Утечка жила с момента выкатки отчёта — по истории миграций полтора месяца.
- симптом: прямой SELECT фильтрует по залу, тот же запрос через view — нет;
- причина: view принадлежит владельцу таблицы, владелец обходит RLS;
- цена вопроса: 7 представлений, 6 GRANT, один вечер работы;
- что нашли попутно: у роли отчётов для бухгалтерии стоял BYPASSRLS «чтобы выгрузки не падали» — сняли и дали ей явную политику на чтение всех залов.
Как включить security_invoker правильно
Параметр появился в PostgreSQL 15 — в релиз-нотах он записан как «Allow table accesses done by a view to optionally be controlled by privileges of the view's caller (Christoph Heiss)», и там же прямо сказано: «Previously, view accesses were always treated as being done by the view's owner. That's still the default». Обратите внимание на «That's still the default» — в 18-й версии поведение по умолчанию не изменилось, никакого автоматического ужесточения при апгрейде не произошло. Если вы приехали на 18 с 13-й или 14-й, все ваши старые представления по-прежнему работают от имени владельца.
Задаётся он двумя способами — при создании и на живом объекте. На живом объекте меняются только метаданные, перестройки данных нет, но ALTER VIEW берёт на представление блокировку и будет ждать, пока закончится тяжёлый отчёт, который его сейчас читает, — а новые запросы к view встанут в очередь за ним. Я делаю такие правки в окно и с lock_timeout, чтобы не поймать очередь.
-- при создании
CREATE VIEW app.v_payments_summary
WITH (security_invoker = true, security_barrier = true) AS
SELECT branch_id, date_trunc('month', created_at) AS m, count(*), sum(amount)
FROM app.payments
GROUP BY 1, 2;
-- на существующем
SET lock_timeout = '3s';
ALTER VIEW app.v_payments_summary SET (security_invoker = true);
-- вернуть как было
ALTER VIEW app.v_payments_summary RESET (security_invoker);Порядок действий, который я держу как шаблон, чтобы не уронить прод посреди правки: сначала выдать вызывающей роли права на базовые отношения и функции, потом переключить представление, потом проверить под ролью, потом включать FORCE RLS на таблицах. Если сделать наоборот — переключить view раньше, чем выданы GRANT, — вы получите отчёт, падающий с permission denied, ровно между двумя командами, и обязательно именно в этот момент кто-нибудь нажмёт «Сформировать».
Отдельно про FORCE ROW LEVEL SECURITY: после него владелец таблицы подчиняется политикам наравне со всеми. Если политика написана только TO app_api, для app_owner политик нет вовсе, и срабатывает default-deny — миграция с UPDATE по данным молча обновит ноль строк, а INSERT упадёт на проверке политики. Поэтому вместе с FORCE я сразу добавляю владельцу явную политику (например, отдельную CREATE POLICY ... TO app_owner USING (true)) или гоняю миграции данных под отдельной ролью, которой такая политика выдана осознанно.
- 1. GRANT SELECT (и, если надо, INSERT/UPDATE/DELETE) на базовые таблицы вызывающей роли;
- 2. сверить EXECUTE на функции из тела представления — это требование действует и без security_invoker (функции в view всегда вызываются от имени выполняющего запрос), но после REVOKE EXECUTE ... FROM PUBLIC про него часто забывают;
- 3. GRANT USAGE на схему, если роль в неё ещё не ходила;
- 4. ALTER VIEW ... SET (security_invoker = true);
- 5. проверка под ролью: SET ROLE app_api; SET app.branch_id = '...'; SELECT count(*) FROM app.v_payments_...;
- 6. ALTER TABLE ... FORCE ROW LEVEL SECURITY на таблицах с политиками.
Permission denied после переключения: какие права выдать и почему их стало больше
Это второй акт спектакля, и он пугает людей сильнее первого. Только что отчёт показывал лишнее — теперь он не показывает ничего и падает с ERROR: permission denied for table payments. Ощущение, что security_invoker всё сломал. На самом деле он ровно это и должен делать: документация прямым текстом требует, чтобы «the user of a security invoker view must have the relevant permissions on the view and its underlying base relations». Раньше прав на таблицу не было и не требовалось — их подставлял владелец. Теперь требуется.
Сколько именно прав выдать — вопрос, где начинается расхождение вкусов. Минималистичный подход: только SELECT и только на те таблицы, что реально участвуют в представлении. Ленивый подход: GRANT SELECT ON ALL TABLES IN SCHEMA app и забыть. Я за первый, но с оговоркой: без ALTER DEFAULT PRIVILEGES вы будете доставлять права руками после каждой миграции, которая создаёт таблицу. Поэтому у меня всегда есть строка про дефолтные привилегии.
GRANT USAGE ON SCHEMA app TO app_api;
GRANT SELECT ON app.payments, app.branches TO app_api;
GRANT EXECUTE ON FUNCTION app.fmt_period(date, date) TO app_api;
-- чтобы не бегать за каждой новой таблицей
ALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA app
GRANT SELECT ON TABLES TO app_api;Отдельно — обновляемые представления. Если через view идут INSERT/UPDATE/DELETE, вызывающей роли нужны эти же права на базовой таблице, а не только SELECT. И политика должна иметь WITH CHECK, иначе администратор одного зала сможет записать оплату с чужим branch_id: USING фильтрует то, что видно, WITH CHECK — то, что записывается. Если WITH CHECK не задан явно, он неявно повторяет USING — это удобно и в 90 % случаев то, что нужно, но полагаться на неявное поведение в коде, отвечающем за изоляцию данных, я не советую. Пишите обе части руками.
И проверяйте не глазами, а функцией. has_table_privilege врёт реже, чем память разработчика, а список того, что представление реально трогает, лучше брать из pg_depend, а не из чтения SQL-текста.
- SELECT — обязательно на все базовые таблицы, попавшие в тело view;
- INSERT/UPDATE/DELETE — если представление обновляемое;
- EXECUTE — на все функции, вызываемые из тела, включая пользовательские агрегаты (нужно и для обычных view, но проверьте, не отозван ли EXECUTE у PUBLIC);
- USAGE — на схему и на последовательности, если через view идут вставки с DEFAULT nextval();
- ALTER DEFAULT PRIVILEGES — чтобы права не отваливались после следующей миграции.
Ловушки, на которые я натыкался: барьер, утечки через функции, вложенность и pg_dump
Первая: security_invoker сам по себе не делает представление безопасным барьером. Это разные параметры про разные вещи. Барьерное view защищает от того, что планировщик протащит дешёвую пользовательскую функцию внутрь и вычислит её на строках, которые вы хотели скрыть. В документации есть каноничный пример с функцией tricky и COST 0.0000000000000000000001: «Every person and phone number in the phone_data table will be printed as a NOTICE, because the planner will choose to execute the inexpensive tricky function before the more expensive NOT LIKE». Если через ваше представление ходят пользователи, которые могут создавать свои функции, — ставьте security_barrier = true. Если в базу ходит только ваше приложение с фиксированным набором запросов, риск сильно преувеличен, и я бы не стал платить за него планами.
Потому что платить придётся. Документация прямо пишет, что индексный скан не может быть выбран для запросов к барьерным представлениям и таблицам с политиками RLS, если оператор из WHERE относится к семейству операторов индекса, но его функция не помечена LEAKPROOF; и что барьерные view «may perform far worse». Большинство простых операторов равенства уже LEAKPROOF, поэтому проблемы начинаются на пользовательских функциях и приведениях типов в условиях. У «Танцпол-стиля» после включения барьера на одном из отчётов выборка оплат за период ушла с 40 мс на 1,4 с — лечилось не отменой защиты, а тем, что условие по дате переписали без пользовательской функции-обёртки, и планировщик снова смог взять индекс.
Вторая ловушка — вложенные представления, о ней я уже сказал выше, но повторю, потому что именно на ней ловятся чаще всего: переключили верхнее view, проверили — работает, а промежуточное осталось обычным, и данные снова текут через него. Проверять надо по всей цепочке зависимостей, а не по одному объекту.
Третья — материализованные представления. Там параметра security_invoker нет вовсе: CREATE MATERIALIZED VIEW принимает storage-параметры от CREATE TABLE, и в этот список security_invoker не входит. Матвью — это, по сути, таблица со снимком данных, посчитанным при REFRESH. Если вам нужно разное содержимое для разных филиалов или клиентов, стройте RLS-политику на самом матвью-подобном хранилище (обычной таблице с branch_id) или не кладите в снимок то, что нельзя показывать всем.
Четвёртая — pg_dump и логика «выгрузим и посмотрим». Есть параметр row_security, по умолчанию on; когда он off, «queries fail which would otherwise apply at least one policy», и pg_dump выставляет его в off по умолчанию именно для того, чтобы не выгрузить молча половину таблицы. То есть если у вас бэкап делает роль без BYPASSRLS, дамп таблицы с политиками не «тихо усечётся» — он честно упадёт с ошибкой. Это хорошее поведение, но оно ломает наивные скрипты выгрузок, написанные до включения RLS. И обратите внимание на связку с FORCE: пока FORCE не включён, владелец таблицы обходит политики и его дамп проходит; после FORCE дамп под владельцем тоже упадёт. На сам row_security суперпользователи и роли с BYPASSRLS не реагируют — поэтому бэкапы обычно и делают под отдельной ролью с BYPASSRLS, которую не используют ни для чего другого.
- security_invoker ≠ security_barrier — включайте осознанно и по отдельности;
- RLS и барьер режут планы: следите за LEAKPROOF-операторами и за уходом в Seq Scan;
- цепочка view → view → таблица требует переключения каждого звена;
- у материализованных представлений security_invoker нет;
- бэкапы под ролью без BYPASSRLS упадут, а не выгрузят усечённые данные.
Как я это аудирую и как не даю сломаться обратно
Разовая починка ничего не стоит, если через два спринта миграция создаст новое представление без параметра. Поэтому у меня на такие базы всегда два артефакта: запрос-инвентаризация и регресс-тест, который гоняется в CI на каждой ветке. Инвентаризация отвечает на вопрос «где сейчас дыра», тест — «не появилась ли новая».
Инвентаризация ищет представления, которые опираются на таблицы с включённым RLS и при этом не имеют security_invoker. Опции представления лежат в pg_class.reloptions, зависимости — в pg_depend; читать текст view регулярками не надо, это ненадёжно. Учтите ограничение: запрос ниже видит только прямые зависимости, поэтому представление поверх другого представления, которое уже смотрит в RLS-таблицу, нужно проверять вторым проходом по найденным view.
SELECT DISTINCT v.oid::regclass AS view_name,
c.oid::regclass AS base_table,
coalesce(array_to_string(v.reloptions, ','), '(нет опций)') AS opts
FROM pg_class v
JOIN pg_rewrite r ON r.ev_class = v.oid
JOIN pg_depend d ON d.objid = r.oid AND d.refclassid = 'pg_class'::regclass
JOIN pg_class c ON c.oid = d.refobjid AND c.relkind IN ('r','p')
WHERE v.relkind = 'v'
AND c.relrowsecurity -- на базовой таблице включён RLS
AND c.oid <> v.oid
AND NOT EXISTS (SELECT 1 FROM pg_options_to_table(v.reloptions) o
WHERE o.option_name = 'security_invoker'
AND lower(o.option_value) IN ('true','on','1','yes'))
ORDER BY 1;Регресс-тест ещё проще и намного полезнее. Заводим два тестовых зала, вставляем по одной оплате в каждый, ходим под ролью приложения и требуем ровно одну строку в каждом представлении, торчащем наружу. Падает — сборка красная. Пишется за час, ловит потом годами.
SET ROLE app_api;
SET app.branch_id = '11111111-1111-1111-1111-111111111111';
SELECT count(*) = 1 AS ok_view FROM app.v_payments_summary;
SELECT count(*) = 1 AS ok_table FROM app.payments;
RESET ROLE;И ещё одна вещь, которую я делаю до всего остального, когда прихожу в чужую базу: проверяю, кто вообще обходит RLS. Суперпользователи и роли с BYPASSRLS видят всё, и никакие security_invoker их не касаются. Довольно часто оказывается, что приложение годами ходит в базу под ролью с rolsuper или rolbypassrls, и тогда обсуждать политики бессмысленно — сначала надо развести роли.
- SELECT rolname, rolsuper, rolbypassrls FROM pg_roles WHERE rolsuper OR rolbypassrls; — кто обходит RLS;
- SELECT relname, relrowsecurity, relforcerowsecurity FROM pg_class WHERE relrowsecurity; — где RLS включён и где форсирован;
- SELECT * FROM pg_policies WHERE schemaname = 'app'; — что за политики на самом деле применяются;
- \d+ app.v_payments_summary в psql — покажет опции представления без лазания в каталоги.
Что делать в первую очередь, а на что можно забить
Если у вас в одной базе живут данные нескольких филиалов или клиентов, разделённые RLS, и вы читаете это с мыслью «надо будет посмотреть» — не надо откладывать. Порядок приоритетов такой. Первое и срочное: прогнать запрос-инвентаризацию и посмотреть, есть ли представления над RLS-таблицами без security_invoker, которые доступны наружу через API или BI. Это буквально минута работы, и это единственный шаг, который может выявить активную утечку прямо сейчас. Второе: снять BYPASSRLS и суперюзера с ролей приложения — без этого остальное декоративно. Третье: включить FORCE ROW LEVEL SECURITY на таблицах с политиками, чтобы владелец перестал быть исключением из правил, — не забыв дать ему явную политику, иначе встанут миграции.
Что можно отложить без угрызений совести. security_barrier на представлениях, в которые ходит только ваш бэкенд с фиксированным набором запросов, — риск реальный, но эксплуатируется он только тем, кто может выполнять произвольный SQL и создавать функции. Если такого доступа у пользователей нет, ставьте барьер плановой задачей, а не ночным хотфиксом. Разбор LEAKPROOF-операторов и охоту за индексными сканами — тоже потом, сначала корректность, потом производительность. Переписывание представлений на функции SECURITY DEFINER с ручной проверкой — почти всегда лишнее усложнение, и я его не рекомендую.
И честно про спорное. RLS — не универсальный ответ на изоляцию арендаторов. Он даёт настоящую защиту на уровне СУБД, но стоит планов, усложняет отладку (запрос «не видит» строку, и вы полчаса ищете, почему) и требует дисциплины в правах. Есть команды, которым дешевле развести арендаторов по схемам или по базам — и они правы для своего масштаба. Но если вы уже выбрали RLS в одной таблице с branch_id, то делайте его целиком: политика на таблице без разбора представлений даёт ложное чувство защищённости, а это хуже, чем честное её отсутствие. Потому что от отсутствия защиты вы хотя бы не строите отчёты для клиентов.
Последнее наблюдение из практики. Ошибка с представлениями почти никогда не рождается вместе с системой — её приносит третий-четвёртый релиз, когда «нужен быстрый отчёт», а отчёт удобнее сделать view поверх готовых таблиц. RLS писал один человек полгода назад, view делает другой сегодня, и связь между ними нигде не зафиксирована. Так что помимо параметра в базе нужна ещё одна строчка в чек-листе ревью миграций: «создаёт ли этот PR представление над таблицей с RLS, и стоит ли там security_invoker». Стоит дешевле любого инцидента.
- срочно: инвентаризация view над RLS-таблицами; снять BYPASSRLS с ролей приложения;
- на этой неделе: security_invoker + GRANT + FORCE ROW LEVEL SECURITY, регресс-тест в CI;
- плановой задачей: security_barrier там, где пользователи пишут свой SQL; оптимизация планов;
- не делать: LEAKPROOF-пометки «для скорости», переписывание всего на SECURITY DEFINER.
Частые вопросы
С какой версии PostgreSQL доступен security_invoker?
С PostgreSQL 15 — параметр добавлен в CREATE VIEW и ALTER VIEW. В релиз-нотах 15.0 это записано в разделе Privileges с оговоркой, что прежнее поведение (доступ от имени владельца представления) остаётся значением по умолчанию. В PostgreSQL 18 умолчание не изменилось: при апгрейде старые представления продолжат работать правами владельца, автоматического ужесточения не будет.
Почему после включения security_invoker появился permission denied for table?
Потому что раньше права на базовую таблицу подставлял владелец представления, а теперь они проверяются у того, кто выполняет запрос. Документация требует, чтобы пользователь security-invoker view имел права и на само представление, и на базовые отношения. Выдайте роли GRANT USAGE на схему, GRANT SELECT (и DML, если view обновляемое) на базовые таблицы и проверьте EXECUTE на функции из тела представления — это требование действует и для обычных view, но его легко потерять после REVOKE ... FROM PUBLIC.
Достаточно ли включить security_invoker, или нужен ещё FORCE ROW LEVEL SECURITY?
Одного security_invoker хватает, чтобы закрыть путь через представление. Но владелец таблицы по-прежнему обходит RLS при прямом обращении, поэтому FORCE ROW LEVEL SECURITY я включаю всегда (вместе с явной политикой для роли миграций, иначе для неё сработает default-deny) — это закрывает случай, когда в базу ходят под ролью миграций, скриптом обслуживания или из BI под административной учёткой. Суперпользователя и роли с BYPASSRLS это не остановит — их надо просто не использовать для приложения.
Нужно ли одновременно ставить security_barrier?
Только если пользователи могут выполнять произвольный SQL и создавать свои функции: барьер защищает от того, что планировщик вычислит дешёвую вредоносную функцию раньше фильтра представления и та утечёт значения через NOTICE или текст ошибки. Если в базу ходит исключительно ваше приложение с фиксированным набором запросов, риск заметно преувеличен, а плата в планах реальна — барьер и RLS мешают выбрать индексный скан, когда операторы не помечены LEAKPROOF.
Работает ли security_invoker для материализованных представлений?
Нет. CREATE MATERIALIZED VIEW принимает storage-параметры из CREATE TABLE, и security_invoker в их числе нет. Матвью — это снимок данных, посчитанный при REFRESH, и содержимое снимка одинаково для всех, кто имеет на него SELECT. Если нужна изоляция по филиалам или клиентам, кладите снимок в обычную таблицу с branch_id и вешайте политику RLS уже на неё.
Как быстро проверить всю базу, а не по одному представлению?
Запросом по системным каталогам: соединить pg_class (relkind = 'v') с pg_rewrite и pg_depend, отобрать те представления, чьи базовые таблицы имеют relrowsecurity = true, и отфильтровать те, у кого в reloptions нет security_invoker=true. Готовый запрос есть в разделе про аудит. Разбирать текст view регулярками не стоит — зависимости надёжнее брать из каталога.
Источники
- PostgreSQL 18: CREATE VIEW — Параметр security_invoker, раздел Parameters и Notes («access to the underlying base relations ... determined by the permissions of the view owner», требование прав на view и базовые отношения, оговорка про CURRENT_USER и неэквивалентность SECURITY DEFINER): https://www.postgresql.org/docs/18/sql-createview.html
- PostgreSQL 18: ALTER VIEW — Синопсис ALTER VIEW ... SET (option = value) и RESET (...), перечень опций check_option, security_barrier, security_invoker: https://www.postgresql.org/docs/18/sql-alterview.html
- PostgreSQL 18: Row Security Policies — ENABLE/FORCE ROW LEVEL SECURITY, default-deny при отсутствии политик, обход RLS суперпользователем и ролями с BYPASSRLS, PERMISSIVE/RESTRICTIVE, USING и WITH CHECK: https://www.postgresql.org/docs/18/ddl-rowsecurity.html
- PostgreSQL 18: Rules and Privileges (39.5) — security_barrier, пример утечки через дешёвую функцию с COST 0.0000000000000000000001, запрет индексного скана при не-LEAKPROOF операторе для индексных сканов по барьерным view и таблицам с RLS: https://www.postgresql.org/docs/18/rules-privileges.html
- PostgreSQL 18: Client Connection Defaults — row_security — Параметр row_security, значение по умолчанию on, поведение при off («queries fail which would otherwise apply at least one policy») и его использование в pg_dump: https://www.postgresql.org/docs/18/runtime-config-client.html
- PostgreSQL 15 Release Notes — Появление security_invoker: «Allow table accesses done by a view to optionally be controlled by privileges of the view's caller (Christoph Heiss) ... That's still the default», раздел Privileges: https://www.postgresql.org/docs/release/15.0/
- PostgreSQL 18: CREATE POLICY — Политики для таблиц, на которые ссылается view, проверяются с правами и политиками владельца представления, кроме view с security_invoker; неявное использование USING как WITH CHECK для ALL/UPDATE; политики не применяются к проверкам ссылочной целостности: https://www.postgresql.org/docs/18/sql-createpolicy.html
- PostgreSQL 18: CREATE MATERIALIZED VIEW — Предложение WITH (storage_parameter) принимает параметры хранения из CREATE TABLE — опций представлений security_invoker/security_barrier там нет: https://www.postgresql.org/docs/18/sql-creatematerializedview.html
