Oracle Virtual Private Database
von Heinz-Wilhelm Fabry, ORACLE Deutschland GmbH

Die existierenden Objektprivilegien, die Anwendern das Lesen, Einfügen, Ändern und Löschen von Daten erlauben, zielen immer auf alle Zeilen einer Tabelle. Soll der Zugriff auf Zeilenebene gesteuert werden, weicht man entweder auf die Steuerung des Zugriffs über Anwendungen aus oder verwendet Views. Beides ist uneffektiv: Die Steuerung über Anwendungen, weil sie in jeder Anwendung separat programmiert werden muss und Änderungen in jeder Anwendung nachgezogen werden müssen. Die Steuerung über Views, weil bei großen Benutzergruppen oder vielen Views das ganze Berechtigungssystem schnell unüberschaubar wird.

Oracle bietet schon seit der Version Oracle8i eine Lösung für dieses Problem: Die Lösung ist unter den Namen Fine Grained Access Control (FGAC) oder auch Virtual Private Database (VPD) bekannt. Es handelt sich dabei um ein Feature der Enterprise Edition, dessen Nutzung in der Regel eine enge Zusammenarbeit von Datenbankadministratoren und Anwendungsentwicklern voraussetzt.

VPD implementiert die Kontrolle für den Zugriff auf einzelne Zeilen auf der Ebene der Tabelle: Mit dem Paket DBMS_RLS werden die Befehle INSERT, UPDATE, DELETE und SELECT, die auf eine mit VPD geschützte Tabelle zugreifen, um eine zusätzliche WHERE-Bedingung erweitert. Diese WHERE-Bedingung wird flexibel durch eine Funktion erstellt, die der Datenbankadministrator oder Anwendungsentwickler schreiben muß. Enthält das SELECT, UPDATE oder DELETE bereits eine WHERE-Bedingung, wird die von der Funktion erzeugte WHERE-Bedingung einfach mit AND angehängt. Die Verbindung zwischen Funktion und Tabelle wird als Policy bezeichnet, und die Funktion deshalb auch als Policy-Funktion.

Es ist möglich, identische oder auch unterschiedliche Policy-Funktionen für die Aktionen SELECT, INSERT, UPDATE und DELETE anzulegen. Sogar mehrere Policy-Funktionen für ein und dieselbe Aktion sind erlaubt. Und man kann auch Policy-Funktionen in Gruppen zusammenlegen (Oracle-Terminologie PARTITIONED FGAC): Die Ergebnisse der Funktionen werden einfach durch AND verbunden.

Die wahre Leistungsfähigkeit der VPD offenbart sich aber erst im Zusammenspiel mit Variablen, die als sogenannter Application Context im Arbeitsspeicher angelegt werden. Ein Application Context kann z. B. beim ersten Connect auf die Datenbank im Rahmen eines Logon-Triggers mit Werten aus der Betriebssystem- oder Anwendungsumgebung des Anwenders aber auch mit Informationen aus der Datenbank initialisiert werden. Durch den Zugriff auf den Application Context vermeiden Policy-Funktionen dann unnötige Mehrfachausführungen von Programmcode oder aufwendige SQL-Zugriffe.

Ein Application Context kann im Bereich des Arbeitsspeichers eines Anwenders angelegt werden. Er kann aber auch in einem Bereich angelegt werden, der für alle Benutzer verfügbar ist. Dann spricht man von sogenannten Global Application Contexts. Global Application Contexts unterstützen das Connection Pooling, Multitier-Anwendungen oder Anwendungen, die ihre eigene Authentifizierung vornehmen und selbst als Datenbankbenutzer agieren.

Anhand eines Beispiels soll die Verwendung von Policies und Application Contexts im Rahmen von VPD vorgestellt werden. Das Beispiel basiert auf den Tabellen EMPLOYEES, ORDERS und CUSTOMERS aus den Beispielschemas HR und OE. Beide Schemas sind im Lieferumfang der Datenbank enthalten und in der folgenden Graphik ausschnittweise dargestellt.

Die Tabellen des Beispiels
Zum Vergrößern auf das Bild klicken, zurück zum Artikel mit dem Back-Button des Browser

Für das Beispiel werden die Tabellen modifiziert. Um eindeutige Nummernkreise für Angestellten- und Kundennummern (employee_id, customer_id) zu erhalten, werden die Kundennummern in ORDERS und CUSTOMERS mit 10 multipliziert. Dazu wird das Foreign Key Constraint auf ORDERS.CUSTOMER_ID zuvor mit DROP gelöscht. Ausserdem wird der Zugriff auf die Tabelle ORDERS für Verkäufer und Kunden freigegeben.

Aufgabenstellung

Die Auftragsdaten aus ORDERS sollen nur von den Verkäufern, die den Auftrag abgeschlossen haben, lesbar sein. Ausserdem sollen Kunden ihre eigenen Auftragsdaten lesen dürfen.

Der Lösungsweg sieht so aus, dass im Rahmen eines Logon-Triggers Variablen eines Application Context gefüllt werden, die dann im zweiten Schritt für die Policies gebraucht werden. Dabei wird davon ausgegangen, dass die Verkäufer sich mit ihrem Emailnamen einloggen. Dieser Emailname wird in der Tabelle EMPLOYEES in der Spalte EMAIL gelistet und besteht aus dem 1. Buchstaben des Vornamens und dem vollständigen Nachnamen. Analog sind die Kundenzugänge über Benutzernamen möglich, die ebenfalls nach diesem Muster aufgebaut sind.

Application Contexts und Logon-Trigger anlegen

Da das Anlegen der folgenden Objekte mehr als die üblichen Benutzerprivilegien erfordert, sollen die Objekte des Beispiels der Einfachheit halber von einem DBA angelegt werden, hier vom Benutzer SYSTEM.

Der Application Context wird mit folgendem Befehl angelegt

CREATE OR REPLACE CONTEXT initvpd USING initpack
initpack ist der Name eines Package, das zum Erstellen und Füllen der Variablen des Application Context genutzt wird. Das Package wird folgendermassen erstellt.
CREATE OR REPLACE PACKAGE BODY initpack IS
  PROCEDURE benutzerdaten IS
    v_benutzer_id  number(6);
  BEGIN
    SELECT employee_id INTO v_benutzer_id 
    FROM hr.employees 
    WHERE email = SYS_CONTEXT('USERENV', 'SESSION_USER');
    DBMS_SESSION.SET_CONTEXT('initvpd', 'benutzer_id', v_benutzer_id);
  EXCEPTION
    WHEN NO_DATA_FOUND THEN 
    SELECT customer_id INTO v_benutzer_id 
    FROM oe.customers 
    WHERE upper(substr(cust_first_name,1,1)||cust_last_name) = SYS_CONTEXT('USERENV', 'SESSION_USER');
    DBMS_SESSION.SET_CONTEXT('initvpd', 'benutzer_id', v_benutzer_id);
-- Der Einfachheit halber
    WHEN OTHERS THEN null;
  END;
END;
/
Zur Erläuterung: Nach der Deklaration der Variablen V_BENUTZER_ID wird in der Prozedur zunächst die Angestelltennummer (employee_id) des Verkäufers gesucht. Dies geschieht mit der im Lieferumfang der Datenbank enthaltenen Funktion SYS_CONTEXT. Liefert das SELECT ein Ergebnis, wird dieses Ergebnis durch den Aufruf von DBMS_SESSION.SET_CONTEXT dem Application Context INITVPD in der Variablen BENUTZER_ID zur Verfügung gestellt. Scheitert das SELECT, handelt es sich offenbar nicht um einen Verkäufer, sondern um einen Kunden. Dessen Kundennummer wird im EXCEPTION HANDLER ermittelt und der Variablen BENUTZER_ID des Context INITVPD zugewiesen. Das bedeutet, dass diese Variable immer einen Wert hat, sofern ein Verkäufer oder ein Kunde diese Prozedur zur Ausführung bringt.

Die Prozedur initpack.benutzerdaten soll durch einen Logon-Trigger ausgeführt werden, der mit folgendem Befehl erstellt wird.
CREATE OR REPLACE TRIGGER vpd_init_trig AFTER LOGON ON DATABASE
  BEGIN
    system.initpack.benutzerdaten;
  EXCEPTION
-- Auch vereinfacht, um das Nachstellen zu erleichtern: Loggen sich Benutzer
-- ein, die nicht in EMPLOYEES / CUSTOMERS gelistet sind (z.B. SYSTEM, SYS!),
-- scheitert der Trigger nicht und ein solcher Benutzer kann sich einloggen.
    WHEN NO_DATA_FOUND THEN null;
END;
/
Policy-Funktion anlegen

Folgende Policy-Funktion führt zur Umsetzung der genannten Anforderungen.
CREATE OR REPLACE FUNCTION ord_policy (schemaname IN VARCHAR2, tabname IN VARCHAR2) 
  RETURN VARCHAR2 AS
    v_benutzer_id  NUMBER;
    bedingung      VARCHAR2(200);
  BEGIN
    SELECT sys_context('initvpd', 'benutzer_id') 
            INTO v_benutzer_id 
    FROM dual;
     IF v_benutzer_id IS NOT null THEN
       bedingung := 'orders.sales_rep_id = '|| v_benutzer_id ||' OR orders.customer_id = ' || v_benutzer_id ; 
-- Die letzte Vereinfachung damit z.B. SYSTEM noch abfragen kann   
     ELSE
       bedingung := '1=1';
     END IF;
  RETURN bedingung;
END ord_policy;
/
Zur Erläuterung: Nach der Deklaration der Variablen liest das SELECT über die Funktion SYS_CONTEXT die Variable BENUTZER_ID aus dem Application Context INITVPD in die lokale Variable V_BENUTZER_ID. Der Variablen BEDINGUNG wird ein Wert in Form einer Zeichenkette zugewiesen, in die wiederum die Variable V_BENUTZER_ID eingebaut wird. Die so entstandene Zeichenkette kann als als WHERE-Bedingung durch die VPD genutzt werden.

Die Funktion wird mit den Rechten des Eigentümers ausgeführt. Ihr stehen nicht nur die Möglichkeiten von PL/SQL zur Verfügung, sondern es ist auch möglich, C- oder Java-Programme einzubinden. Da der Code der Policy-Funktion zusätzlich ausgeführt werden muss, ist es sinnvoll, diesen Code so kompakt wie möglich zu gestalten.

Die Struktur der Funktion ist in zwei Punkten vorgegeben: Sie muss erstens zwei Eingabeparameter zur Verfügung stellen. Das System benötigt diese Informationen , um die Tabelle zu identifizieren, auf die die Funktion angewendet werden soll. Gleichzeitig hält die Parametrisierung die Möglichkeit offen, die Funktion auf unterschiedliche Tabellen anzuwenden. Die Parameter sind im Beispiel mit den sprechenden Namen SCHEMANAME und TABNAME angelegt. Zweitens muss die Funktion als Rückgabewert eine Zeichenkette liefern. In unserem Beispiel ist die Variable BEDINGUNG wie beschrieben für diese Zeichenkette vorgesehen.

Zwei Hinweise für die Praktiker:

1. Läßt sich die Funktion kompilieren und treten erst zur Laufzeit Probleme auf, erhält man durch einen Blick in die Trace-Datei (unter USER_DUMP_DEST) hilfreiche Informationen zur Fehleranalyse.
2. Bitte in der Funktion nicht auf die Tabelle zugreifen, für die die Funktion ausgeführt werden soll. Es treten sonst Fehler auf, die vergleichbar sind mit denen, die bei Triggern als mutating table bekannt sind.

Die Verbindung der Funktion mit einer Tabelle erfolgt über das Paket DBMS_RLS. SYSTEM hat Zugriff auf dieses Paket.
BEGIN
  DBMS_RLS.ADD_POLICY (object_schema   => 'oe',
                       object_name     => 'orders',
                       policy_function => 'ord_policy',
                       policy_name     => 'o_policy',
                       statement_types => 'select');
END;
/
Die Parameter OBJECT_SCHEMA, OBJECT_NAME und POLICY_FUNCTION legen fest, für welches Objekt die Policy gilt. Der Parameter POLICY_NAME gibt dieser Verbindung von Funktion und Tabelle einen Namen, hier O_POLICY. Unter diesem Namen kann die Policy in anderen Zusammenhängen, z. B. um sie zu löschen, angesprochen werden. Der Parameter STATEMENT_TYPES legt fest, dass die Funktion nur für den Befehl SELECT ausgeführt wird.

Das Ergebnis

Der folgende Screenshot zeigt, dass das identische Statement einmal ausgeführt von dem Verkäufer COLSEN und einmal ausgeführt durch den Kunden EFAWCETT unterschiedliche Ergebnisse liefern. Die VPD funktioniert. Trotz der scheinbar gleichen Abfrage wird die Ergebnismenge durch die unterschiedlichen WHERE-Bedingungen jeweils unterschiedlich: Jeder sieht nur die für ihn durch die WHERE-Bedingung gefilterten Datensätze.

Ein SELECT, unterschiedliche Ergebnisse

Zum Vergrößern auf das Bild klicken, zurück zum Artikel mit dem Back-Button des Browser

Weitere Hinweise

Das Beispiel zeigt nur eine Verwendungsmöglichkeit von VPD. Eine andere, die ebenso leicht aufzusetzen ist, könnte z.B. das Lesen aller Zeilen einer Tabelle grundsätzlich erlauben - außer wenn schützenswerte Spalten angesprochen werden. Hier entscheidet dann die unternehmensspezifische Praxis, ob durch den Zugriff auf eine schützenswerte Spalte der gesamte selektierte Satz nicht angezeigt wird, oder ob die Werte der geschützten Spalte als NULL dargestellt werden.

Die VPD-Infrastruktur ist für die Anwender nicht sichtbar - in der Oracle-Terminologie transparent. Die Policies werden auf alle Benutzer außer auf den Benutzer SYS angewendet: SYS hat als einziger das Privileg EXEMPT ACCESS POLICY. Allerdings kann sich ein 'normaler' Datenbankadministrator das Privileg zuweisen. Soll auf jeden Fall verhindert werden, dass Datenbankadministratoren auf Benutzerdaten zugreifen, geschieht das durch den Einsatz der Datenbankoption Oracle Database Vault.

Die VPD-Technologie wird eingesetzt für Anwendungen, in denen innerhalb eines Unternehmens mit unterschiedlichen Privilegien auf unterschiedliche Tabellenzeilen zugegriffen wird, bis hin zur Implementierung von mandantenfähigen Tabellen durch Outsourcing-Dienstleister, das heißt, dass Daten vieler Kunden in einer einzigen Tabelle gespeichert und damit das Anlegen separater Tabellen oder gar ganzer Datenbanken pro Kunde vermieden wird.

Weitere Informationen zum Thema VPD finden sich im Security Guide, im Oracle Technology Network oder in Form eines weiteren Beispiels auch auf den Seiten der Application Express Community.

Zurück zur Community-Seite