Rastreando o usuário atual através de visualizações e gatilhos no PostgreSQL


11

Eu tenho um banco de dados PostgreSQL (9.4) que limita o acesso aos registros, dependendo do usuário atual, e rastreia as alterações feitas pelo usuário. Isso é conseguido através de visualizações e gatilhos, e na maioria das vezes isso funciona bem, mas estou tendo problemas com visualizações que exigem INSTEAD OFgatilhos. Tentei reduzir o problema, mas peço desculpas antecipadamente por isso ainda ser muito longo.

A situação

Todas as conexões com o banco de dados são feitas a partir de um front-end da web por meio de uma única conta dbweb. Depois de conectada, a função é alterada SET ROLEpara corresponder à pessoa que usa a interface da Web, e todas essas funções pertencem à função do grupo dbuser. (Veja esta resposta para detalhes). Vamos supor que o usuário seja alice.

A maioria das minhas tabelas é colocada em um esquema ao qual chamarei privatee pertenço dbowner. Essas tabelas não são diretamente acessíveis dbuser, mas são para outra função dbview. Por exemplo:

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;

A disponibilidade de linhas específicas para o usuário atual aliceé determinada por outras visualizações. Um exemplo simplificado (que pode ser reduzido, mas precisa ser feito dessa maneira para apoiar casos mais gerais) seria:

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

O acesso às linhas é fornecido por meio de uma exibição acessível a dbuserfunções como 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;

Observe que, como apenas a única relação aparece na FROMcláusula, esse tipo de visualização é atualizável sem nenhum gatilho adicional.

Para o log, existe outra tabela para registrar qual tabela foi alterada e quem a alterou. Uma versão reduzida é:

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;

Isso é preenchido através de gatilhos colocados em cada uma das relações que desejo rastrear. Por exemplo, um exemplo para private.incidentinserções limitadas a apenas é:

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

Portanto, agora, se for aliceinserido public.incident, um registro será ('incident','alice')exibido na auditoria.

O problema

Essa abordagem atinge problemas quando as visualizações se tornam mais complicadas e precisam de INSTEAD OFacionadores para suportar inserções.

Digamos que eu tenha duas relações, por exemplo, representando entidades envolvidas em alguma relação muitos-para-um:

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;

Suponha que eu não queira expor os detalhes que não sejam o nome de private.drivere, portanto, tenha uma exibição que junte as tabelas e projete os bits que eu quero expor:

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;

Para alicepoder inserir nessa visão, é necessário fornecer um gatilho, por exemplo:

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

O problema é que a SECURITY DEFINERopção na função acionadora faz com que ela seja executada com current_userdefinido como dbowner, portanto, se aliceinserir um novo registro na exibição, a entrada correspondente nos private.auditregistros do autor a ser dbowner.

Então, existe uma maneira de preservar current_user, sem conceder ao dbuserpapel do grupo acesso direto às relações no esquema private?

Solução Parcial

Conforme sugerido por Craig, o uso de regras em vez de gatilhos evita alterar o arquivo current_user. Usando o exemplo acima, o seguinte pode ser usado no lugar do gatilho de atualização:

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

Isso preserva current_user. RETURNINGCláusulas de suporte podem ser um pouco cabeludas, no entanto. Além disso, não consegui encontrar uma maneira segura de usar regras para inserir simultaneamente em ambas as tabelas, a fim de lidar com o uso de uma sequência para driver_id. A maneira mais fácil seria usar uma WITHcláusula em um INSERT(CTE), mas elas não são permitidas em conjunto com NEW(error rules cannot refer to NEW within WITH query:), deixando um recurso ao lastval()qual é fortemente desencorajado .

Respostas:


4

Portanto, existe uma maneira de preservar current_user, sem conceder ao papel do grupo dbuser acesso direto às relações no esquema privado?

Você pode usar uma regra, em vez de um INSTEAD OFgatilho, para fornecer acesso de gravação por meio da exibição. Visualizações agir sempre com os direitos do criador vista ao invés do usuário consulta de segurança, mas eu não acho current_user mudanças.

Se seu aplicativo se conectar diretamente como usuário, você poderá verificar em session_uservez de current_user. Isso também funciona se você se conectar com um usuário genérico SET SESSION AUTHORIZATION. SET ROLEPorém, não funcionará se você se conectar como um usuário genérico ao usuário desejado.

Não há como obter o usuário imediatamente anterior a partir de uma SECURITY DEFINERfunção. Você só pode obter o current_usere session_user. Uma maneira de obter a last_userou uma pilha de identidades de usuário seria boa, mas atualmente não é suportada.


Aha, não tinha lidado com regras antes, obrigado. SET SESSIONpoderia ser ainda melhor, mas acho que o usuário de login inicial precisaria ter privilégios de superusuário, o que é perigoso.
precisa saber é

@beldaz Yeah. É o grande problema com SET SESSION AUTHORIZATION. Eu realmente quero algo entre isso e SET ROLE, mas no momento não existe.
Craig Ringer

1

Não é uma resposta completa, mas não caberia em um comentário.

lastval() & currval()

O que faz você pensar lastval() é desencorajado? Parece um mal-entendido.

Na resposta mencionada , Craig recomenda usar um gatilho em vez da regra em um comentário . E eu concordo - exceto no seu caso especial, obviamente.

A resposta desencoraja fortemente o uso de currval()- mas isso parece ser um erro de compreensão. Não há nada errado lastval()ou melhor currval(). Deixei um comentário com a resposta referenciada.

Citando o manual:

currval

Retorne o valor obtido mais recentemente por nextvalpara esta sequência na sessão atual. (Um erro é relatado se nextvalnunca foi chamado para esta sequência nesta sessão.) Como isso está retornando um valor local da sessão, fornece uma resposta previsível se outras sessões foram executadas ou não nextvaldesde a sessão atual.

Portanto, isso é seguro com transações simultâneas. A única complicação possível pode surgir de outros gatilhos ou regras que podem chamar o mesmo gatilho inadvertidamente - o que seria um cenário muito improvável e você terá controle total sobre quais gatilhos / regras você instala.

No entanto , não sei se a sequência de comandos é preservada dentro das regras (mesmo que currval()seja uma função volátil ). Além disso, uma linha múltipla INSERTpode fazer com que você fique fora de sincronia. Você pode dividir sua REGRA em duas regras, apenas a segunda INSTEAD. Lembre-se, de acordo com a documentação:

Várias regras na mesma tabela e no mesmo tipo de evento são aplicadas em ordem alfabética dos nomes.

Eu não investiguei mais, fora do tempo.

DEFAULT PRIVILEGES

Quanto a:

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

Você pode estar interessado:

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

Relacionado:


Obrigado, eu estava realmente errado na minha compreensão lastvale currval, como eu não sabia que eles eram locais para uma sessão. Na verdade, eu uso privilégios padrão no meu esquema real, mas os por tabela eram de copiar e colar do banco de dados despejado. Concluí que reestruturar as relações é mais fácil do que mexer com regras, por mais legais que sejam, pois posso vê-las com dor de cabeça mais tarde.
beldaz

@eldeldaz: Eu acho que é uma boa decisão. Seu design estava ficando muito complicado.
Erwin Brandstetter
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.