[Tipy - triky] - Ve dvanáctém pokračování seriálu věnovaného praktickým tipům a trikům pro databázovou platformu Oracle Database se podíváme na řízení přístupu k datům na úrovni jednotlivých záznamů. Cílem seriálu je přinášet vám nejen praktické informace, které vám mohou pomoci při správě či vývoji na této platformě, ale také upozorňovat na zajímavé funkce. O kterých možná ani nevíte, že existují.
Jednou z důležitých funkcí databáze je řízení přístupu k datům. Dnešní aplikace ale už je málokdy vystačí s řízením přístupu na úrovni jednotlivých tabulek. Čím dál častěji je třeba zajistit detailnější řízení přístupu na úrovni jednotlivých záznamů. V dnešním pokračování si proto ukážeme, jak lze pro tyto účely použít mechanismus Virtual Private Database.
Důležitým pravidlem při řízení přístupu k datům je zajištění principu nejnižších možných práv – uživatel (a potažmo i aplikace či modul) by tedy měl mít přístup pouze k takovým datům a operacím, které jsou nezbytně nutné pro plnění jeho pracovní role. Čím více práv má uživatel nad rámec svých potřeb, tím větší škody může napáchat jejich zneužitím, nebo prostě omylem. To samé platí i pro práva, se kterými pracují jednotlivé aplikace – čím přesněji se podaří řídit přístup přímo na úrovni databáze, tím menší budou i následky zneužití případných bezpečnostních děr na úrovni aplikace (například pomocí SQL Injection).
Tradiční mechanismy objektových práv ale přestávají stačit. V jednodušších případech pomůže vytvoření pohledu. Jak ale jednoduše vynutit například tato pravidla při přístupu k seznamu objednávek?
- Zákazník má přístup jen ke svým objednávkám
- Obchodník má přístup jen k objednávkám, které sám prodal
- Manažer má mít přístup jen k objednávkám, které prodal on sám, nebo některý z jeho podřízených
Oracle na tyto požadavky reagoval už ve verzi databáze 8i vytvořením technologie Virtual Private Database. Je součástí Enterprise Edition. I když jde o relativně unikátní technologii, její základní logika je jednoduchá. Kdykoliv uživatel zadá dotaz pracující s chráněnou tabulkou, databáze automaticky připojí k SQL dotazu definovanou bezpečnostní podmínku. Teprve takto vzniklý výsledný dotaz je optimalizován a proveden. Bezpečnostní podmínka přitom není statická, je generována uloženou procedurou v době spuštění dotazu. Podmínka se tak může lišit v závislosti na aktuálním uživateli (jiná podmínka pro zaměstnance, jiná pro zákazníky), IP adrese klienta, nebo libovolných jiných faktorech.

Máme-li již základní objekty v databázi, stačí pro implementaci Virtual Private Database dva kroky:
- Napsat PL/SQL funkci vracející bezpečnostní podmínku
- Vytvořit bezpečnostní politiku, a tím spojit výše vytvořenou funkci s určitou tabulkou
Níže si celý postup ukážeme na příkladu vynucení již dříve uvedených pravidel pro přístup k seznamu objednávek. Pokud si postup chcete vyzkoušet, na konci článku najdete skript pro vytvoření základních objektů, na které budeme nyní aplikovat Virtual Private Database.
1. Vytvoření PL/SQL funkce generující bezpečnostní podmínku
Operaci proveďte jako vlastník dat – uživatel OBJEDNAVKY.
CREATE OR REPLACE FUNCTION obj_vpd_sec (
schema_name IN VARCHAR2,
table_name IN VARCHAR2)RETURN VARCHAR2 AS
l_user_id uzivatele.user_id%TYPE;
l_typ uzivatele.typ%TYPE;
l_podminka VARCHAR2(4000);
BEGIN
--Zjištění informací o aktuálním uživateli
SELECT user_id,typ
INTO l_user_id,l_typ
FROM uzivatele
WHERE username = sys_context('USERENV','SESSION_USER');
IF (l_typ=1 or l_typ=2) THEN
--Obchodník či manager smí pracovat s
--objednávkami, které prodal
l_podminka := l_podminka ||'(obchodnik='
|| l_user_id||')';
IF (l_typ = 2) THEN
-- Manager smí navíc pracovat s objednávkami
-- svých podřízených
l_podminka := l_podminka || ' OR obchodnik in ('
|| 'SELECT user_id
FROM objednavky.uzivatele '
|| ' CONNECT BY nadrizeny = PRIOR user_id'
|| ' START WITH nadrizeny ='
|| l_user_id || ')';
END IF;
ELSE
--Zákazník smí pracovat jen se svými objednávkami
l_podminka:= '(zakaznik=' || l_user_id ||')';
END IF;
RETURN l_podminka;
EXCEPTION
WHEN no_data_found THEN
-- Ani zákazník, ani zaměstnanec - nevracet žádná data
return '(1=2)';
END;
/
Funkce načte informace o aktuálním uživateli a podle typu uživatele vygeneruje text příslušné podmínky:
- Zákazník smí vidět jen své objednávky:
(zakaznik = <číslo uživatele>)
- Obchodník smí vidět jen objednávky, které sám prodal:
(obchodnik = <číslo uživatele>)
- Manažer smí vidět jen ty objednávky, které prodal on sám, nebo některý z jeho podřízených:
(obchodnik = <číslo uživatele>)
OR obchodnik in (
SELECT user_id
FROM objednavky.uzivatele
CONNECT BY nadrizeny = PRIOR user_id
START WITH nadrizeny = <číslo uživatele>
)
2. Vytvoření bezpečnostní politiky
Funkci generující bezpečnostní podmínky již máme. Nyní vytvoříme bezpečnostní politiku, a tím dříve vytvořenou funkci zaregistrujeme k tabulce OBJ. Operaci proveďte jako uživatel SYSTEM.
BEGIN
dbms_rls.add_policy(
object_schema=>'OBJEDNAVKY',
object_name => 'OBJ',
policy_name => 'OBJEDNAVKY_POLICY',
function_schema => 'OBJEDNAVKY',
policy_function => 'OBJ_VPD_SEC',
update_check => TRUE);
END;
/
Při každém dalším přístupu k tabulce OBJ se od teď použije bezpečnostní podmínka, kterou vrací procedura OBJ_VPD_SEC. Parametr update_check=TRUE zajistí, že se podmínka aplikuje nejen při čtení dat (ať již v rámci dotazu či čtení vstupních dat pro DML operaci), ale testují se i uživatelem měněná data. Uživatel ani nemůže vložit či změnit záznam tak, aby záznam odporoval bezpečnostní podmínce.
Test funkčnosti
Implementovaný mechanismus Virtual Private Database nyní vyzkoušíme. Ověříme, zda každý uživatel vidí pouze ty objednávky, které dle výše uvedených pravidel má vidět.
Manažer SEFICEK vidí objednávky, ve kterých figuruje jako obchodník, ale i ty, které prodali jeho podřízení (uživatelé 2 a 3).
CONNECT seficek@orcl
SELECT *
from OBJEDNAVKY.OBJ;
OBJ_ID ZAKAZNIK OBCHODNIK ZBOZI CENA
---------- ---------- ---------- ---------------- ----------
1 4 2 Propisky 2520
2 5 3 Bloky 3425
3 4 3 Ořezávátka 1250
4 5 2 Tužky 860
5 4 1 Propisky 2520
Obchodník NOVACEK (user_id=2) vidí objednávky, ve kterých figuruje jako obchodník.
CONNECT novacek@orcl
SELECT *
from OBJEDNAVKY.OBJ;
OBJ_ID ZAKAZNIK OBCHODNIK ZBOZI CENA
---------- ---------- ---------- ---------------- ----------
1 4 2 Propisky 2520
4 5 2 Tužky 860
Zákazník MALY (user_id=4) vidí jen svoje objednávky.
CONNECT maly@orcl
SELECT *
from OBJEDNAVKY.OBJ;
OBJ_ID ZAKAZNIK OBCHODNIK ZBOZI CENA
---------- ---------- ---------- ---------------- ----------
1 4 2 Propisky 2520
3 4 3 Ořezávátka 1250
5 4 1 Propisky 2520
Ostatní uživatelé databáze neuvidí v tabulce OBJEDNAVKY.OBJ žádná data. To se při stávající podobě funkce OBJ_VPD_SEC týká i vlastníka tabulek, uživatele OBJEDNAVKY. Pokud by měl vlastník tabulek vidět data, bylo by potřeba patřičně upravit funkci OBJ_VPD_SEC. Výjimku ve VPD má pouze uživatel SYS a uživatelé se systémovým právem EXEMPT ACCESS POLICY. Ti vidí vždy veškerá data.
Protože jsme při volání DBMS_RLS.ADD_POLICY použili parametr update_check=TRUE, omezuje se nejen rozsah záznamů, nad kterými se jednotlivé dotazy a DML operace provádějí, ale kontrolují se také hodnoty vkládané uživateli. Uživatel NOVACEK (user_id=2) tedy není schopný vložit objednávku, ve které není uvedený jako obchodník. Totéž platí i pro UPDATE.
CONNECT novacek@orcl
INSERT INTO objednavky.obj
(obj_id, zakaznik, obchodnik, zbozi, cena)
VALUES
(6,5,2,'Ořezávátka',1860);
1 row created.
INSERT INTO objednavky.obj
(obj_id, zakaznik, obchodnik, zbozi, cena)
VALUES
(7,5,3,'Ořezávátka',1860);
ERROR at line 1:
ORA-28115: policy with check option violation
UPDATE objednavky.obj
SET obchodnik = 1
WHERE obchodnik = 2;
ERROR at line 1:
ORA-28115: policy with check option violation
Uvedený příklad je samozřejmě zjednodušený tak, aby z něj bylo zřejmé základní použití Virtual Private Database. Při skutečném nasazení této technologie bychom nejspíš uvažovali ještě o některých dalších změnách z důvodů optimalizace:
- Pokud by PL/SQL funkce generující podmínku byla sama o sobě náročná na zdroje a přitom pro určité spojení vracela stále tutéž hodnotu, bylo by možné definovat politiku jako context-sensitive. Databáze by si pak pamatovala vrácenou hodnotu pro určité spojení a nevolala funkci při každém dotazu nad tabulkou.
- Stejně jako je obvykle dobré z bezpečnostních i výkonnostních důvodů nahradit hodnoty v dotazech vázanými proměnnými (bind variables), bylo by v tomto případě vhodné použít Application Context pro uložení hodnoty ID a typu uživatele v rámci databázového spojení. Hodnoty bychom mohli naplnit například v logon triggeru. V generované bezpečnostní podmínce bychom se pak na tyto hodnoty odkazovali funkcí SYS_CONTEXT.
- Pokud by se jako výkonnostní problém ukázal vnořený hierarchický dotaz, který je v bezpečnostní podmínce generován pro manažery, bylo by opět možné uvažovat o nacachování jeho výsledků – například v globální proměnné typu nested array uvnitř PL/SQL balíku. Generovaná PL/SQL podmínka by se pak odkazovala na obsah tohoto pole. Samozřejmě by to znamenalo, že případné změny v hierarchii zaměstnanců by se projevily až po dalším přihlášení.
Skript pro vytvoření základních objektů
Výše uvedený příklad využívá databázové objekty, které si můžete vytvořit následujícím skriptem. Tyto operace proveďte jako uživatel SYSTEM (nebo jiný DBA).
-- SCHÉMA pro aplikační objekty
CREATE USER objednavky IDENTIFIED BY objednavky
DEFAULT TABLESPACE users QUOTA UNLIMITED ON users;
GRANT CONNECT, CREATE TABLE, CREATE PROCEDURE TO objednavky;
-- UŽIVATELÉ - Zaměstnanci a zákazníci
CREATE USER seficek IDENTIFIED BY seficek;
CREATE USER novacek IDENTIFIED BY novacek;
CREATE USER maly IDENTIFIED BY maly;
GRANT CONNECT TO seficek,novacek,maly;
-- TABULKY A DATA
CREATE TABLE objednavky.uzivatele (
user_id number primary key,
username varchar2(30) unique,
jmeno varchar2(30),
-- typ:0=zakaznik, 1=obchodnik, 2=manager
typ number CHECK (typ IN (0,1,2)),
nadrizeny number references objednavky.uzivatele);
CREATE TABLE objednavky.obj (
obj_id number primary key,
zakaznik number references objednavky.uzivatele,
obchodnik number references objednavky.uzivatele,
zbozi varchar2(30),
cena number);
INSERT INTO objednavky.uzivatele
VALUES (1,'SEFICEK', 'Tomáš Šéfíček', 2,null);
INSERT INTO objednavky.uzivatele
VALUES (2,'NOVACEK','Petr Nováček', 1,1);
INSERT INTO objednavky.uzivatele
VALUES (3,'STARY','Zdeněk Starý',1,1);
INSERT INTO objednavky.uzivatele
VALUES (4,'MALY','Jiří Malý',0,null);
INSERT INTO objednavky.uzivatele
VALUES (5,'CHUDOBA','Pavel Chudoba',0,null);
INSERT INTO objednavky.obj VALUES (1,4,2,'Propisky',2520);
INSERT INTO objednavky.obj VALUES (2,5,3,'Bloky',3425);
INSERT INTO objednavky.obj VALUES (3,4,3,'Ořezávátka',1250);
INSERT INTO objednavky.obj VALUES (4,5,2,'Tužky',860);
INSERT INTO objednavky.obj VALUES (5,4,1,'Propisky',2520);
COMMIT;
-- Práva uživatelů na tabulku objednávek
GRANT select, insert, update, delete
ON objednavky.obj TO seficek, novacek, maly;
Dnešní ukázky si můžete vyzkoušet na Oracle Database Enterprise Edition. Tu si pro testovací účely můžete stahnout z Oracle Technology Network.
O autorovi
David Krch (*1976) pracuje ve společnosti Oracle Czech na pozici Technology Sales Consultant. Vystudoval obor Informační technologie na Vysoké škole ekonomické. Od roku 2001 působí ve firmě Oracle Czech na pozici Technology Sales Consultant, ve které je zodpovědný za podporu prodeje technologií Oracle. Z počátku se zaměřoval na portálová řešení. Od roku 2002 se specializuje na podporu prodeje databáze Oracle a témata související se zajištěním provozu, jako je bezpečnost a vysoká dostupnost. Jako nezávislý vývojář vyvíjel na zakázku databázové aplikace s využitím desktopových databází.