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 OF
Trigger 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 private
und 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 alice
wird 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 , dbuser
Rollen 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 FROM
diese 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.incident
Beschrä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 alice
Einfü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 OF
Trigger 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.driver
und 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 alice
in 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 DEFINER
Option in der Triggerfunktion bewirkt, dass sie mit current_user
set to ausgeführt dbowner
wird. Wenn also alice
ein neuer Datensatz in die Ansicht eingefügt wird, wird der entsprechende Eintrag in private.audit
Datensätzen vom Autor erstellt dbowner
.
Gibt es also eine Möglichkeit zu bewahren current_user
, ohne der dbuser
Gruppenrolle 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 RETURNING
Klauseln 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 WITH
Klausel 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 .
SET SESSION
könnte noch besser sein, aber ich denke, der anfängliche Login-Benutzer müsste über Superuser-Berechtigungen verfügen, was gefährlich riecht.