Как заставить работать оператор INSERT RETURNING при использовании защиты на уровне строк (RLS)?

Ниже вы можете найти код минимального тестового примера для проблемы, с которой я сталкиваюсь в системе, где RLS используется для управления доступом к иерархической структуре данных. Я использую Postgres v11.

В коде у меня units, это объект верхнего уровня. units имеют subunits в отношении 1-n.

Есть также users, где user может иметь доступ к нескольким units через таблицу unit_owner.

Политики RLS предназначены для того, чтобы позволить user вставить новый subunits в units, которым он владеет.

Все это прекрасно работает, как видно до 2-й последней строчки кода.

НО вот моя проблема: Эта база данных предоставляется через промежуточное программное обеспечение GraphQL (Postgraphile), которому требуется результат вставки обратно с помощью функции INSERT ... RETURNING.

И, как видно из последнего оператора вставки, это не работает, возникает ОШИБКА: новая строка нарушает политику безопасности на уровне строк.

Проблема, по-видимому, возникает из-за того, что для RETURNING требуются права выбора, а функция политики выбора оценивается с использованием набора subunit идентификаторов, доступных до вставки, а не после.

Любые советы о том, как я могу позволить моим пользователям вставлять подразделения в свои устройства, будут оценены!

CREATE SCHEMA insert_returning;
CREATE ROLE users;

GRANT USAGE ON SCHEMA insert_returning TO users;

DROP TABLE IF EXISTS insert_returning.unit;
DROP TABLE IF EXISTS insert_returning.subunit;
DROP TABLE IF EXISTS insert_returning.unit_owner;

CREATE TABLE insert_returning.unit (
    id integer NOT NULL,
    description varchar NULL,
    CONSTRAINT unit_pk PRIMARY KEY (id)
);

CREATE TABLE insert_returning.subunit (
    id integer NOT NULL,
    unit_id integer NOT NULL,
    description varchar NULL,
    CONSTRAINT subunit_pk PRIMARY KEY (id)
);

CREATE TABLE insert_returning.unit_owner (
    user_id integer NOT NULL,
    unit_id integer NOT NULL
);

GRANT SELECT,INSERT,UPDATE ON TABLE insert_returning.unit TO users;
GRANT SELECT,INSERT,UPDATE ON TABLE insert_returning.subunit TO users;

GRANT SELECT ON TABLE insert_returning.unit_owner TO users;

CREATE OR REPLACE FUNCTION insert_returning.get_users_units()
RETURNS SETOF integer
LANGUAGE sql VOLATILE SECURITY DEFINER AS
$$
  SELECT uo.unit_id FROM insert_returning.unit_owner uo
    WHERE uo.user_id = 17;
$$;


CREATE OR REPLACE FUNCTION insert_returning.get_users_subunits()
RETURNS SETOF integer
LANGUAGE sql VOLATILE SECURITY DEFINER AS
$$
  SELECT s.id FROM insert_returning.subunit s
    JOIN insert_returning.unit_owner uo ON uo.unit_id = s.unit_id
    WHERE uo.user_id = 17;
$$;


ALTER TABLE insert_returning.unit ENABLE ROW LEVEL SECURITY;
ALTER TABLE insert_returning.subunit ENABLE ROW LEVEL SECURITY;

DROP POLICY IF EXISTS select_unit ON insert_returning.unit;
DROP POLICY IF EXISTS select_subunit ON insert_returning.subunit;
DROP POLICY IF EXISTS insert_subunit ON insert_returning.subunit;

CREATE POLICY select_unit ON insert_returning.unit FOR SELECT TO PUBLIC USING ((
    SELECT (id IN ( SELECT unit_id FROM insert_returning.unit_owner WHERE user_id = 17))
));

CREATE POLICY select_subunit ON insert_returning.subunit FOR SELECT TO PUBLIC USING ((
    SELECT (id IN (SELECT insert_returning.get_users_subunits()) )
));

CREATE POLICY insert_subunit ON insert_returning.subunit FOR INSERT TO PUBLIC WITH CHECK ((
    SELECT (unit_id IN (SELECT insert_returning.get_users_units()) )
));


INSERT INTO insert_returning.unit (id, description) VALUES (1, 'I am visible');
INSERT INTO insert_returning.unit (id, description) VALUES (2, 'I am hidden');

INSERT INTO insert_returning.subunit (id, unit_id, description) VALUES (1, 1, 'I belong to a visible unit');
INSERT INTO insert_returning.subunit (id, unit_id, description) VALUES (2, 2, 'I belong to a hidden unit');
INSERT INTO insert_returning.subunit (id, unit_id, description) VALUES (3, 1, 'I too belong to a visible unit');

INSERT INTO insert_returning.unit_owner (user_id,unit_id) VALUES (17,1);

SET ROLE users;

SELECT * FROM insert_returning.subunit; -- works

INSERT INTO insert_returning.subunit VALUES (4, 1, 'I am a new subunit'); -- works

INSERT INTO insert_returning.subunit VALUES (5, 1, 'I am another new subunit') RETURNING *; -- FAILS

--

person Jesper We    schedule 07.10.2020    source источник
comment
Почему вы используете id IN get_users_subunits() для SELECT политики вместо более простого unit_id in get_users_units()? Этот подход работал для политики INSERT, я думаю, его следует использовать и для политики SELECT.   -  person Bergi    schedule 07.10.2020
comment
:-) Конечно. Это сильно уменьшенная / минимизированная версия фактического кода, просто чтобы убедиться, что я публикую рабочий тестовый пример, имеющий отношение к проблеме. Никаких усилий, потраченных на то, чтобы сделать его лучше в чем-либо еще. В реальном коде алгоритм получения набора субъединиц, к которым имеет доступ пользователь, более сложен.   -  person Jesper We    schedule 07.10.2020
comment
Однако он по-прежнему страдает той же проблемой: политика выбора оценивается перед вставкой и, таким образом, не разрешает доступ для чтения к только что вставленной строке.   -  person Jesper We    schedule 07.10.2020
comment
Да, я так думал (и вопрос написан очень хорошо), но разве вы не можете сделать тот же трюк в своем реальном коде, чтобы использовать то же правило в политике выбора, что и в политике вставки?   -  person Bergi    schedule 07.10.2020
comment
Единственная альтернатива, о которой я могу думать, - это еще одна SECURITY DEFINER функция, которая работает с rls, проверяет разрешение вручную и возвращает идентификатор вставленной строки.   -  person Bergi    schedule 07.10.2020


Ответы (2)


Вы правильно проанализировали проблему: вставленная строка недоступна для подзапроса в политике FOR SELECT на subunit.

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

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

person Laurenz Albe    schedule 07.10.2020

Чтобы заставить его работать (и вы не видите способа изменить базовый RLS), вы можете создать пользовательская функция мутации, которую можно пометить как SECURITY DEFINER.

В этой функции мутации вам нужно будет выполнить проверку самостоятельно.

Это не отвечает на ваш вопрос относительно RLS, который, я думаю, уже получил надлежащий доступ в другом ответе на этот вопрос. Скорее всего, это подсказка другого пользователя Postgraphile.

Также:

  • По моему опыту, использование функций в RLS почти всегда снижает производительность. Особенно, если они не встроены. В вашем случае VOLATILE и SECURITY DEFINER должны уже предотвращать встраивание.

  • Почти всегда быстрее использовать EXISTS вместо IN в определениях RLS. Ваш опыт может отличаться.

person madflow    schedule 09.10.2020
comment
Спасибо за советы! - person Jesper We; 09.10.2020