J'ai une base de données PostgreSQL (9.4) qui limite l'accès aux enregistrements en fonction de l'utilisateur actuel et suit les modifications apportées par l'utilisateur. Ceci est réalisé grâce aux vues et aux déclencheurs, et pour la plupart, cela fonctionne bien, mais j'ai des problèmes avec les vues qui nécessitent des INSTEAD OF
déclencheurs. J'ai essayé de réduire le problème, mais je m'excuse à l'avance que c'est encore assez long.
La situation
Toutes les connexions à la base de données sont établies à partir d'un frontal Web via un seul compte dbweb
. Une fois connecté, le rôle est modifié via SET ROLE
pour correspondre à la personne utilisant l'interface Web, et tous ces rôles appartiennent au rôle de groupe dbuser
. (Voir cette réponse pour plus de détails). Supposons que l'utilisateur l'est alice
.
La plupart de mes tables sont placées dans un schéma que j'appellerai ici et auquel private
j'appartiens dbowner
. Ces tables ne sont pas directement accessibles à dbuser
, mais sont à un autre rôle dbview
. Par exemple:
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é de lignes spécifiques pour l'utilisateur actuel alice
est déterminée par d'autres vues. Un exemple simplifié (qui pourrait être réduit, mais doit être fait de cette façon pour prendre en charge des cas plus généraux) serait:
-- 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'accès aux lignes est ensuite fourni via une vue accessible à des dbuser
rôles tels que 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;
Notez que, comme seule la relation apparaît dans la FROM
clause, ce type de vue peut être mis à jour sans déclencheurs supplémentaires.
Pour la journalisation, une autre table existe pour enregistrer quelle table est modifiée et qui l'a modifiée. Une version réduite est:
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;
Ceci est rempli via des déclencheurs placés sur chacune des relations que je souhaite suivre. Par exemple, un exemple pour les private.incident
insertions limitées aux seuls est:
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();
Alors maintenant, si alice
insère dans public.incident
, un enregistrement ('incident','alice')
apparaît dans l'audit.
Le problème
Cette approche pose des problèmes lorsque les vues deviennent plus complexes et nécessitent des INSTEAD OF
déclencheurs pour prendre en charge les insertions.
Disons que j'ai deux relations, par exemple représentant des entités impliquées dans une relation plusieurs-à-un:
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;
Supposons que je ne souhaite pas exposer les détails autres que le nom de private.driver
, et donc avoir une vue qui joint les tables et projette les bits que je veux exposer:
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;
Pour alice
pouvoir insérer dans cette vue, un déclencheur doit être fourni, par exemple:
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();
Le problème avec cela est que l' SECURITY DEFINER
option dans la fonction de déclenchement provoque son exécution avec la valeur current_user
set dbowner
, donc si alice
insère un nouvel enregistrement dans la vue, l'entrée correspondante dans les private.audit
enregistrements sera l'auteur dbowner
.
Alors, y a-t-il un moyen de préserver current_user
, sans donner au dbuser
rôle de groupe un accès direct aux relations dans le schéma private
?
Solution partielle
Comme l'a suggéré Craig, l'utilisation de règles plutôt que de déclencheurs évite de changer le current_user
. En utilisant l'exemple ci-dessus, les éléments suivants peuvent être utilisés à la place du déclencheur de mise à jour:
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));
)
Cela préserve current_user
. Les RETURNING
clauses de support peuvent être un peu velues, cependant. De plus, je n'ai trouvé aucun moyen sûr d'utiliser des règles à insérer simultanément dans les deux tables afin de gérer l'utilisation d'une séquence pour driver_id
. La façon la plus simple aurait été d'utiliser une WITH
clause dans un INSERT
(CTE), mais celles-ci ne sont pas autorisées en conjonction avec NEW
(erreur:) rules cannot refer to NEW within WITH query
, ce qui en laisse une à lastval()
laquelle il est fortement déconseillé .
SET SESSION
pourrait être encore mieux, mais je pense que l'utilisateur de connexion initial devrait avoir des privilèges de superutilisateur, ce qui sent dangereux.