Suivi de l'utilisateur actuel via des vues et des déclencheurs dans PostgreSQL

11

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 OFdé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 ROLEpour 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 privatej'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 aliceest 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 dbuserrô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 FROMclause, 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.incidentinsertions 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 aliceinsè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 OFdé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 alicepouvoir 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 DEFINERoption dans la fonction de déclenchement provoque son exécution avec la valeur current_userset dbowner, donc si aliceinsère un nouvel enregistrement dans la vue, l'entrée correspondante dans les private.auditenregistrements sera l'auteur dbowner.

Alors, y a-t-il un moyen de préserver current_user, sans donner au dbuserrô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 RETURNINGclauses 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 WITHclause 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é .

beldaz
la source

Réponses:

4

Alors, existe-t-il un moyen de préserver current_user, sans donner au rôle du groupe dbuser un accès direct aux relations dans le schéma privé?

Vous pouvez peut-être utiliser une règle, plutôt qu'un INSTEAD OFdéclencheur, pour fournir un accès en écriture via la vue. Les vues agissent toujours avec les droits de sécurité du créateur de la vue plutôt que de l'utilisateur interrogateur, mais je ne pense pas que lescurrent_user changements.

Si votre application se connecte directement en tant qu'utilisateur, vous pouvez vérifier session_userau lieu de current_user. Cela fonctionne également si vous vous connectez avec un utilisateur générique SET SESSION AUTHORIZATION. SET ROLECependant, cela ne fonctionnera pas si vous vous connectez en tant qu'utilisateur générique à l'utilisateur souhaité.

Il n'existe aucun moyen d'obtenir l'utilisateur immédiatement précédent à partir d'une SECURITY DEFINERfonction. Vous ne pouvez obtenir que le current_useret session_user. Un moyen d'obtenir le last_userou une pile d'identités utilisateur serait bien, mais n'est pas actuellement pris en charge.

Craig Ringer
la source
Aha, n'avait pas traité de règles auparavant, merci. SET SESSIONpourrait être encore mieux, mais je pense que l'utilisateur de connexion initial devrait avoir des privilèges de superutilisateur, ce qui sent dangereux.
beldaz
@beldaz Ouais. C'est le gros problème avec SET SESSION AUTHORIZATION. Je veux vraiment quelque chose entre ça et SET ROLE, mais pour le moment il n'y a rien de tel.
Craig Ringer
1

Pas une réponse complète, mais elle ne rentrerait pas dans un commentaire.

lastval() & currval()

Qu'est-ce qui vous lastval()décourage? On dirait un malentendu.

Dans la réponse référencée , Craig recommande fortement d'utiliser un déclencheur au lieu de la règle dans un commentaire . Et je suis d'accord - sauf pour votre cas particulier, évidemment.

La réponse décourage fortement l'utilisation de currval()- mais cela semble être un malentendu. Il n'y a rien de mal avec lastval()ou plutôt currval(). J'ai laissé un commentaire avec la réponse référencée.

Citant le manuel:

currval

Renvoie la dernière valeur obtenue par nextvalpour cette séquence dans la session en cours. (Une erreur est signalée si elle nextvaln'a jamais été appelée pour cette séquence dans cette session.) Comme cela renvoie une valeur de session locale, cela donne une réponse prévisible si d'autres sessions se sont exécutées nextvaldepuis la session en cours ou non .

C'est donc sûr avec des transactions simultanées. La seule complication possible pourrait provenir d'autres déclencheurs ou règles qui pourraient appeler le même déclencheur par inadvertance - ce qui serait un scénario très improbable et vous avez un contrôle complet sur les déclencheurs / règles que vous installez.

Cependant , je ne suis pas sûr que la séquence de commandes soit préservée dans les règles (même si currval()c'est une fonction volatile ). En outre, une ligne multiple INSERTpeut vous désynchroniser. Vous pouvez diviser votre RÈGLE en deux règles, seule la seconde étant INSTEAD. N'oubliez pas, selon la documentation:

Plusieurs règles sur la même table et le même type d'événement sont appliquées dans l'ordre alphabétique des noms.

Je n'ai pas enquêté davantage, hors du temps.

DEFAULT PRIVILEGES

Pour ce qui est de:

SET SESSION AUTHORIZATION dbowner;
...
GRANT ALL ON TABLE private.incident TO dbview;

Vous pourriez être intéressé à la place:

ALTER DEFAULT PRIVILEGES FOR ROLE dbowner IN SCHEMA private
   GRANT ALL ON TABLES TO dbview;

En relation:

Erwin Brandstetter
la source
Merci, j'avais vraiment tort de comprendre lastvalet currval, comme je ne savais pas qu'ils étaient locaux pour une session. En fait, j'utilise des privilèges par défaut dans mon schéma réel, mais ceux par table provenaient de copier-coller à partir de la base de données vidée. J'ai conclu qu'il était plus facile de restructurer les relations que de jouer avec les règles, aussi soignées soient-elles, car je peux les voir devenir un casse-tête plus tard.
beldaz
@beldaz: Je pense que c'est une bonne décision. Votre conception devenait trop compliquée.
Erwin Brandstetter