PostgreSQLのビューとトリガーを通じて現在のユーザーを追跡する


11

現在のユーザーに応じてレコードへのアクセスを制限し、ユーザーが行った変更を追跡するPostgreSQL(9.4)データベースがあります。これはビューとトリガーによって達成され、ほとんどの場合これはうまく機能しますが、INSTEAD OFトリガーを必要とするビューに問題があります。私は問題を減らすように努めましたが、これがまだかなり長いことを前もってお詫びします。

状況

データベースへのすべての接続は、単一のアカウントを介してWebフロントエンドから行われますdbweb。接続されると、SET ROLEWebインターフェースを使用するユーザーに対応するように役割が変更され、そのような役割はすべてグループ役割に属し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句には1つのリレーションのみが表示されるため、この種のビューはトリガーを追加しなくても更新できます。

ロギングの場合、どのテーブルが変更され、誰が変更したかを記録する別のテーブルが存在します。削減バージョンは次のとおりです。

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挿入をサポートするためのトリガーが必要になると問題にぶつかります。

たとえば、多対1の関係に関与するエンティティを表す2つの関係があるとします。

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_usersetが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(エラー:rules cannot refer to NEW within WITH query)に頼るものを残して、lastval()されて強く反対

回答:


4

では、current_userスキーマプライベートの関係への直接アクセスをdbuserグループロールに与えずに、を保持する方法はありますか?

INSTEAD OFトリガーではなくルールを使用して、ビューを介した書き込みアクセスを提供できる場合があります。ビューは常に、クエリを実行するユーザーではなく、ビュー作成者のセキュリティ権限で動作しますが、変更はないと思い current_userます。

アプリケーションがユーザーとして直接接続する場合は、のsession_user代わりにチェックできますcurrent_user。これは、一般ユーザーと接続した場合にも機能しますSET SESSION AUTHORIZATIONSET ROLEただし、汎用ユーザーとして接続してから目的のユーザーに接続すると機能しません。

SECURITY DEFINER関数内から直前のユーザーを取得する方法はありません。current_userとのみを取得できsession_userます。last_userまたはユーザーIDのスタックを取得する方法は便利ですが、現在サポートされていません。


ああ、以前はルールを扱ったことがなかったので、ありがとう。SET SESSIONさらに良いかもしれませんが、最初のログインユーザーはスーパーユーザー特権を持っている必要があると思います。
beldaz 2015

@beldazうん。これはの大きな問題SET SESSION AUTHORIZATIONです。との間に本当に何かが欲しいのですSET ROLEが、現時点ではそのようなことはありません。
クレイグリンガー、

1

完全な回答ではありませんが、コメントには収まりません。

lastval()currval()

lastval()が落胆していると思いますか?誤解のようです。

参照さ答え、クレイグは強くして、ルールの代わりにトリガーを使用することを推奨していますコメント。そして、私は同意します-あなたの特別な場合を除いて、明らかに。

その答えはの使用を強くお勧めしませんcurrval()が、それは誤解されているようです。というlastval()か何も悪いことはありませんcurrval()。参照された回答とともにコメントを残しました。

マニュアルの引用:

currval

nextval現在のセッションでこのシーケンスに対して最後に取得された値を返します。(nextvalこのセッションでこのシーケンスに対して一度も呼び出されていない場合はエラーが報告されます。)これはセッションローカル値を返すnextvalため、現在のセッションが実行されてから他のセッションが実行されたかどうかにかかわらず、予測可能な回答を提供します。

したがって、これは並行トランザクションで安全です。起こり得る唯一の複雑さは、同じトリガーを誤って呼び出す可能性のある他のトリガーまたはルールから発生する可能性があります。これは非常にまれなシナリオであり、インストールするトリガー/ルールを完全に制御できます。

ただしコマンドシーケンスがcurrval()揮発性関数としてであってもルール内で保持されるかどうかはわかりません。また、複数行INSERTを使用すると同期が取れなくなる可能性があります。ルールを2つのルールに分割できますが、2番目のルールのみ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;

関連:


おかげで、私はの私の理解では確かに間違っていたlastvalcurrval私は、彼らがセッションにローカルで実現しなかったとして、。実際、実際のスキーマではデフォルトの特権を使用していますが、テーブルごとの特権は、ダンプされたDBからのコピーと貼り付けによるものでした。後で頭痛の種となるので、ルールをいじるよりも関係を再構築する方が簡単です。
beldaz

@beldaz:それは良い決断だと思います。デザインが複雑になりすぎました。
Erwin Brandstetter 2015
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.