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 OF
gatilhos. 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 ROLE
para 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 private
e 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 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 a única relação aparece na FROM
clá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.incident
inserçõ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 alice
inserido 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 OF
acionadores 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.driver
e, 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 alice
poder 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 DEFINER
opção na função acionadora faz com que ela seja executada com current_user
definido como dbowner
, portanto, se alice
inserir um novo registro na exibição, a entrada correspondente nos private.audit
registros do autor a ser dbowner
.
Então, existe uma maneira de preservar current_user
, sem conceder ao dbuser
papel 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
. RETURNING
Clá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 WITH
clá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 .
fonte
SET SESSION
poderia ser ainda melhor, mas acho que o usuário de login inicial precisaria ter privilégios de superusuário, o que é perigoso.SET SESSION AUTHORIZATION
. Eu realmente quero algo entre isso eSET ROLE
, mas no momento não existe.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 erradolastval()
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ê 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últiplaINSERT
pode fazer com que você fique fora de sincronia. Você pode dividir sua REGRA em duas regras, apenas a segundaINSTEAD
. Lembre-se, de acordo com a documentação:Eu não investiguei mais, fora do tempo.
DEFAULT PRIVILEGES
Quanto a:
Você pode estar interessado:
Relacionado:
fonte
lastval
ecurrval
, 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.