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