Побочный эффект триггера Postgres возникает не в порядке с политикой выбора безопасности на уровне строк

Контекст

Я использую безопасность на уровне строк вместе с триггерами для реализации чистой реализации RBAC SQL. При этом я обнаружил странное поведение между INSERT триггерами и SELECT политиками безопасности на уровне строк.

Для простоты в остальной части этого вопроса проблема будет обсуждаться с использованием следующих упрощенных таблиц:

CREATE TABLE a (id TEXT);
ALTER TABLE a ENABLE ROW LEVEL SECURITY;
ALTER TABLE a FORCE ROW LEVEL SECURITY;

CREATE TABLE b (id TEXT);

Проблема

Обратите внимание на следующие политики и триггеры:

CREATE POLICY aSelect ON a FOR SELECT
USING (EXISTS(
    select * from b where a.id = b.id
));

CREATE POLICY aInsert ON a FOR INSERT
WITH CHECK (true);

CREATE FUNCTION reproHandler() RETURNS TRIGGER AS $$
BEGIN
    RAISE NOTICE USING MESSAGE = 'inside trigger handler';
    INSERT INTO b (id) VALUES (NEW.id);
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER reproTrigger BEFORE INSERT ON a
FOR EACH ROW EXECUTE PROCEDURE reproHandler();

Теперь рассмотрим следующее утверждение:

INSERT INTO a VALUES ('fails') returning id;

Мои ожидания основаны на чтении примененных политик по таблице типов команд и общее понимание SQL таково, что следующие вещи должны происходить по порядку:

  1. Новый ряд ('fails') поставлен для INSERT
  2. Триггер BEFORE срабатывает с NEW, установленным на новую строку
  3. Строка ('fails') вставляется в b и возвращается из процедуры триггера без изменений.
  4. WITH CHECK политика true INSERT оценивается как true
  5. Оценивается USING политика select * from b where a.id = b.id SELECT. Это должно вернуть истину согласно шагу 3
  6. После прохождения всех политик строка ('fails') вставляется в таблицу
  7. Возвращается идентификатор (fails) вставленной строки.

К сожалению (как вы уже догадались), вместо выполнения описанных выше шагов мы видим следующее:

test=> INSERT INTO a VALUES ('fails') returning id;
NOTICE:  inside trigger handler
ERROR:  new row violates row-level security policy for table "a"

Цель этого вопроса - выяснить, почему не происходит ожидаемого поведения.

Обратите внимание, что следующие операторы работали правильно, как и ожидалось:

test=> INSERT INTO a VALUES ('works');
NOTICE:  inside trigger handler
INSERT 0 1
test=> select * from a; select * from b;
  id   
-------
 works
(1 row)

  id   
-------
 works
(1 row)

Что я пробовал?

  • Experimented with BEFORE versus AFTER in the trigger definition
    • AFTER results in the trigger not executing at all
  • Experimented with defining a single policy which applies to ALL commands (with the same using/with check expression)
    • results in the same behavior

Приложение

  • Postgres Version
    • PostgreSQL 10.3 on x86_64-pc-linux-musl, compiled by gcc (Alpine 6.4.0) 6.4.0, 64-bit
  • Если вы попытаетесь воспроизвести проблему, убедитесь, что вы не работаете с разрешениями SUPER, так как это игнорирует безопасность строк

person Carl Sverre    schedule 29.09.2018    source источник


Ответы (1)


После нескольких разговоров с другими пользователями / разработчиками PostgreSQL в общем списке рассылки было определено, что эта конкретная проблема вызвана видимостью мутации в пределах одного оператора. . Особая благодарность Дину Рашиду за объяснение проблемы и предложение решения. Я обобщил его ответ здесь для сообщества Stack Overflow.

Таким образом, строка, вставленная триггером, не отображается последующим предложением EXISTS в политике SELECT безопасности на уровне строк, поскольку весь оператор выполняется в одном моментальном снимке PostgreSQL.

Один из способов обойти эту проблему - убедиться, что предложение EXISTS выполняется с новым моментальным снимком. Для этого в предложении EXISTS можно использовать функцию PostgreSQL с пометкой VOLATILE. Этот атрибут функции позволяет функции отслеживать изменения, внесенные в один и тот же оператор. Для получения дополнительной информации см. документацию. Соответствующий абзац извлечен здесь для справки:

Для функций, написанных на SQL или на любом из стандартных процедурных языков, существует второе важное свойство, определяемое категорией волатильности, а именно видимость любых изменений данных, которые были сделаны командой SQL, вызывающей функцию. Функция VOLATILE увидит такие изменения, а функция STABLE или IMMUTABLE - нет. Это поведение реализуется с использованием режима создания снимков в MVCC (см. Главу 13): функции STABLE и IMMUTABLE используют снимок, созданный в начале вызывающего запроса, тогда как функции VOLATILE получают новый снимок в начале каждого выполняемого запроса.

Итак, одно из решений этой проблемы - реализовать политику выбора RLS как функцию VOLATILE. Пример изменения политики:

CREATE OR REPLACE FUNCTION rlsCheck(_id text) RETURNS TABLE (id text) AS $$
    select * from b where b.id = _id
$$ LANGUAGE sql VOLATILE;

CREATE POLICY reproPolicySelect ON a FOR SELECT
USING (
    EXISTS(select * from rlsCheck(a.id))
);

В этом решении для каждой строки, проецируемой из таблицы a, потребуется, чтобы функция rlsCheck возвращала хотя бы одну строку. Эта функция будет запускаться с новым снимком для каждой спроецированной строки. Новый моментальный снимок, созданный при каждом вызове rlsCheck, позволит ему увидеть изменение таблицы b триггером INSERT в исходном примере.

Если вы внесете указанные выше изменения и запустите тест, вы увидите следующее поведение:

test=> select * from a;
id 
----
(0 rows)

test=> select * from b;
id 
----
(0 rows)

test=> insert into a values ('hi') returning id;
NOTICE:  inside trigger handler
id 
----
hi
(1 row)

INSERT 0 1

Такое поведение согласуется с моими ожиданиями, поэтому я принимаю это как ответ на проблему. К сожалению, эта функция приводит к недопустимому ограничению оптимизации во время выполнения запроса, поэтому я не буду использовать это в моей реализации RBAC. Я не верю, что возможно найти оптимизированное решение моей проблемы, поскольку выражение EXISTS в политике SELECT не может быть встроено и VOLATILE одновременно.

person Carl Sverre    schedule 01.10.2018