Відстеження поточного користувача через перегляди та тригери в PostgreSQL


11

У мене є база даних 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()якого сильно не рекомендується .

Відповіді:


4

Отже, чи існує спосіб зберегти current_user, не надаючи ролі групи dbuser прямий доступ до відносин у схемі приватних?

Можливо, ви зможете використовувати правило, а не INSTEAD OFтригер, щоб забезпечити доступ для запису через перегляд. Перегляди завжди відповідають правам безпеки творця перегляду, а не користувачу, який запитує, але я не думаю, що current_user зміни.

Якщо ваша програма підключається безпосередньо як користувач, ви можете перевірити session_userзамість цього current_user. Це також працює, якщо ви підключитесь із загальним користувачем SET SESSION AUTHORIZATION. Це не спрацює, якщо ви підключитесь як загальний користувач, а потім SET ROLEдо потрібного користувача.

Немає можливості отримати безпосередньо попереднього користувача з SECURITY DEFINERфункції. Ви можете отримати тільки current_userі session_user. Спосіб отримання last_userабо стопки ідентифікацій користувачів був би непоганим, але наразі не підтримується.


Ага, раніше не займався правилами, дякую. SET SESSIONможе бути ще краще, але я думаю, що початковому користувачеві для входу потрібно мати привілеї суперпользователя, що пахне небезпечно.
beldaz

@beldaz Так. Це велика проблема SET SESSION AUTHORIZATION. Мені дуже хочеться чогось між цим і SET ROLE, але наразі такого немає.
Крейг Рінгер

1

Не повна відповідь, але це не впишеться в коментар.

lastval() & currval()

Що змушує вас lastval()дурити себе? Начебто непорозуміння.

У відповідній відповіді Крейг настійно рекомендує використовувати тригер замість правила в коментарі . І я згоден - окрім вашого особливого випадку, очевидно.

Відповідь настійно рекомендує використовувати currval()- але це , здається, misundertstanding. Існує нічого поганого lastval()або , вірніше currval(). Я залишив коментар із посиланням на відповідь.

Цитуючи посібник:

currval

Повернути значення, отримане останнім часом nextvalдля цієї послідовності в поточному сеансі. (Повідомляється про помилку, якщо nextvalв цій сесії ніколи не викликали цю послідовність.) Оскільки це повертає значення локального сеансу, воно дає передбачувану відповідь, чи виконувались інші сеанси nextvalз моменту поточного сеансу.

Тож це безпечно при одночасних транзакціях. Єдине можливе ускладнення може виникнути внаслідок інших тригерів або правил, які можуть ненавмисно викликати той самий тригер - що було б дуже малоймовірним сценарієм, і ви маєте повний контроль над тим, які тригери / правила встановлюєте.

Однак я не впевнений, що послідовність команд зберігається в межах правил (хоча currval()це є мінливою функцією ). Крім того, кілька рядків INSERTможе позбавити вас від синхронізації. Ви можете розділити своє правило на два правила, лише друге INSTEAD. Пам'ятайте, щодо документації:

Кілька правил для однієї таблиці та одного типу подій застосовуються в алфавітному порядку.

Я не досліджував далі, поза часом.

DEFAULT PRIVILEGES

Як для:

SET SESSION AUTHORIZATION dbowner;
...
GRANT ALL ON TABLE private.incident TO dbview;

Натомість вас може зацікавити:

ALTER DEFAULT PRIVILEGES FOR ROLE dbowner IN SCHEMA private
   GRANT ALL ON TABLES TO dbview;

Пов'язані:


Дякую, я дійсно помилявся в своєму розумінні lastvalі currval, оскільки не розумів, що вони місцеві для сесії. Насправді я використовую привілеї за замовчуванням у своїй реальній схемі, але ті, що стоять за столом, були від копіювання та вставки з скинутого БД. Я прийшов до висновку, що перебудову відносин простіше, ніж возитися з правилами, хоч вони є акуратними, оскільки я можу побачити, як вони пізніше болять у голові.
beldaz

@beldaz: Я думаю, що це гарне рішення. Ваша конструкція надто складна.
Ервін Брандстеттер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.