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