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 é obtido por meio de exibições e acionadores e, na maioria das vezes, funciona bem, mas estou tendo problemas com exibições que exigem INSTEAD OF
acionadores. 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
. Uma vez conectado, o papel é alterado SET ROLE
para corresponder à pessoa que está usando a interface da web, e todos esses papéis pertencem ao grupo role dbuser
. (Veja esta resposta para detalhes). Vamos supor que o usuário seja alice
.
A maioria das minhas tabelas são colocadas em um esquema que aqui vou chamar private
e pertencer dbowner
. Essas tabelas não são acessÃveis diretamente para dbuser
, mas 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 exibições. Um exemplo simplificado (que poderia ser reduzido, mas precisa ser feito dessa forma para dar suporte a 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 visualização acessÃvel a dbuser
funçõ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 uma relação aparece na FROM
cláusula, esse tipo de exibição é atualizável sem nenhum acionador adicional.
Para criação de 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 por meio de gatilhos colocados em cada uma das relações que desejo rastrear. Por exemplo, um exemplo private.incident
limitado a apenas inserções é:
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();
Agora, se alice
insere em public.incident
, um registro ('incident','alice')
aparece na auditoria.
O problema
Essa abordagem atinge problemas quando as exibições se tornam mais complicadas e precisam INSTEAD OF
de gatilhos para suportar inserções.
Digamos que eu tenha duas relações, por exemplo, representando entidades envolvidas em algum relacionamento 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 além do nome de private.driver
, e assim ter uma visão que una as tabelas e projete os bits que desejo 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 alice
poder inserir nesta visão, um gatilho deve ser fornecido, 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 disso é que a SECURITY DEFINER
opção na função trigger faz com que ela seja executada com current_user
set to dbowner
, então se alice
insere um novo registro na view a entrada correspondente nos private.audit
registros o autor a ser dbowner
.
Então, existe uma maneira de preservar current_user
, sem dar ao dbuser
grupo acesso direto às relações no esquema private
?
Solução Parcial
Conforme sugerido por Craig, usar regras em vez de gatilhos evita alterar o arquivo current_user
. Usando o exemplo acima, o seguinte pode ser usado no lugar do acionador 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
. RETURNING
Cláusulas de apoio podem ser um pouco complicadas, no entanto. Além disso, não consegui encontrar uma maneira segura de usar regras para inserir simultaneamente em ambas as tabelas para lidar com o uso de uma sequência para arquivos driver_id
. A maneira mais fácil seria usar uma WITH
cláusula em um INSERT
(CTE), mas estes não são permitidos em conjunto com NEW
(error: rules cannot refer to NEW within WITH query
), deixando um para recorrer ao lastval()
qual é fortemente desencorajado .
Você pode usar uma regra, em vez de um
INSTEAD OF
gatilho, para fornecer acesso de gravação por meio da exibição. As exibições sempre agem com os direitos de segurança do criador da exibição, e não do usuário que faz a consulta, mas não acho quecurrent_user
mude.Se seu aplicativo se conecta diretamente como usuário, você pode verificar
session_user
em vez decurrent_user
. Isso também funciona se você se conectar com um usuário genéricoSET SESSION AUTHORIZATION
. Não funcionará se você se conectar como um usuário genérico e, em seguida,SET ROLE
para o usuário desejado.Não há como obter o usuário imediatamente anterior de dentro de uma
SECURITY DEFINER
função. Você só pode obter ocurrent_user
esession_user
. Uma maneira de obterlast_user
ou uma pilha de identidades de usuário seria legal, mas não é suportada atualmente.Não é uma resposta completa, mas não caberia em um comentário.
lastval()
&currval()
O que te faz pensar que
lastval()
está desanimado? Parece um mal-entendido.Na resposta referenciada , Craig recomenda fortemente o uso de um gatilho em vez da regra em um comentário . E eu concordo - exceto pelo seu caso especial, obviamente.
A resposta desencoraja fortemente o uso de
currval()
- mas isso parece ser um mal-entendido. Não há nada de errado comlastval()
ou melhorcurrval()
. Deixei um comentário com a resposta referenciada.Citando o manual:
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ê tem controle total sobre quais gatilhos/regras você instala.
No entanto , não tenho certeza se a sequência de comandos é preservada nas regras (mesmo que
currval()
seja uma função volátil ). Além disso, uma linha múltiplaINSERT
pode deixar você fora de sincronia. Você pode dividir sua REGRA em duas regras, sendo apenas a segundaINSTEAD
. Lembre-se, por documentação:Não investiguei mais, fora de tempo.
DEFAULT PRIVILEGES
Quanto a:
Você pode estar interessado em vez disso:
Relacionado: