Seguimiento del usuario actual a través de vistas y disparadores en PostgreSQL

11

Tengo una base de datos PostgreSQL (9.4) que limita el acceso a los registros dependiendo del usuario actual, y rastrea los cambios realizados por el usuario. Esto se logra a través de vistas y disparadores, y en su mayor parte funciona bien, pero tengo problemas con las vistas que requieren INSTEAD OFdisparadores. He tratado de reducir el problema, pero pido disculpas de antemano porque todavía es bastante largo.

La situación

Todas las conexiones a la base de datos se realizan desde un front-end web a través de una sola cuenta dbweb. Una vez conectado, el rol se cambia SET ROLEpara corresponder a la persona que usa la interfaz web, y todos esos roles pertenecen al rol grupal dbuser. (Ver esta respuesta para más detalles). Asumamos que el usuario es alice.

La mayoría de mis tablas están ubicadas en un esquema al que llamaré privatey perteneceré dbowner. Estas tablas no son accesibles directamente dbuser, pero tienen otro rol dbview. P.ej:

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 disponibilidad de filas específicas para el usuario actual aliceestá determinada por otras vistas. Un ejemplo simplificado (que podría reducirse, pero debe hacerse de esta manera para admitir casos más generales) sería:

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

El acceso a las filas se proporciona a través de una vista accesible para dbuserroles 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;

Tenga en cuenta que debido a que solo aparece una relación en la FROMcláusula, este tipo de vista es actualizable sin ningún desencadenante adicional.

Para el registro, existe otra tabla para registrar qué tabla se cambió y quién la cambió. Una versión reducida es:

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;

Esto se completa a través de disparadores ubicados en cada una de las relaciones que deseo rastrear. Por ejemplo, un ejemplo para private.incidentinsertos limitados a solo es:

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();

Entonces, si se aliceinserta public.incident, ('incident','alice')aparece un registro en la auditoría.

El problema

Este enfoque tiene problemas cuando las vistas se vuelven más complicadas y necesitan INSTEAD OFactivadores para admitir inserciones.

Digamos que tengo dos relaciones, por ejemplo, que representan entidades involucradas en alguna relación de muchos 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;

Suponga que no quiero exponer los detalles que no sean el nombre de private.driver, y así tener una vista que une las tablas y proyecta los bits que quiero exponer:

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 alicepoder insertar en esta vista se debe proporcionar un activador, por ejemplo:

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();

El problema con esto es que la SECURITY DEFINERopción en la función de activación hace que se ejecute con current_userset to dbowner, por lo que si aliceinserta un nuevo registro en la vista, la entrada correspondiente en los private.auditregistros debe ser el autor dbowner.

Entonces, ¿hay alguna manera de preservar current_user, sin darle al dbuserrol de grupo acceso directo a las relaciones en el esquema private?

Solución parcial

Como sugiere Craig, el uso de reglas en lugar de desencadenantes evita cambiar el current_user. Usando el ejemplo anterior, se puede usar lo siguiente en lugar del activador de actualización:

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));               
   )

Esto conserva current_user. Sin RETURNINGembargo, las cláusulas de apoyo pueden ser un poco complicadas. Además, no pude encontrar una forma segura de usar reglas para insertar simultáneamente en ambas tablas para manejar el uso de una secuencia driver_id. La forma más fácil hubiera sido usar una WITHcláusula en un INSERT(CTE), pero no se permiten en conjunto con NEW(error:) rules cannot refer to NEW within WITH query, dejando que recurra, lo lastval()que se desaconseja .

beldaz
fuente

Respuestas:

4

Entonces, ¿hay alguna forma de preservar current_user, sin darle al rol del grupo dbuser acceso directo a las relaciones en esquema privado?

Es posible que pueda usar una regla, en lugar de un INSTEAD OFactivador, para proporcionar acceso de escritura a través de la vista. Las vistas siempre actúan con los derechos de seguridad del creador de la vista en lugar del usuario que realiza la consulta, pero no creo que los current_user cambios.

Si su aplicación se conecta directamente como usuario, puede verificar en session_userlugar de current_user. Esto también funciona si te conectas con un usuario genérico SET SESSION AUTHORIZATION. Sin SET ROLEembargo, no funcionará si se conecta como un usuario genérico al usuario deseado.

No hay forma de obtener el usuario inmediatamente anterior desde una SECURITY DEFINERfunción. Solo puedes obtener el current_usery session_user. last_userSería buena una forma de obtener la o una pila de identidades de usuario, pero actualmente no es compatible.

Craig Ringer
fuente
Ajá, no había tratado con reglas antes, gracias. SET SESSIONpodría ser aún mejor, pero creo que el usuario de inicio de sesión inicial necesitaría tener privilegios de superusuario, lo que huele a peligroso.
beldaz
@beldaz Sí. Es el gran problema con SET SESSION AUTHORIZATION. Realmente quiero algo entre eso y SET ROLE, pero por el momento no existe tal cosa.
Craig Ringer
1

No es una respuesta completa, pero no cabe en un comentario.

lastval() Y currval()

¿Qué te hace pensar que lastval()se desalienta? Parece un malentendido.

En la respuesta a la que se hace referencia , Craig recomienda encarecidamente utilizar un activador en lugar de la regla en un comentario . Y estoy de acuerdo, excepto por su caso especial, obviamente.

La respuesta desalienta fuertemente el uso de currval(), pero eso parece ser un malentendido. No hay nada malo lastval()o más bien currval(). Dejé un comentario con la respuesta referenciada.

Citando el manual:

currval

Devuelve el valor obtenido más recientemente nextvalpara esta secuencia en la sesión actual. (Se informa un error si nextvalnunca se ha llamado para esta secuencia en esta sesión). Debido a que esto devuelve un valor local de sesión, da una respuesta predecible si otras sesiones se han ejecutado o no nextvaldesde la sesión actual.

Entonces esto es seguro con transacciones concurrentes. La única complicación posible podría surgir de otros desencadenantes o reglas que podrían llamar inadvertidamente al mismo desencadenador, lo cual sería un escenario muy poco probable y usted tiene control completo sobre los desencadenantes / reglas que instala.

Sin embargo , no estoy seguro de que la secuencia de comandos se conserve dentro de las reglas (aunque currval()es una función volátil ). Además, una fila múltiple INSERTpuede hacer que no esté sincronizado. Podrías dividir tu REGLA en dos reglas, solo la segunda INSTEAD. Recuerde, por documentación:

Se aplican varias reglas en la misma tabla y el mismo tipo de evento en orden alfabético de nombres.

No investigué más, fuera de tiempo.

DEFAULT PRIVILEGES

Como para:

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

Quizás te interese:

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

Relacionado:

Erwin Brandstetter
fuente
Gracias, realmente me equivoqué en mi comprensión lastvaly currval, como no me di cuenta de que eran locales en una sesión. De hecho, uso privilegios predeterminados en mi esquema real, pero los por tabla eran de copiar y pegar desde el DB volcado. Llegué a la conclusión de que reestructurar las relaciones es más fácil que jugar con las reglas, por muy claras que sean, ya que puedo verlas como un dolor de cabeza más adelante.
beldaz
@beldaz: Creo que es una buena decisión. Tu diseño se estaba volviendo demasiado complicado.
Erwin Brandstetter