У мене є база даних 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
(CTE), але це не дозволено спільно з NEW
(error:) rules cannot refer to NEW within WITH query
, залишаючи одне вдатися до lastval()
якого сильно не рекомендується .
SET SESSION
може бути ще краще, але я думаю, що початковому користувачеві для входу потрібно мати привілеї суперпользователя, що пахне небезпечно.