Tracciamento dell'utente corrente tramite visualizzazioni e trigger in PostgreSQL


11

Ho un database PostgreSQL (9.4) che limita l'accesso ai record a seconda dell'utente corrente e tiene traccia delle modifiche apportate dall'utente. Ciò si ottiene attraverso visualizzazioni e trigger e, nella maggior parte dei casi, funziona bene, ma ho problemi con le visualizzazioni che richiedono INSTEAD OFtrigger. Ho cercato di ridurre il problema, ma mi scuso in anticipo che questo è ancora piuttosto lungo.

La situazione

Tutte le connessioni al database vengono effettuate da un front-end Web tramite un singolo account dbweb. Una volta connesso, il ruolo viene modificato in modo SET ROLEda corrispondere alla persona che utilizza l'interfaccia Web e tutti questi ruoli appartengono al ruolo di gruppo dbuser. (Vedi questa risposta per i dettagli). Supponiamo che l'utente lo sia alice.

La maggior parte delle mie tabelle sono inserite in uno schema che qui chiamerò privatee al quale appartengo dbowner. Queste tabelle non sono direttamente accessibili dbuser, ma sono per un altro ruolo dbview. Per esempio:

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à di righe specifiche per l'utente corrente aliceè determinata da altre viste. Un esempio semplificato (che potrebbe essere ridotto, ma deve essere fatto in questo modo per supportare casi più generali) sarebbe:

-- 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'accesso alle righe viene quindi fornito attraverso una vista accessibile a dbuserruoli come 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;

Si noti che poiché solo una relazione appare nella FROMclausola, questo tipo di vista è aggiornabile senza ulteriori trigger.

Per la registrazione, esiste un'altra tabella per registrare quale tabella è stata modificata e chi l'ha modificata. Una versione ridotta è:

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;

Questo è popolato da trigger posizionati su ciascuna delle relazioni che desidero tracciare. Ad esempio, un esempio per i private.incidentsoli inserti è:

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

Quindi ora se viene aliceinserito in public.incident, un record ('incident','alice')appare nel controllo.

Il problema

Questo approccio presenta problemi quando le viste diventano più complicate e necessitano di INSTEAD OFtrigger per supportare gli inserti.

Diciamo che ho due relazioni, ad esempio che rappresentano entità coinvolte in una relazione molti-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;

Supponiamo che non voglio esporre i dettagli oltre al nome di private.driver, e quindi avere una vista che unisce le tabelle e proietta i bit che voglio esporre:

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;

Per alicepoter inserire in questa vista è necessario fornire un trigger, ad esempio:

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

Il problema con questo è che l' SECURITY DEFINERopzione nella funzione trigger fa sì che venga eseguita con current_userset su dbowner, quindi se aliceinserisce un nuovo record nella vista la corrispondente voce nei private.auditrecord dell'autore dbowner.

Quindi, c'è un modo per preservare current_user, senza dare al dbuserruolo del gruppo l'accesso diretto alle relazioni nello schema private?

Soluzione parziale

Come suggerito da Craig, l'utilizzo delle regole anziché dei trigger evita di modificare il file current_user. Utilizzando l'esempio sopra, al posto del trigger di aggiornamento è possibile utilizzare quanto segue:

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

Questo conserva current_user. RETURNINGTuttavia, le clausole di supporto possono essere un po 'pelose. Inoltre, non sono riuscito a trovare un modo sicuro per utilizzare le regole da inserire simultaneamente in entrambe le tabelle per gestire l'uso di una sequenza driver_id. Il modo più semplice sarebbe stato di utilizzare una WITHclausola in un INSERT(CTE), ma questi non sono consentiti in combinazione con NEW(errore rules cannot refer to NEW within WITH query:), lasciandone uno a lastval()cui è fortemente sconsigliato .

Risposte:


4

Quindi, c'è un modo per preservare current_user, senza dare al ruolo del gruppo dbuser l'accesso diretto alle relazioni nello schema privato?

Potresti essere in grado di utilizzare una regola, anziché un INSTEAD OFtrigger, per fornire l'accesso in scrittura attraverso la vista. Le viste agiscono sempre con i diritti di sicurezza del creatore della vista anziché dell'utente che esegue la query, ma non credo che le current_user modifiche.

Se l'applicazione si connette direttamente come utente, è possibile selezionare session_userinvece di current_user. Questo funziona anche se ti connetti con un utente generico SET SESSION AUTHORIZATION. SET ROLETuttavia, non funzionerà se ti connetti come utente generico, quindi all'utente desiderato.

Non è possibile ottenere l'utente immediatamente precedente all'interno di una SECURITY DEFINERfunzione. Puoi solo ottenere il current_usere session_user. Un modo per ottenere last_useruna pila di identità utente sarebbe utile, ma al momento non è supportato.


Ah, non avevo mai affrontato le regole prima, grazie. SET SESSIONpotrebbe essere ancora migliore, ma penso che l'utente di accesso iniziale dovrebbe avere i privilegi di superutente, che ha un odore pericoloso.
Beldaz,

@beldaz Sì. È il grosso problema con SET SESSION AUTHORIZATION. Voglio davvero qualcosa tra questo e SET ROLE, ma al momento non esiste nulla del genere.
Craig Ringer,

1

Non è una risposta completa, ma non si adatterebbe a un commento.

lastval() & currval()

Cosa ti fa pensare che lastval()sia scoraggiato? Sembra un malinteso.

Nella risposta di riferimento , Craig consiglia vivamente di utilizzare un trigger anziché la regola in un commento . E sono d'accordo - tranne che per il tuo caso speciale, ovviamente.

La risposta scoraggia fortemente l'uso di currval()- ma sembra essere un malinteso. Non c'è niente di sbagliato lastval()o meglio currval(). Ho lasciato un commento con la risposta di riferimento.

Citando il manuale:

currval

Restituisce il valore più recente ottenuto da nextvalper questa sequenza nella sessione corrente. (Viene segnalato un errore se nextvalnon è mai stato chiamato per questa sequenza in questa sessione.) Dato che questo sta restituendo un valore locale della sessione, fornisce una risposta prevedibile se altre sessioni sono state eseguite o meno nextvaldall'ultima sessione.

Quindi questo è sicuro con transazioni simultanee. L'unica possibile complicazione potrebbe derivare da altri trigger o regole che potrebbero chiamare lo stesso trigger inavvertitamente, il che sarebbe uno scenario molto improbabile e avrai il controllo completo su quali trigger / regole installi.

Tuttavia , non sono sicuro che la sequenza di comandi sia conservata all'interno delle regole (anche se currval()è una funzione volatile ). Inoltre, una riga multipla INSERTpotrebbe non essere sincronizzata. Puoi dividere la tua REGOLA in due regole, solo la seconda INSTEAD. Ricorda, per documentazione:

Più regole sulla stessa tabella e lo stesso tipo di evento vengono applicate in ordine alfabetico dei nomi.

Non ho indagato ulteriormente, fuori tempo.

DEFAULT PRIVILEGES

Quanto a:

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

Potresti essere interessato invece:

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

Relazionato:


Grazie, avevo davvero torto nella mia comprensione lastvale currval, poiché non mi rendevo conto che erano locali per una sessione. In effetti, utilizzo i privilegi predefiniti nel mio schema reale, ma quelli per tabella derivano dalla copia e dall'incollaggio dal DB di cui è stato eseguito il dump. Ho concluso che ristrutturare le relazioni è più facile che scherzare con le regole, per quanto siano pulite, dal momento che posso vederle essere un mal di testa in seguito.
Beldaz,

@beldaz: penso che sia una buona decisione. Il tuo design stava diventando troppo complicato.
Erwin Brandstetter,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.