Ho un database PostgreSQL (9.4) che limita l'accesso ai record a seconda dell'utente corrente e tiene traccia delle modifiche apportate dall'utente. Ciò si ottiene attraverso visualizzazioni e trigger e, nella maggior parte dei casi, funziona bene, ma ho problemi con le visualizzazioni che richiedono INSTEAD OF
trigger. Ho cercato di ridurre il problema, ma mi scuso in anticipo che questo è ancora piuttosto lungo.
La situazione
Tutte le connessioni al database vengono effettuate da un front-end Web tramite un singolo account dbweb
. Una volta connesso, il ruolo viene modificato in modo SET ROLE
da corrispondere alla persona che utilizza l'interfaccia Web e tutti questi ruoli appartengono al ruolo di gruppo dbuser
. (Vedi questa risposta per i dettagli). Supponiamo che l'utente lo sia alice
.
La maggior parte delle mie tabelle sono inserite in uno schema che qui chiamerò private
e al quale appartengo dbowner
. Queste tabelle non sono direttamente accessibili dbuser
, ma sono per un altro ruolo dbview
. Per esempio:
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;
La disponibilità di righe specifiche per l'utente corrente alice
è determinata da altre viste. Un esempio semplificato (che potrebbe essere ridotto, ma deve essere fatto in questo modo per supportare casi più generali) sarebbe:
-- 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;
L'accesso alle righe viene quindi fornito attraverso una vista accessibile a dbuser
ruoli come 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;
Si noti che poiché solo una relazione appare nella FROM
clausola, questo tipo di vista è aggiornabile senza ulteriori trigger.
Per la registrazione, esiste un'altra tabella per registrare quale tabella è stata modificata e chi l'ha modificata. Una versione ridotta è:
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;
Questo è popolato da trigger posizionati su ciascuna delle relazioni che desidero tracciare. Ad esempio, un esempio per i private.incident
soli inserti è:
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();
Quindi ora se viene alice
inserito in public.incident
, un record ('incident','alice')
appare nel controllo.
Il problema
Questo approccio presenta problemi quando le viste diventano più complicate e necessitano di INSTEAD OF
trigger per supportare gli inserti.
Diciamo che ho due relazioni, ad esempio che rappresentano entità coinvolte in una relazione molti-a-uno:
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;
Supponiamo che non voglio esporre i dettagli oltre al nome di private.driver
, e quindi avere una vista che unisce le tabelle e proietta i bit che voglio esporre:
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;
Per alice
poter inserire in questa vista è necessario fornire un trigger, ad esempio:
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();
Il problema con questo è che l' SECURITY DEFINER
opzione nella funzione trigger fa sì che venga eseguita con current_user
set su dbowner
, quindi se alice
inserisce un nuovo record nella vista la corrispondente voce nei private.audit
record dell'autore dbowner
.
Quindi, c'è un modo per preservare current_user
, senza dare al dbuser
ruolo del gruppo l'accesso diretto alle relazioni nello schema private
?
Soluzione parziale
Come suggerito da Craig, l'utilizzo delle regole anziché dei trigger evita di modificare il file current_user
. Utilizzando l'esempio sopra, al posto del trigger di aggiornamento è possibile utilizzare quanto segue:
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));
)
Questo conserva current_user
. RETURNING
Tuttavia, le clausole di supporto possono essere un po 'pelose. Inoltre, non sono riuscito a trovare un modo sicuro per utilizzare le regole da inserire simultaneamente in entrambe le tabelle per gestire l'uso di una sequenza driver_id
. Il modo più semplice sarebbe stato di utilizzare una WITH
clausola in un INSERT
(CTE), ma questi non sono consentiti in combinazione con NEW
(errore rules cannot refer to NEW within WITH query
:), lasciandone uno a lastval()
cui è fortemente sconsigliato .
SET SESSION
potrebbe essere ancora migliore, ma penso che l'utente di accesso iniziale dovrebbe avere i privilegi di superutente, che ha un odore pericoloso.