Drobnoziarnista kontrola dostępu (Fine-Grained Access Control – FGAC) jest popularną techniką kontroli dostępu użytkowników do danych na poziomie rekordów (w przeciwieństwie do kontroli dostępu na poziomie tabeli – realizowanej poleceniami GRANT), pozwalającą udostępniać różnym użytkownikom różne zestawy rekordów z tej samej tabeli (co daje efekt tzw. Virtual Private Database – VPD). Ogólna idea FGAC sprowadza się do implementacji funkcji PL/SQL, które generują treści predykatów selekcji – automatycznie i nieodwołalnie dołączanych do klauzuli WHERE każdego zapytania kierowanego do tabeli. Poniższa ilustracja przedstawia przebieg realizacji zapytania na tabeli objętej drobnoziarnistą kontrolą dostępu. Użytkownicy U1 i U2 początkowo kierują identyczne zapytania do tabeli. Serwer bazy danych Oracle Database automatycznie wywołuje funkcję generującą predykaty selekcji, a jej wyniki dokleja do klazul WHERE zapytań. W efekcie zapytania użytkowników U1 i U2 zostają odpowiednio zawężone np. tylko do rekordów, w których figuruje użytkownik o nazwie zgodnej z nazwą użytkownika wykonującego zapytanie. Stąd każdy z użytkowników uzyskuje inną odpowiedź na pozornie identyczne zapytania.
Programiści rozważający zastosowanie mechanizmów FGAC często pytają o konsekwencje wydajnościowe takiego rozwiązania. Przeprowadźmy zatem prosty eksperyment. Dla przykładowej tabeli PAYMENTS(PAYMENT_ID, AMOUNT, USER_ID) zdefiniujemy politykę FGAC pozwalającą użytkownikom na dostęp wyłącznie do tych rekordów, w których w kolumnie USER_ID znajduje się wartość zgodna z identyfikatorem użytkownika wykonującego zapytanie. Funkcja generująca predykaty selekcji mogłaby mieć następującą postać:
create function fgac_fun( schema_var in varchar2, table_var in varchar2) return varchar2
is
return_val varchar2(400);
begin
return_val := 'USER_ID = '''||SYS_CONTEXT('userenv','session_user')||'''';
return return_val;
end;
/
W celu użycia powyższej funkcji do drobnoziarnistej kontroli dostępu tworzymy politykę FGAC:
begin
dbms_rls.add_policy (
object_schema => 'system',
object_name => 'payments',
policy_name => 'payments_policy',
function_schema => 'system',
policy_function => 'fgac_fun',
statement_types => 'select, insert, update, delete'
);
end;
/
Od tego momentu każde zapytanie użytkownika do tabeli PAYMENTS zostanie automatycznie „wzbogacone” o predykat selekcji zwrócony dynamicznie przez funkcję FGAC_FUN. (Co ciekawe, administrator bazy danych nie zobaczy tak wzbogaconej treści w perspektywie V$SQL – należy dodatkowo sięgnąć do V$VPD_POLICY). Oto co widzą użytkownicy U1 i U2 wykonując zapytania „SELECT * FROM PAYMENTS”:
SQL> connect u1
SQL> select * from system.payments;
PAYMENT_ID AMOUNT USER_ID
---------- ---------- -------
1 950 U1
2 1500 U1
SQL> connect u2
SQL> select * from system.payments;
PAYMENT_ID AMOUNT USER_ID
---------- ---------- -------
3 300 U2
4 875 U2
A oto co widzi użytkownik SYS (spod polityk FGAC wyjęci są SYSDBA i posiadacze roli EXEMPT_ACCESS_POLICY):
SQL> connect sys as sysdba
SQL> select * from system.payments;
PAYMENT_ID AMOUNT USER_ID
---------- ---------- -------
1 950 U1
2 1500 U1
3 300 U2
4 875 U2
Wróćmy teraz do kwestii wydajności. Każde zapytanie skierowane do tabeli PAYMENTS powoduje obecnie uruchomienie funkcji FGAC_FUN, która dynamicznie generuje treść predykatu selekcji. To istotny narzut obliczeniowy – 1 000 000 zapytań spowoduje 1 000 000 wywołań/wykonań funkcji PL/SQL! Oto jak wyglądałyby wskaźniki wydajnościowe dla powtórzonego milion razy zapytania „SELECT COUNT(*) FROM PAYMENTS” (serwer Oracle Database 12.1.0.2, RHEL 7):
SQL ordered by CPU Time
CPU Time(s) Executions SQL Text
76.23 1,000,000 SELECT COUNT(*) FROM SYSTEM.PAYMENTS
33.11 1,000,000 begin :con := "FGAC_FUN"(:sn, ...
Zauważmy, że w tym prostym przykładzie czas wykonywania funkcji FGAC_FUN stanowi prawie połowę czasu (33s) wykonania zapytań (76s)! To bardzo duża rozrzutność…
Co z tym możemy zrobić? Na przykład możemy skorzystać z technik automatycznego buforowania wyników funkcji generującej predykaty selekcji. FGAC oferuje następujące poziomy buforowania [https://docs.oracle.com/database/121/DBSEG/vpd.htm]:
- DYNAMIC (domyślny, wykorzystywany w badanym systemie): funkcja generująca predykaty jest wywoływana za każdym razem, kiedy użytkownik wykonuje operację SQL na chronionej tabeli
- STATIC: funkcja generująca predykaty jest wywołana tylko raz, a jej wynik jest wykorzystywany przez wszystkich użytkowników instancji serwera bazy danych kiedy wykonują operację SQL na chronionej tabeli
- SHARED_STATIC: funkcja generująca predykaty jest wywołana tylko raz, a jej wynik jest wykorzystywany przez wszystkich użytkowników instancji serwera bazy danych kiedy wykonują operację SQL na wszystkich tabelach chronionych z użyciem tej funkcji
- CONTEXT_SENSITIVE: funkcja generująca predykaty jest wywołana gdy użytkownik(sesja) korzysta po raz pierwszy z chronionej tabeli, jej wynik jest buforowany i wykorzystywany ponownie pod warunkiem, że nie doszło do żadnych zmian w kontekstach aplikacyjnych [https://docs.oracle.com/database/121/DBSEG/app_context.htm]. Jeżeli nastąpi zmiana dowolnego kontekstu aplikacyjnego, to przy kolejnym odwołaniu użytkownika do chronionej tabeli, funkcja generująca predykatu zostanie wywołana ponownie. Istnieje możliwość ograniczenia wrażliwości tej metody na nieistotne zmienne i konteksty aplikacyjne (parametry „namespace” i „attribute”).
- SHARED_CONTEXT_SENSITIVE: jak wyżej lecz buforowany wynik może być zastosowany na wszystkich tabelach chronionych z użyciem tej funkcji
Ponieważ nasza funkcja opiera swoje działanie wyłącznie na zawartości kontekstu aplikacyjnego USERENV, to możemy bez obaw włączyć buforowanie na poziomie CONTEXT_SENSITIVE (parametr POLICY_TYPE):
begin
dbms_rls.drop_policy( object_schema => 'system',
object_name => 'payments',
policy_name => 'payments_policy');
end;
/
begin
dbms_rls.add_policy (
object_schema => 'system',
object_name => 'payments',
policy_name => 'payments_policy',
function_schema => 'system',
policy_function => 'fgac_fun',
statement_types => 'select, insert, update, delete',
policy_type => DBMS_RLS.CONTEXT_SENSITIVE
);
end;
/
Jak teraz wyglądałyby wskaźniki wydajnościowe dla wykonania miliona zapytań do tabeli chronionej przez FGAC? Sprawdźmy:
SQL ordered by CPU Time
CPU Time(s) Executions SQL Text
13.63 1,000,000 SELECT COUNT(*) FROM SYSTEM.PAYMENTS
0.00 1 begin :con := "FGAC_FUN"(:sn, ...
Oto efekt: 13 sekund zamiast 76 sekund (czas samego wykonania funkcji PL/SQL – niemierzalny)!!! Oczywiście nie zawsze możliwe jest buforowanie na poziomie CONTEXT_SENSITIVE (funkcje niedeterministyczne, funkcje wykonujące zapytania do tabel, itp.), jednak niewątpliwie należy dążyć do takiej implementacji funkcji generującej predykaty, która umożliwi ten poziom buforowania.
Wytrawni programiści będą pewnie kręcić nosem na zastosowanie FGAC do rozwiązania tak trywialnego problemu bezpieczeństwa i zgodnie ze „starą szkołą” po prostu zbudują perspektywę:
create view payments_restricted as select * from payments
where user_id=SYS_CONTEXT('userenv','session_user');
Jak szybko teraz działałyby nasze zapytania? Jak to zwykle bywa, stare rozwiązania wygrywają z nowymi…:
SQL ordered by CPU Time
CPU Time(s) Executions SQL Text
10.00 1,000,000 SELECT COUNT(*) FROM SYSTEM.PAYMENTS_RESTRICTED
Okrągłe 10 sekund zamiast wcześniejszych 13, czy nawet 76! Nie oznacza to jednak, że FGAC jest niepotrzebne – wystarczy wyobrazić sobie bardziej skomplikowany przykład uprawnień typu: „Użytkownicy z tego samego miasta, co oddział wystawiający fakturę mają dostęp do niej tylko w dni robocze w godzinach 9:00-17:00. Ograniczenie to nie dotyczy użytkowników pracujących w sieci lokalnej”.