Побочный эффект Postgres Trigger происходит не по порядку с политикой выбора безопасности на уровне строк - PullRequest
0 голосов
/ 29 сентября 2018

Контекст

Я использую безопасность на уровне строк вместе с триггерами для реализации реализации чистого 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. Политика INSERT WITH CHECK true оценивается как true
  5. Политика SELECT USING select * from b where a.id = b.id оценивается. Это должно вернуть true из-за шага 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)

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

  • Эксперимент с BEFORE против AFTER в определении триггера
    • AFTER приводит к тому, что триггер вообще не выполняет
  • Эксперимент с определением единой политики, которая применяется к ALL командам (с тем же самым использованием / с проверочным выражением)
    • приводит к тому же поведению

Приложение

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

1 Ответ

0 голосов
/ 01 октября 2018

После некоторых обсуждений с другими пользователями / разработчиками 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 одновременно.

...