Theo dõi người dùng hiện tại thông qua lượt xem và trình kích hoạt trong PostgreSQL


11

Tôi có cơ sở dữ liệu PostgreSQL (9.4) giới hạn quyền truy cập vào các bản ghi tùy thuộc vào người dùng hiện tại và theo dõi các thay đổi được thực hiện bởi người dùng. Điều này đạt được thông qua các lượt xem và trình kích hoạt, và đối với hầu hết các phần này hoạt động tốt, nhưng tôi gặp vấn đề với các lượt xem yêu cầu INSTEAD OFtrình kích hoạt. Tôi đã cố gắng giảm vấn đề xuống, nhưng tôi xin lỗi trước rằng điều này vẫn còn khá dài.

Tình huống

Tất cả các kết nối đến cơ sở dữ liệu được thực hiện từ một giao diện người dùng web thông qua một tài khoản dbweb. Sau khi kết nối, vai trò được thay đổi thông qua SET ROLEđể tương ứng với người sử dụng giao diện web và tất cả các vai trò đó thuộc về vai trò nhóm dbuser. (Xem câu trả lời này để biết chi tiết). Giả sử người dùng là alice.

Hầu hết các bảng của tôi được đặt trong một lược đồ mà ở đây tôi sẽ gọi privatevà thuộc về dbowner. Các bảng này không thể truy cập trực tiếp dbuser, nhưng là một vai trò khác dbview. Ví dụ:

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;

Tính khả dụng của các hàng cụ thể cho người dùng hiện tại aliceđược xác định bởi các chế độ xem khác. Một ví dụ đơn giản hóa (có thể giảm bớt, nhưng cần được thực hiện theo cách này để hỗ trợ các trường hợp tổng quát hơn) sẽ là:

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

Truy cập vào các hàng sau đó được cung cấp thông qua chế độ xem có thể truy cập được với các dbuservai trò như 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;

Lưu ý rằng vì chỉ có một quan hệ xuất hiện trong FROMmệnh đề, nên loại quan điểm này có thể cập nhật mà không cần bất kỳ trình kích hoạt bổ sung nào.

Để ghi nhật ký, một bảng khác tồn tại để ghi lại bảng nào được thay đổi và ai đã thay đổi bảng đó. Một phiên bản rút gọn là:

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;

Điều này được phổ biến thông qua các kích hoạt được đặt trên mỗi mối quan hệ mà tôi muốn theo dõi. Chẳng hạn, một ví dụ cho private.incidentgiới hạn chỉ chèn là:

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

Vì vậy, bây giờ nếu alicechèn vào public.incident, một bản ghi ('incident','alice')xuất hiện trong kiểm toán.

Vấn đề

Cách tiếp cận này gặp vấn đề khi các chế độ xem trở nên phức tạp hơn và cần INSTEAD OFkích hoạt để hỗ trợ chèn.

Giả sử tôi có hai mối quan hệ, ví dụ đại diện cho các thực thể liên quan đến một số mối quan hệ nhiều-một:

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;

Giả sử rằng tôi không muốn để lộ các chi tiết khác so với tên private.driver, và do đó có một cái nhìn mà tham gia các bảng và các dự án các bit Tôi muốn để lộ:

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;

Để alicecó thể chèn vào chế độ xem này, một bộ kích hoạt phải được cung cấp, ví dụ:

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

Vấn đề với điều này là SECURITY DEFINERtùy chọn trong chức năng kích hoạt khiến nó được chạy với current_usercài đặt dbowner, vì vậy nếu alicechèn một bản ghi mới vào chế độ xem mục tương ứng trong private.auditbản ghi của tác giả dbowner.

Vì vậy, có cách nào để bảo tồn current_user, mà không cho dbuservai trò nhóm truy cập trực tiếp vào các mối quan hệ trong lược đồ private?

Giải pháp từng phần

Theo đề xuất của Craig, sử dụng các quy tắc thay vì kích hoạt sẽ tránh thay đổi current_user. Sử dụng ví dụ trên, có thể sử dụng các mục sau thay cho trình kích hoạt cập nhật:

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

Điều này bảo tồn current_user. RETURNINGMệnh đề hỗ trợ có thể là một chút lông, mặc dù. Hơn nữa, tôi không thể tìm thấy cách an toàn nào để sử dụng các quy tắc để chèn đồng thời vào cả hai bảng để xử lý việc sử dụng chuỗi cho driver_id. Cách dễ nhất có thể là sử dụng một WITHmệnh đề trong INSERT(CTE), nhưng những điều này không được phép kết hợp với NEW(lỗi rules cannot refer to NEW within WITH query:), khiến cho một điều khoản phải lastval()được khuyến khích mạnh mẽ .

Câu trả lời:


4

Vì vậy, có cách nào để bảo tồn current_user, mà không cho vai trò của nhóm dbuser truy cập trực tiếp vào các mối quan hệ trong lược đồ riêng tư không?

Bạn có thể sử dụng quy tắc, thay vì INSTEAD OFkích hoạt, để cung cấp quyền truy cập ghi thông qua chế độ xem. Lượt xem luôn hành động với quyền bảo mật của người tạo chế độ xem thay vì người dùng truy vấn, nhưng tôi không nghĩ current_user thay đổi.

Nếu ứng dụng của bạn kết nối trực tiếp với tư cách người dùng, bạn có thể kiểm tra session_userthay vì current_user. Điều này cũng hoạt động nếu bạn kết nối với một người dùng chung sau đó SET SESSION AUTHORIZATION. Tuy nhiên, nó sẽ không hoạt động nếu bạn kết nối như một người dùng chung với người dùng SET ROLEmong muốn.

Không có cách nào để có được người dùng trước ngay lập tức từ bên trong một SECURITY DEFINERchức năng. Bạn chỉ có thể có được current_usersession_user. Một cách để có được last_userhoặc một chồng danh tính người dùng sẽ tốt, nhưng hiện không được hỗ trợ.


Aha, đã không xử lý các quy tắc trước đây, cảm ơn. SET SESSIONcó thể tốt hơn nữa nhưng tôi nghĩ người dùng đăng nhập ban đầu sẽ cần phải có đặc quyền siêu người dùng, có mùi nguy hiểm.
beldaz

@beldaz Vâng. Đó là vấn đề lớn với SET SESSION AUTHORIZATION. Tôi thực sự muốn một cái gì đó giữa nó và SET ROLE, nhưng tại thời điểm này không có thứ đó.
Craig Ringer

1

Không phải là một câu trả lời hoàn chỉnh, nhưng nó sẽ không phù hợp với một nhận xét.

lastval() & currval()

Điều gì khiến bạn nghĩ lastval()là nản lòng? Có vẻ như một sự hiểu lầm.

Trong câu trả lời được tham chiếu , Craig đặc biệt khuyên bạn nên sử dụng kích hoạt thay vì quy tắc trong một nhận xét . Và tôi đồng ý - ngoại trừ trường hợp đặc biệt của bạn, rõ ràng.

Câu trả lời không khuyến khích mạnh mẽ việc sử dụng currval()- nhưng dường như đó là một sự hiểu lầm. Không có gì sai với lastval()hoặc đúng hơn currval(). Tôi đã để lại một bình luận với câu trả lời được tham khảo.

Trích dẫn hướng dẫn sử dụng:

currval

Trả về giá trị gần đây nhất thu được nextvalcho chuỗi này trong phiên hiện tại. (Một lỗi được báo cáo nếu nextvalchưa bao giờ được gọi cho chuỗi này trong phiên này.) Vì đây là trả về giá trị phiên cục bộ, nên nó đưa ra câu trả lời có thể dự đoán được liệu các phiên khác có thực hiện hay không nextvalkể từ phiên hiện tại.

Vì vậy, điều này là an toàn với các giao dịch đồng thời. Sự phức tạp duy nhất có thể xảy ra từ các trình kích hoạt hoặc quy tắc khác có thể vô tình gọi cùng một trình kích hoạt - đó sẽ là một tình huống rất khó xảy ra và bạn có toàn quyền kiểm soát các trình kích hoạt / quy tắc nào bạn cài đặt.

Tuy nhiên , tôi không chắc chuỗi lệnh được bảo toàn trong các quy tắc (mặc dù currval()là hàm dễ bay hơi ). Ngoài ra, nhiều hàng INSERTcó thể khiến bạn không đồng bộ. Bạn có thể chia RULE của bạn thành hai quy tắc, chỉ có điều thứ hai INSTEAD. Hãy nhớ rằng, mỗi tài liệu:

Nhiều quy tắc trên cùng một bảng và cùng loại sự kiện được áp dụng theo thứ tự tên bảng chữ cái.

Tôi đã không điều tra thêm, hết thời gian.

DEFAULT PRIVILEGES

Đối với:

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

Bạn có thể quan tâm thay thế:

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

Liên quan:


Cảm ơn, tôi thực sự đã sai trong cách hiểu của tôi lastvalcurrval, vì tôi không nhận ra họ là người địa phương trong một phiên. Trên thực tế tôi sử dụng các đặc quyền mặc định trong lược đồ thực của mình, nhưng các đặc quyền trên mỗi bảng là từ sao chép và dán từ DB bị đổ. Tôi đã kết luận rằng việc tái cấu trúc các mối quan hệ dễ dàng hơn so với các quy tắc, mặc dù chúng rất đơn giản, vì tôi có thể thấy chúng là một vấn đề đau đầu sau đó.
beldaz

@beldaz: Tôi nghĩ đó là một quyết định tốt. Thiết kế của bạn đã trở nên quá phức tạp.
Erwin Brandstetter
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.