У меня есть база данных PostgreSQL (9.4), которая ограничивает доступ к записям в зависимости от текущего пользователя и отслеживает изменения, сделанные пользователем. Это достигается с помощью представлений и триггеров, и по большей части это работает хорошо, но у меня возникают проблемы с представлениями, которые требуют INSTEAD OF
триггеров. Я пытался уменьшить проблему, но заранее прошу прощения, что это все еще довольно долго.
Ситуация
Все подключения к базе данных выполняются из веб-интерфейса через одну учетную запись dbweb
. После подключения роль изменяется с помощью SET ROLE
соответствия человеку, использующему веб-интерфейс, и все такие роли принадлежат групповой роли dbuser
. (Подробности см. В этом ответе ). Давайте предположим, что пользователь alice
.
Большинство моих таблиц размещены в схеме, к которой я здесь буду обращаться private
и которой они будут принадлежать dbowner
. Эти таблицы не доступны напрямую dbuser
, но предназначены для другой роли dbview
. Например:
SET SESSION AUTHORIZATION dbowner;
CREATE TABLE private.incident
(
incident_id serial PRIMARY KEY,
incident_name character varying NOT NULL,
incident_owner character varying NOT NULL
);
GRANT ALL ON TABLE private.incident TO dbview;
Доступность определенных строк для текущего пользователя alice
определяется другими представлениями. Упрощенный пример (который может быть уменьшен, но должен быть сделан таким образом, чтобы поддерживать более общие случаи):
-- Simplified case, but in principle could join multiple tables to determine allowed ids
CREATE OR REPLACE VIEW usr_incident AS
SELECT incident_id
FROM private.incident
WHERE incident_owner = current_user;
ALTER TABLE usr_incident
OWNER TO dbview;
Доступ к строкам затем предоставляется через представление, доступное для dbuser
таких ролей, как alice
:
CREATE OR REPLACE VIEW public.incident AS
SELECT incident.*
FROM private.incident
WHERE (incident_id IN ( SELECT incident_id
FROM usr_incident));
ALTER TABLE public.incident
OWNER TO dbview;
GRANT ALL ON TABLE public.incident TO dbuser;
Обратите внимание, что поскольку в предложении присутствует только одно отношение FROM
, этот вид представления можно обновлять без каких-либо дополнительных триггеров.
Для ведения журнала существует другая таблица для записи, какая таблица была изменена и кто ее изменил. Сокращенная версия:
CREATE TABLE private.audit
(
audit_id serial PRIMATE KEY,
table_name text NOT NULL,
user_name text NOT NULL
);
GRANT INSERT ON TABLE private.audit TO dbuser;
Это заполняется через триггеры, помещенные в каждое из отношений, которые я хочу отслеживать. Например, пример для private.incident
ограничения только вставками:
CREATE OR REPLACE FUNCTION private.if_modified_func()
RETURNS trigger AS
$BODY$
BEGIN
IF TG_OP = 'INSERT' THEN
INSERT INTO private.audit (table_name, user_name)
VALUES (tg_table_name::text, current_user::text);
RETURN NEW;
END IF;
END;
$BODY$
LANGUAGE plpgsql;
GRANT EXECUTE ON FUNCTION private.if_modified_func() TO dbuser;
CREATE TRIGGER log_incident
AFTER INSERT ON private.incident
FOR EACH ROW
EXECUTE PROCEDURE private.if_modified_func();
Так что теперь, если alice
вставить в public.incident
, запись ('incident','alice')
появляется в аудите.
Эта проблема
Этот подход сталкивается с проблемами, когда представления становятся более сложными и нуждаются в INSTEAD OF
триггерах для поддержки вставок.
Допустим, у меня есть два отношения, например, представляющих сущности, вовлеченные в некоторые отношения многие-к-одному:
CREATE TABLE private.driver
(
driver_id serial PRIMARY KEY,
driver_name text NOT NULL
);
GRANT ALL ON TABLE private.driver TO dbview;
CREATE TABLE private.vehicle
(
vehicle_id serial PRIMARY KEY,
incident_id integer REFERENCES private.incident,
make text NOT NULL,
model text NOT NULL,
driver_id integer NOT NULL REFERENCES private.driver
);
GRANT ALL ON TABLE private.vehicle TO dbview;
Предположим, что я не хочу раскрывать детали, кроме имени private.driver
, и поэтому имею представление, которое объединяет таблицы и проецирует биты, которые я хочу раскрыть:
CREATE OR REPLACE VIEW public.vehicle AS
SELECT vehicle_id, make, model, driver_name
FROM private.driver
JOIN private.vehicle USING (driver_id)
WHERE (incident_id IN ( SELECT incident_id
FROM usr_incident));
ALTER TABLE public.vehicle OWNER TO dbview;
GRANT ALL ON TABLE public.vehicle TO dbuser;
Для того, alice
чтобы иметь возможность вставить в это представление, должен быть предусмотрен триггер, например:
CREATE OR REPLACE FUNCTION vehicle_vw_insert()
RETURNS trigger AS
$BODY$
DECLARE did INTEGER;
BEGIN
INSERT INTO private.driver(driver_name) VALUES(NEW.driver_name) RETURNING driver_id INTO did;
INSERT INTO private.vehicle(make, model, driver_id) VALUES(NEW.make_id,NEW.model, did) RETURNING vehicle_id INTO NEW.vehicle_id;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql SECURITY DEFINER;
ALTER FUNCTION vehicle_vw_insert()
OWNER TO dbowner;
GRANT EXECUTE ON FUNCTION vehicle_vw_insert() TO dbuser;
CREATE TRIGGER vehicle_vw_insert_trig
INSTEAD OF INSERT ON public.vehicle
FOR EACH ROW
EXECUTE PROCEDURE vehicle_vw_insert();
Проблема с этим заключается в том, что SECURITY DEFINER
опция в функции триггера заставляет его запускаться с current_user
установленным значением dbowner
, поэтому, если alice
вставить в представление новую запись соответствующую запись в private.audit
записях, которыми должен быть автор dbowner
.
Итак, есть ли способ сохранить current_user
, не предоставляя dbuser
групповой роли прямой доступ к отношениям в схеме private
?
Частичное решение
Как предполагает Крейг, использование правил, а не триггеров, позволяет избежать изменения current_user
. Используя приведенный выше пример, вместо триггера обновления можно использовать следующее:
CREATE OR REPLACE RULE update_vehicle_view AS
ON UPDATE TO vehicle
DO INSTEAD
(
UPDATE private.vehicle
SET make = NEW.make,
model = NEW.model
WHERE vehicle_id = OLD.vehicle_id
AND (NEW.incident_id IN ( SELECT incident_id
FROM usr_incident));
UPDATE private.driver
SET driver_name = NEW.driver_name
FROM private.vehicle v
WHERE driver_id = v.driver_id
AND vehicle_id = OLD.vehicle_id
AND (NEW.incident_id IN ( SELECT incident_id
FROM usr_incident));
)
Это сохраняет current_user
. Поддерживающие RETURNING
пункты могут быть немного волосатыми, все же. Кроме того, я не смог найти безопасного способа использовать правила для одновременной вставки в обе таблицы, чтобы обрабатывать использование последовательности для driver_id
. Проще всего было бы использовать WITH
условие в INSERT
(КТР), но они не допускаются в сочетании с NEW
(ошибка: rules cannot refer to NEW within WITH query
), в результате чего один прибегать к lastval()
которой настоятельно не рекомендуется .
источник
SET SESSION
может быть даже лучше, но я думаю, что первоначальный пользователь должен иметь привилегии суперпользователя, что пахнет опасно.SET SESSION AUTHORIZATION
. Я действительно хочу что-то между этим иSET ROLE
, но на данный момент такого нет.Не полный ответ, но он не вписывается в комментарий.
lastval()
&currval()
Что заставляет вас думать, что
lastval()
не рекомендуется? Похоже на недоразумение.В ссылочном ответе Крэйг настоятельно рекомендует использовать триггер вместо правила в комментарии . И я согласен - за исключением вашего особого случая, очевидно.
Ответ настоятельно рекомендует использовать
currval()
- но это , кажется, misundertstanding. Там нет ничего плохогоlastval()
или, вернееcurrval()
. Я оставил комментарий с ссылочным ответом.Цитирование руководства:
Так что это безопасно с параллельными транзакциями. Единственное возможное осложнение может возникнуть из-за других триггеров или правил, которые могут вызывать один и тот же триггер непреднамеренно - что является очень маловероятным сценарием, и вы имеете полный контроль над тем, какие триггеры / правила вы устанавливаете.
Тем не менее , я не уверен, что последовательность команд сохраняется в правилах (хотя она и
currval()
является изменяемой функцией ). Кроме того, многорядныйINSERT
может вывести вас из синхронизации. Вы можете разделить свое ПРАВИЛО на два правила, только второе существоINSTEAD
. Помните, согласно документации:Я не расследовал дальше, вне времени.
DEFAULT PRIVILEGES
Что касается:
Вы можете быть заинтересованы вместо этого:
Связанные с:
источник
lastval
иcurrval
, поскольку я не понимал, что они были локальными для сеанса. Фактически я использую привилегии по умолчанию в моей реальной схеме, но для каждой таблицы они были от копирования и вставки из дампированной БД. Я пришел к выводу, что реструктурировать отношения легче, чем возиться с правилами, хотя они и аккуратны, поскольку позже я вижу, что они становятся головной болью.