Отслеживание текущего пользователя через представления и триггеры в 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(КТР), но они не допускаются в сочетании с NEW(ошибка: rules cannot refer to NEW within WITH query), в результате чего один прибегать к lastval()которой настоятельно не рекомендуется .

beldaz
источник

Ответы:

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 Да. Это большая проблема с 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: я думаю, что это хорошее решение. Ваш дизайн становился слишком сложным.
Эрвин Брандстеттер