Melacak pengguna saat ini melalui tampilan dan pemicu di PostgreSQL

11

Saya memiliki database PostgreSQL (9.4) yang membatasi akses ke catatan tergantung pada pengguna saat ini, dan melacak perubahan yang dibuat oleh pengguna. Ini dicapai melalui tampilan dan pemicu, dan sebagian besar ini berfungsi dengan baik, tapi saya mengalami masalah dengan tampilan yang memerlukan INSTEAD OFpemicu. Saya telah mencoba untuk mengurangi masalah, tetapi saya minta maaf sebelumnya bahwa ini masih cukup lama.

Situasi

Semua koneksi ke database dibuat dari front-end web melalui satu akun dbweb. Setelah tersambung, peran diubah melalui SET ROLEagar sesuai dengan orang yang menggunakan antarmuka web, dan semua peran tersebut menjadi milik peran grup dbuser. (Lihat jawaban ini untuk detailnya). Anggap pengguna itu alice.

Sebagian besar meja saya ditempatkan dalam skema yang di sini saya akan menelepon privatedan milik dbowner. Tabel ini tidak dapat diakses secara langsung dbuser, tetapi ke peran lain dbview. Misalnya:

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;

Ketersediaan baris tertentu untuk pengguna saat aliceini ditentukan oleh tampilan lain. Contoh yang disederhanakan (yang dapat dikurangi, tetapi perlu dilakukan dengan cara ini untuk mendukung kasus yang lebih umum) adalah:

-- 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;

Akses ke baris kemudian diberikan melalui tampilan yang dapat diakses oleh dbuserperan seperti 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;

Perhatikan bahwa karena hanya satu relasi yang muncul dalam FROMklausa, tampilan seperti ini dapat diperbarui tanpa pemicu tambahan.

Untuk masuk, ada tabel lain untuk merekam tabel mana yang diubah dan siapa yang mengubahnya. Versi yang dikurangi adalah:

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;

Ini diisi melalui pemicu yang ditempatkan pada masing-masing hubungan yang ingin saya lacak. Misalnya, contoh untuk private.incidentsisipan terbatas hanya:

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();

Jadi sekarang jika alicememasukkan public.incident, catatan ('incident','alice')muncul di audit.

Masalah

Pendekatan ini menimbulkan masalah ketika pandangan menjadi lebih rumit dan perlu INSTEAD OFpemicu untuk mendukung sisipan.

Katakanlah saya memiliki dua hubungan, misalnya mewakili entitas yang terlibat dalam beberapa hubungan banyak-ke-satu:

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;

Asumsikan bahwa saya tidak ingin mengekspos detail selain nama private.driver, dan juga memiliki pandangan yang bergabung dengan tabel dan memproyeksikan bit yang ingin saya paparkan:

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;

Agar alicedapat memasukkan ke dalam tampilan ini pemicu harus disediakan, misalnya:

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();

Masalah dengan ini adalah bahwa SECURITY DEFINERopsi dalam fungsi pemicu menyebabkannya dijalankan dengan current_userdiatur ke dbowner, jadi jika alicememasukkan catatan baru ke tampilan entri yang sesuai dalam private.auditcatatan penulis menjadi dbowner.

Jadi, apakah ada cara untuk melestarikan current_user, tanpa memberi dbuserperan langsung akses kelompok kepada hubungan dalam skema private?

Solusi Parsial

Seperti yang disarankan oleh Craig, menggunakan aturan dan bukannya pemicu menghindari mengubah current_user. Menggunakan contoh di atas, berikut ini dapat digunakan sebagai pengganti pemicu pembaruan:

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));               
   )

Ini mempertahankan current_user. RETURNINGKlausa pendukung bisa jadi sedikit berbulu. Selain itu, saya tidak menemukan cara aman untuk menggunakan aturan untuk secara bersamaan memasukkan ke dalam kedua tabel untuk menangani penggunaan urutan driver_id. Cara termudah adalah dengan menggunakan WITHklausa dalam INSERT(CTE), tetapi ini tidak diizinkan bersamaan dengan NEW(kesalahan rules cannot refer to NEW within WITH query:), meninggalkan satu untuk resor lastval()yang sangat tidak dianjurkan .

beldaz
sumber

Jawaban:

4

Jadi, apakah ada cara untuk melestarikan current_user, tanpa memberi akses langsung ke grup dbuser peran ke relasi dalam skema pribadi?

Anda mungkin dapat menggunakan aturan, alih-alih INSTEAD OFpemicu, untuk menyediakan akses tulis melalui tampilan. Tampilan selalu bertindak dengan hak keamanan pembuat tampilan daripada pengguna yang meminta, tapi saya tidak berpikir current_user perubahan.

Jika aplikasi Anda terhubung langsung sebagai pengguna, Anda dapat memeriksa session_useralih-alih current_user. Ini juga berfungsi jika Anda terhubung dengan pengguna umum SET SESSION AUTHORIZATION. Namun, itu tidak akan berfungsi jika Anda terhubung sebagai pengguna generik SET ROLEke pengguna yang diinginkan.

Tidak ada cara untuk mendapatkan pengguna langsung sebelumnya dari dalam suatu SECURITY DEFINERfungsi. Anda hanya bisa mendapatkan current_userdan session_user. Cara untuk mendapatkan last_useratau tumpukan identitas pengguna akan menyenangkan, tetapi saat ini tidak didukung.

Craig Ringer
sumber
Aha, belum pernah berurusan dengan aturan sebelumnya, terima kasih. SET SESSIONbisa lebih baik tapi saya pikir pengguna login awal harus memiliki hak superuser, yang baunya berbahaya.
beldaz
@ Beldaz Ya. Ini masalah besar SET SESSION AUTHORIZATION. Saya benar-benar menginginkan sesuatu di antaranya dan SET ROLE, tetapi saat ini tidak ada hal seperti itu.
Craig Ringer
1

Bukan jawaban yang lengkap, tetapi itu tidak akan cocok dengan komentar.

lastval() & currval()

Apa yang membuat Anda berpikir lastval()tidak bersemangat? Sepertinya kesalahpahaman.

Dalam jawaban yang dirujuk , Craig sangat menyarankan untuk menggunakan pemicu alih-alih aturan dalam komentar . Dan saya setuju - kecuali untuk kasus khusus Anda, jelas.

The jawabannya sangat melarang penggunaan currval()- tapi itu tampaknya misundertstanding a. Tidak ada yang salah dengan lastval()atau lebih tepatnya currval(). Saya meninggalkan komentar dengan jawaban yang dirujuk.

Mengutip manual:

currval

Kembalikan nilai yang terakhir diperoleh nextvaluntuk urutan ini di sesi saat ini. (Kesalahan dilaporkan jika nextvalbelum pernah dipanggil untuk urutan ini dalam sesi ini.) Karena ini mengembalikan nilai sesi-lokal, itu memberikan jawaban yang dapat diprediksi apakah sesi lain telah dijalankan atau tidak nextvalsejak sesi saat ini melakukannya.

Jadi ini aman dengan transaksi bersamaan. Satu-satunya komplikasi yang mungkin timbul dari pemicu lain atau aturan yang mungkin menyebut pemicu yang sama secara tidak sengaja - yang merupakan skenario yang sangat tidak mungkin dan Anda memiliki kontrol penuh atas pemicu / aturan yang Anda instal.

Namun , saya tidak yakin urutan perintah dipertahankan dalam aturan (meskipun currval()fungsi volatile ). Selain itu, multi-baris INSERTmungkin membuat Anda tidak sinkron. Anda dapat membagi ATURAN Anda menjadi dua aturan, hanya aturan kedua INSTEAD. Ingat, per dokumentasi:

Beberapa aturan pada tabel yang sama dan tipe acara yang sama diterapkan dalam urutan nama alfabet.

Saya tidak menyelidiki lebih lanjut, kehabisan waktu.

DEFAULT PRIVILEGES

Adapun:

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

Anda mungkin tertarik sebagai gantinya:

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

Terkait:

Erwin Brandstetter
sumber
Terima kasih, saya memang salah dalam pemahaman saya tentang lastvaldan currval, karena saya tidak menyadari mereka lokal untuk sesi. Saya memang menggunakan hak default dalam skema saya yang sebenarnya, tetapi yang per-tabel berasal dari menyalin dan menempel dari DB yang dibuang. Saya telah menyimpulkan bahwa merestrukturisasi hubungan lebih mudah daripada mengotak-atik aturan, meskipun rapi, karena saya bisa melihat mereka menjadi sakit kepala nanti.
beldaz
@ Beldaz: Saya pikir itu keputusan yang bagus. Desain Anda terlalu rumit.
Erwin Brandstetter