Verfolgen des aktuellen Benutzers durch Ansichten und Auslöser in PostgreSQL


11

Ich habe eine PostgreSQL (9.4) -Datenbank, die den Zugriff auf Datensätze abhängig vom aktuellen Benutzer beschränkt und vom Benutzer vorgenommene Änderungen verfolgt. Dies wird durch Ansichten und Trigger erreicht, und zum größten Teil funktioniert dies gut, aber ich habe Probleme mit Ansichten, die INSTEAD OFTrigger erfordern . Ich habe versucht, das Problem zu reduzieren, aber ich entschuldige mich im Voraus, dass dies noch ziemlich lange dauert.

Die Situation

Alle Verbindungen zur Datenbank werden von einem Web-Front-End über ein einziges Konto hergestellt dbweb. Sobald die Verbindung hergestellt ist, wird die Rolle über geändert SET ROLE, um der Person zu entsprechen, die die Weboberfläche verwendet, und alle diese Rollen gehören zur Gruppenrolle dbuser. (Siehe diese Antwort für Details). Nehmen wir an, der Benutzer ist alice.

Die meisten meiner Tabellen befinden sich in einem Schema, das ich hier aufrufe privateund zu dem ich gehöre dbowner. Diese Tabellen sind nicht direkt zugänglich dbuser, sondern haben eine andere Rolle dbview. Z.B:

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;

Die Verfügbarkeit bestimmter Zeilen für den aktuellen Benutzer alicewird durch andere Ansichten bestimmt. Ein vereinfachtes Beispiel (das reduziert werden könnte, aber auf diese Weise durchgeführt werden muss, um allgemeinere Fälle zu unterstützen) wäre:

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

Der Zugang zu den Zeilen wird dann durch einen Blick zur Verfügung gestellt, die zugänglich ist , dbuserRollen wie 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;

Beachten Sie, dass FROMdiese Art von Ansicht ohne zusätzliche Auslöser aktualisiert werden kann , da nur die eine Beziehung in der Klausel enthalten ist.

Für die Protokollierung gibt es eine andere Tabelle, in der aufgezeichnet wird, welche Tabelle geändert wurde und wer sie geändert hat. Eine reduzierte Version ist:

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;

Dies wird über Trigger ausgefüllt, die auf jeder der Beziehungen platziert sind, die ich verfolgen möchte. Ein Beispiel für die private.incidentBeschränkung auf nur Einfügungen ist beispielsweise:

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

Wenn nun aliceEinfügungen vorgenommen werden public.incident, wird ('incident','alice')im Audit ein Datensatz angezeigt.

Das Problem

Dieser Ansatz stößt auf Probleme, wenn die Ansichten komplizierter werden und INSTEAD OFTrigger zur Unterstützung von Einfügungen erforderlich sind .

Angenommen, ich habe zwei Beziehungen, die beispielsweise Entitäten darstellen, die an einer Eins-zu-Eins-Beziehung beteiligt sind:

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;

Angenommen, ich möchte keine anderen Details als den Namen von verfügbar machen private.driverund habe daher eine Ansicht, die die Tabellen verbindet und die Bits projiziert, die ich verfügbar machen möchte:

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;

Um alicein diese Ansicht einfügen zu können, muss ein Trigger bereitgestellt werden, z.

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

Das Problem dabei ist, dass die SECURITY DEFINEROption in der Triggerfunktion bewirkt, dass sie mit current_userset to ausgeführt dbownerwird. Wenn also aliceein neuer Datensatz in die Ansicht eingefügt wird, wird der entsprechende Eintrag in private.auditDatensätzen vom Autor erstellt dbowner.

Gibt es also eine Möglichkeit zu bewahren current_user, ohne der dbuserGruppenrolle direkten Zugriff auf die Beziehungen im Schema zu gewähren private?

Teillösung

Wie von Craig vorgeschlagen, wird durch die Verwendung von Regeln anstelle von Triggern eine Änderung der Regeln vermieden current_user. Anhand des obigen Beispiels kann anstelle des Update-Triggers Folgendes verwendet werden:

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

Dies bewahrt current_user. Unterstützende RETURNINGKlauseln können jedoch etwas haarig sein. Außerdem konnte ich keinen sicheren Weg finden, Regeln zu verwenden, um gleichzeitig in beide Tabellen einzufügen, um die Verwendung einer Sequenz für zu handhaben driver_id. Der einfachste Weg wäre gewesen, eine WITHKlausel in einem INSERT(CTE) zu verwenden, aber diese sind in Verbindung mit NEW(error :) nicht zulässig rules cannot refer to NEW within WITH query, so dass man auf eine zurückgreifen kann, von lastval()der dringend abgeraten wird .

Antworten:


4

current_userGibt es also eine Möglichkeit zu bewahren , ohne der dbuser-Gruppenrolle direkten Zugriff auf die Beziehungen im privaten Schema zu gewähren?

Möglicherweise können Sie eine Regel anstelle eines INSTEAD OFAuslösers verwenden, um Schreibzugriff über die Ansicht bereitzustellen. Ansichten handeln immer mit den Sicherheitsrechten des Erstellers der Ansicht und nicht mit dem abfragenden Benutzer, aber ich denke nicht, dass sich current_user Änderungen ergeben.

Wenn Ihre Anwendung eine direkte Verbindung als Benutzer herstellt, können Sie dies session_userstattdessen überprüfen current_user. Dies funktioniert auch, wenn Sie sich dann mit einem generischen Benutzer verbinden SET SESSION AUTHORIZATION. Es funktioniert jedoch nicht, wenn Sie als generischer Benutzer eine Verbindung zum SET ROLEgewünschten Benutzer herstellen.

Es gibt keine Möglichkeit, den unmittelbar vorherigen Benutzer aus einer SECURITY DEFINERFunktion heraus zu erhalten. Sie können nur das current_userund bekommen session_user. Ein Weg, um die last_useroder einen Stapel von Benutzeridentitäten zu erhalten, wäre nett, wird aber derzeit nicht unterstützt.


Aha, hatte sich vorher noch nicht mit Regeln befasst, danke. SET SESSIONkönnte noch besser sein, aber ich denke, der anfängliche Login-Benutzer müsste über Superuser-Berechtigungen verfügen, was gefährlich riecht.
Beldaz

@eldaz Ja. Es ist das große Problem mit SET SESSION AUTHORIZATION. Ich möchte wirklich etwas dazwischen und SET ROLE, aber im Moment gibt es so etwas nicht.
Craig Ringer

1

Keine vollständige Antwort, aber sie würde nicht in einen Kommentar passen.

lastval() & currval()

Was lässt Sie lastval()entmutigen? Scheint ein Missverständnis zu sein.

In der Antwort , auf die verwiesen wird , empfiehlt Craig dringend, in einem Kommentar anstelle der Regel einen Auslöser zu verwenden . Und ich stimme zu - außer natürlich Ihrem Sonderfall.

Die Antwort rät stark von der Verwendung von ab currval()- aber das scheint ein Missverständnis zu sein. Es ist nichts falsch mit lastval()oder eher currval(). Ich habe einen Kommentar mit der Antwort hinterlassen, auf die verwiesen wird.

Zitat des Handbuchs:

currval

Gibt den Wert zurück, der zuletzt nextvalfür diese Sequenz in der aktuellen Sitzung erhalten wurde. (Ein Fehler wird gemeldet, wenn nextvalin dieser Sitzung noch nie für diese Sequenz aufgerufen wurde.) Da dies einen sitzungslokalen Wert zurückgibt, wird eine vorhersehbare Antwort gegeben, ob nextvalseit der aktuellen Sitzung andere Sitzungen ausgeführt wurden oder nicht .

Dies ist also bei gleichzeitigen Transaktionen sicher. Die einzig mögliche Komplikation kann durch andere Trigger oder Regeln entstehen, die möglicherweise versehentlich denselben Trigger aufrufen. Dies wäre ein sehr unwahrscheinliches Szenario, und Sie haben die vollständige Kontrolle darüber, welche Trigger / Regeln Sie installieren.

Allerdings habe ich nicht sicher , dass die Folge von Befehlen innerhalb Regeln beibehalten wird (obwohl currval()als veränderliche Funktion ). Eine mehrzeilige Version kann auch INSERTdazu führen, dass Sie nicht mehr synchron sind. Sie könnten Ihre REGEL in zwei Regeln aufteilen, nur die zweite INSTEAD. Denken Sie daran, gemäß Dokumentation:

Mehrere Regeln für dieselbe Tabelle und denselben Ereignistyp werden in alphabetischer Reihenfolge angewendet.

Ich habe nicht rechtzeitig nachgeforscht.

DEFAULT PRIVILEGES

Wie für:

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

Sie könnten stattdessen interessiert sein:

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

Verbunden:


Vielen Dank, ich habe mich in der Tat geirrt lastvalund currval, da ich nicht wusste, dass sie lokal für eine Sitzung sind. Tatsächlich verwende ich Standardberechtigungen in meinem realen Schema, aber die Berechtigungen pro Tabelle stammten aus dem Kopieren und Einfügen aus der ausgelagerten Datenbank. Ich bin zu dem Schluss gekommen, dass die Umstrukturierung der Beziehungen einfacher ist, als mit Regeln herumzuspielen, auch wenn sie ordentlich sind, da ich später sehen kann, dass sie Kopfschmerzen bereiten.
Beldaz

@Beldaz: Ich denke das ist eine gute Entscheidung. Ihr Design wurde zu kompliziert.
Erwin Brandstetter
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.