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하지 않고 보존 할 수있는 방법이 있습니까? dbuserprivate

부분 솔루션

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에 절을 INSERT(CTE)하지만, 이러한이와 함께 사용할 수 없습니다 NEW(오류 : rules cannot refer to NEW within WITH query)에 리조트 하나를두고 lastval()하는 권장하지 .

답변:


4

따라서 current_userdbuser 그룹 역할에 스키마 개인의 관계에 직접 액세스하지 않고 보존 할 수있는 방법이 있습니까?

INSTEAD OF뷰를 통한 쓰기 액세스를 제공하기 위해 트리거 대신 규칙을 사용할 수 있습니다. 뷰는 항상 쿼리 사용자가 아닌 뷰 생성자의 보안 권한으로 작동하지만 변경 사항 은 없다고 생각 current_user 합니다.

애플리케이션이 사용자로 직접 연결되는 경우 session_user대신 대신 확인할 수 있습니다 current_user. 일반 사용자와 연결하는 경우에도 작동합니다 SET SESSION AUTHORIZATION. 그래도 일반 사용자로 연결 한 다음 SET ROLE원하는 사용자 에게 연결하면 작동하지 않습니다 .

SECURITY DEFINER함수 내에서 직전 사용자를 얻을 수있는 방법은 없습니다 . current_user과 만 얻을 수 있습니다 session_user. last_user사용자 ID 또는 스택 을 얻는 방법 은 좋지만 현재는 지원되지 않습니다.


Aha, 전에는 규칙을 다루지 않았습니다. 감사합니다. SET SESSION더 좋을 수도 있지만 초기 로그인 사용자에게는 수퍼 유저 권한이 있어야한다고 생각합니다.
beldaz

@beldaz 예. 의 큰 문제입니다 SET SESSION AUTHORIZATION. 나는 정말로 그 사이에 무언가를 원 SET ROLE하지만 지금은 그런 것이 없습니다.
Craig Ringer

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. 실제로는 실제 스키마에서 기본 권한을 사용하지만 테이블 당 권한은 덤프 된 DB에서 복사하여 붙여 넣는 것입니다. 나는 관계를 재구성하는 것이 규칙을 엉망으로 만드는 것보다 더 쉽다고 결론을 내렸다.
beldaz

@beldaz : 좋은 결정이라고 생각합니다. 디자인이 너무 복잡해졌습니다.
Erwin Brandstetter
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.