web-dev-qa-db-ja.com

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

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

状況

データベースへのすべての接続は、単一のアカウントdbwebを介してWebフロントエンドから行われます。接続されると、ロールは_SET ROLE_を介してWebインターフェイスを使用するユーザーに対応するように変更され、そのようなロールはすべてグループロール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_user_をdbownerに設定して実行されるため、aliceが新しいレコードを_private.audit_の対応するエントリのビューは、作成者をdbownerとして記録します。

では、dbuserグループロールにスキーマprivateの関係への直接アクセス権を付与せずに、_current_user_を保持する方法はありますか?

部分的なソリューション

Craigが提案したように、トリガーではなくルールを使用すると、_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(CTE)でINSERT句を使用するのが最も簡単な方法ですが、NEWと組み合わせて使用​​することはできません(エラー:_rules cannot refer to NEW within WITH query_)、1つをlastval()に頼るようにします。これは 強く非推奨 です。

11
beldaz

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

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

アプリケーションがユーザーとして直接接続する場合は、session_userではなくcurrent_userを確認できます。これは、一般ユーザーに接続してからSET SESSION AUTHORIZATIONに接続した場合にも機能します。ただし、一般的なユーザーとして接続し、目的のユーザーにSET ROLEを接続すると機能しません。

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

4
Craig Ringer

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

lastval()currval()

lastval()が推奨されない理由は何ですか?誤解のようです。

参考回答 では、Craigはコメントでルールの代わりにトリガーを使用することを強くお勧めします。そして、私は同意します-あなたの特別な場合を除いて、明らかに。

answercurrval()の使用を強くお勧めしません-しかし、それは誤解のようです。 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;

関連:

1