Контексты приложений в PL/SQL на примере

Илья Дергунов

Илья Дергунов

Автор статьи. ИТ-специалист с 20 летним стажем, автор большого количества публикаций на профильную тематику (разработка ПО, администрирование, новостные заметки). Подробнее.

Контексты приложений в PL/SQL: SYS_CONTEXT, SET_CONTEXTПри обсуждении безопасности уровня строк в Oracle было сделано очень важное допущение: предикат (то есть условие, ограничивающее набор видимых строк таблицы) оставался неизменным. В рассмотренных примерах он базировался на коде отдела пользователя. Допустим, в системе вводится новое требование: теперь пользо­ватели просматривают записи работников на основании не кодов отделов, а специально ведущихся для этой цели списков привилегий. В таблице EMP_ACCESS хранится инфор­мация о том, кому из пользователей разрешено работать с теми или иными данными.


Оглавление статьи[Показать]


SQL> DESC emp_access
 Name              Null?    Type
 ----------------- -------- ------------
 USERNAME                   VARCHAR2(30)
 DEPTNO                     NUMBER
Примерный вид данных:
USERNAME                           DEPTNO
------------------------------ ----------
MARTIN                                 10
MARTIN                                 20
KING                                   20
KING                                   10
KING                                   30
KING                                   40

Пользователь Martin может просматривать данные отделов с кодами 10 и 20, а пользо­ватель King — данные отделов 10, 20, 30 и 40. Если имя пользователя не указано в таб­лице, он не может просматривать записи. Новое правило требует, чтобы предикаты генерировались динамически в функции политики.

Кроме того, должна быть предусмотрена возможность динамического изменения при­вилегий пользователей посредством обновления таблицы EMP_ACCESS, и пользователя при этом не следует заставлять заново подключаться к базе данных. Следовательно, триггер LOGON нам не поможет.

Одно из возможных решений заключается в создании пакета с переменной, в которой хранится предикат; пользователю предоставляется возможность выполнения сегмента кода PL/SQL, присваивающего значение переменной. Внутри функции политики значе­ние пакетной переменной используется в качестве предиката. Насколько приемлемо это решение? Подумайте хорошенько: если пользователь может присвоить другое значение пакетной переменной, что помешает ему присвоить привилегированное значение? Поль­зователь подключается к базе данных, присваивает переменной значение, открывающее доступ ко всем данным, после чего выполняет выборку и видит все записи. Отсутствие безопасности делает этот вариант неприемлемым. Собственно, этот сценарий наглядно демонстрирует, почему код задания значений переменных следует размещать в триггере LOGON, где пользователь не сможет внести изменения.

 

Использование контекстов приложений

Вероятность того, что пользователь может динамически изменить пакетную переменную, заставляет переосмыслить стратегию. Нам необходим механизм задания глобальной переменной неким безопасным механизмом, исключающим несанкционированные из­менения. К счастью, Oracle предоставляет такую возможность. Контекст приложения аналогичен глобальной пакетной переменной; после задания значения он остается до­ступным на протяжении всего сеанса. Впрочем, на этом сходство кончается. Важнейшее различие заключается в том, что в отличие от пакетной переменной, контекст прило­жения не задается простым присваиванием; для изменения значения необходим вызов процедуры — и это обстоятельство делает этот вариант более безопасным.

Как и структуры языка C или записи PL/SQL, контекст приложения обладает атрибу­тами, которым присваиваются значения. Однако в отличие от аналогов из C и PL/SQL, имена атрибутов не фиксируются при создании контекста; это происходит во время выполнения. Контексты приложений по умолчанию хранятся в области PGA, если только они не определены как глобальные. Так как область PGA содержит приватные данные сеанса, хранящиеся в ней значения остаются невидимыми для других сеансов. В следующем примере команда CREATE CONTEXT используется для определения нового контекста с именем dept_ctx:

SQL> CREATE CONTEXT dept_ctx USING set_dept_ctx;

Context created.

Секция USING set_dept_ctx означает, что существует процедура с именем set_dept_ctx, и только эта процедура может изменять атрибуты контекстаdept_ctx. Никаким другим способом атрибуты изменяться не могут. У созданного контекста еще нет атрибутов — пока мы просто определили общий контекст (имя и безопасный механизм изменения). На следующем шаге необходимо создать процедуру, в которой атрибутам контекста будут присваиваться значения с использованием функции SET_CONTEXT встроенного пакета DBMS_SESSI0N, как показано в следующем примере:

PROCEDURE set_dept_ctx (p_attr IN VARCHAR2, p_val IN VARCHAR2)
IS
BEGIN
   DBMS_SESSION.set_context ('DEPT_CTX', p_attr, p_val);
END;

Чтобы присвоить атрибуту DEPTNO значение 10, мы выполняем следующую команду:

SQL> EXEC set_dept_ctx ('DEPTNO','10')

PL/SQL procedure successfully completed.

Для получения текущего значения атрибута вызывается функция SYS_CONTEXT, которая получает два параметра: имя контекста и имя атрибута. Пример:

SQL> DECLARE
  2     l_ret   VARCHAR2 (20);
  3  BEGIN
  4     l_ret := SYS_CONTEXT ('DEPT_CTX', 'DEPTNO');
  5     DBMS_OUTPUT.put_line ('Value of DEPTNO = ' || l_ret);
  6  END;
  7  /

Value of DEPTNO = 10

Функция также может использоваться для получения некоторых предопределенных контекстов, например IP-адресов и терминалов клиентов:

BEGIN
   DBMS_OUTPUT.put_line (   'The Terminal ID is '
                         || SYS_CONTEXT ('USERENV', 'TERMINAL')
                        );
END;

Результат:

The Terminal ID is pts/0

В этом фрагменте используется предопределенный контекстUSERENV, обладающий такими атрибутами, как TERMINAL, IP_ADDRESS, OS_USER и т. д. Значения этих атрибутов присваиваются автоматически, и изменить их в приложении невозможно — допускается только чтение данных.

 

Безопасность в контекстах

В сущности, работа процедуры set_dept_ctx сводится к вызову готовой программы DBMS_SESSION.SET_CONTEXT с соответствующими параметрами. Зачем определять для этого процедуру? Почему бы не вызвать встроенную функцию напрямую? Давайте посмотрим, что произойдет, если пользователь попытается в том же сегменте кода при­своить значение атрибуту DEPTNO:

SQL> BEGIN
  2     DBMS_SESSION.set_context
  3        ('DEPT_CTX', 'DEPTNO',10);
  4  END;
  5  /
begin
*
ERROR at line 1:
ORA-01031: insufficient privileges
ORA-06512: at "SYS.DBMS_SESSION", line 82
ORA-06512: at line 2

Ошибка ORA-01031 выглядит довольно странно: пользователь Martin обладает приви­легией EXECUTE для DBMS_SESSION, так что проблема, очевидно, не в нехватке привилегий. Вы можете убедиться в этом, заново предоставив привилегиюEXECUTE для пакета и по­вторно выполнив тот же сегмент кода; вы получите ту же ошибку.

Дело в том, что контексты приложений не могут изменяться прямыми обращениями к встроенному пакету. Все модификации должны осуществляться программным блоком, связанным с контекстом при его создании. Такой блок называется доверенной програм­мой контекста приложения.

При создании контекста приложения необходимо указать его доверенную про­грамму. Только доверенная программа может задавать значения в этом контексте, но не в других контекстах.

 

Контексты как предикаты в RLS

Итак, для изменения значений атрибутов контекста должна использоваться процедура. Не приводит ли это к бессмысленному увеличению сложности программы?

Нет, потому что доверенная процедура — единственный механизм изменения контекста — играет роль «стража» для работы с контекстом. В ней могут выполняться сколь угодно сложные действия по аутентификации и проверке данных, гарантирующие действитель­ность присваивания. Мы даже можем полностью отказаться от передачи параметров и задавать их на основании предопределенных значений без получения данных от пользователя. Например, из поставленных требований известно, что контексту должна быть присвоена строка с кодами отделов, прочитанными из таблицыEMP_ACCESS (а не передаваемыми пользователем!). Затем контекст приложения используется в функции политики. Давайте посмотрим, как реализовать это требование.

Сначала необходимо внести изменения в функцию политики:

/* File on web: authorized_emps_3.sql */
 1    FUNCTION authorized_emps (
 2       p_schema_name   IN   VARCHAR2,
 3       p_object_name   IN   VARCHAR2
 4    )
 5       RETURN VARCHAR2
 6    IS
 7       l_deptno       NUMBER;
 8       l_return_val   VARCHAR2 (2000);
 9    BEGIN
10       IF (p_schema_name = USER)
11       THEN
12          l_return_val := NULL;
13       ELSE
14          l_return_val := SYS_CONTEXT ('DEPT_CTX', 'DEPTNO_LIST');
15       END IF;
16
17       RETURN l_return_val;
18     END;

Функция политики предполагает, что коды отделов будут передаваться через атрибут DEPTN0_LIST контекста dept_ctx (строка 14). Чтобы задать значение атрибута, необходимо внести изменения в доверенную процедуру контекста:

/* File on web: set_dept_ctx_2.sql */
 1    PROCEDURE set_dept_ctx
 2    IS
 3       l_str   VARCHAR2 (2000);
 4       l_ret   VARCHAR2 (2000);
 5    BEGIN
 6       FOR deptrec IN (SELECT deptno
 7                         FROM emp_access
 8                        WHERE username = USER)
 9       LOOP
10          l_str := l_str || deptrec.deptno || ',';
11       END LOOP;
12
13       IF l_str IS NULL
14       THEN
15          -- No access records found, so no records
16          -- should be visible to this user.
17          l_ret := '1=2';
18       ELSE
19          l_str := RTRIM (l_str, ',');
20          l_ret := 'DEPTNO IN (' || l_str || ')';
21          DBMS_SESSION.set_context ('DEPT_CTX', 'DEPTNO_LIST', l_ret);
22       END IF;
23   END;

Пора протестировать функцию. Сначала пользователь Martin подключается к базе данных и подсчитывает количество работников. Перед выдачей запроса он должен установить контекст:

SQL> EXEC rlsowner.set_dept_ctx

PL/SQL procedure successfully completed.

SQL> SELECT SYS_CONTEXT ('DEPT_CTX', 'DEPTNO_LIST') FROM DUAL;

SYS_CONTEXT('DEPT_CTX','DEPTNO_LIST')
-------------------------------------
DEPTNO IN (20,10)

SQL> SELECT DISTINCT deptno FROM hr.emp;

    DEPTNO
----------
        10
        20

В соответствии с таблицей EMP_ACCESS ему видны только данные работников отделов 10 и 20. Допустим, в данных Martin номер отдела меняется на 30. Администратор вносит соответствующие изменения в таблицу:

SQL> DELETE emp_access WHERE username = 'MARTIN';

2 rows deleted.

SQL> INSERT INTO emp_access VALUES ('MARTIN',30);

1 row created.

SQL> COMMIT;

Commit complete.

Теперь при выполнении тех же запросов Martin получит другой результат:

SQL> EXEC rlsowner.set_dept_ctx

PL/SQL procedure successfully completed.

SQL> SELECT SYS_CONTEXT ('DEPT_CTX','DEPTNO_LIST') FROM DUAL;

SYS_CONTEXT('DEPT_CTX','DEPTNO_LIST')
-------------------------------------------------------------

DEPTNO IN (30)

SQL> SELECT DISTINCT deptno FROM hr.emp;

    DEPTNO
----------
        30

Изменения вступают в силу автоматически. Поскольку Martin не задает атрибуты кон­текста вручную, такое решение по своей природе более безопасно, чем задание глобаль­ной переменной. Кроме того, контекстная политика RLS в Oracle10g и более поздних версиях также способствует повышению производительности. Функция политики выполняется только при изменении контекста, а между изменениями используются кэшированные значения. Тем самым обеспечивается более высокая скорость работы этой политики по сравнению с используемой по умолчанию динамической политикой. Чтобы определить политику как контекстную, следует передать процедуре DBMS_RLS. add_policy дополнительный параметр:

policy_type => DBMS_RLS.context_sensitive

 Итак, чем же этот метод отличается от создания динамически сгенерированной функ­ции политики для таблицы emp_access? В случае функции политики необходимо ее выполнение для получения значения предиката (списка отделов в нашей ситуации). Предположим, имеется таблица, к которой обращены миллионы запросов; функция политики будет выполняться многократно, при этом каждый раз будет происходить обращение к таблице emp_access — с печальными последствиями для производитель­ности. Политику можно определить как статическую, чтобы функция не выполнялась так часто, но тогда при изменении записей emp_access функция политики не будет учитывать изменения и выдаст неправильный результат. Определение контекстной политики в контексте приложения решает обе проблемы — функция политики будет выполняться заново при изменении контекстного значения. Контекстные значения хранятся в памяти, поэтому обращения к ним происходят очень быстро.

ВOracle Database 12c контекстная политика предоставляет еще больше преимуществ. Кроме параметра policy_type, можно передать еще два параметра — namespace (простран­ство имен, а проще говоря — имя) и attribute (атрибут контекста, изменение которого должно приводить к повторному выполнению функции политики). В приведенном выше примере я использовал контекст DEPT_CTX, а конкретнее — атрибут с именемDEPTN0_LIST, для вывода списка номеров отделов, которые разрешено видеть пользователю. Функцию политики можно определить так, чтобы она повторно выполнялась только при изменении списка отделов. Для этого в политику добавляются зависимости от контекста и атрибута:

BEGIN
 DBMS_RLS.alter_policy (object_schema   => 'HR',
                        object_name     => 'EMP',
                        policy_name     => 'EMP_POLICY',
                        alter_option    =>  DBMS_RLS.add_attribute_association,
                        namespace       => 'DEPTNO_CTX',
                        attribute       => 'DEPTNO_LIST');
END;

Пример полного вызова процедуры add_policy:

BEGIN
   DBMS_RLS.add_policy (object_schema              => 'HR',
                        object_name                => 'EMP',
                        policy_name                => 'EMP_POLICY',
                        function_schema            => 'RLSOWNER',
                        policy_function            => 'AUTHORIZED_EMPS',
                        statement_types            => 'SELECT',
                        update_check               => TRUE,
                        sec_relevant_cols          => 'SAL, COMM',
                        sec_relevant_cols_opt      => DBMS_RLS.all_rows,
                        policy_type                => DBMS_RLS.context_sensitive,
                        namespace                  => 'DEPT_CTX',
                        attribute                  => 'DEPTNO_LIST'
                       );
END;

 

Идентификация сторонних пользователей

Полезность контекстов приложений выходит далеко за рамки ситуаций, описанных выше. Самое важное применение контекстов приложения — возможность различать пользователей, которые не могут быть идентифицированы на уровне уникального се­анса. Этот сценарий типичен для веб-приложений, использующих пул подключений с одним пользователем (например, C0NNP00L). Веб-пользователи подключаются к сер­веру приложения, который в свою очередь использует одно из подключений пула для обращения к базе данных (рис. 1).

 

Пользователи приложения и RLS

Рис. 1. Пользователи приложения и RLS

 

В этой схеме пользователи Martin и King не являются пользователями базы данных; это веб-пользователи, и базе данных о них ничего не известно. Пул подключений связы­вается с базой данных с идентификаторомCONNPOOL, который является пользователем базы данных. Когда Martin запрашивает информацию из базы данных, пул должен решить использовать для выборки подключение 1. После завершения запроса под­ключение освобождается. Если в этот момент пользователь King запросит данные, пул может решить использовать то же подключение (1). Соответственно, с точки зрения базы данных сеанс (а на самом деле подключение из пула) принадлежит пользователю CONNPOOL. По этой причине в приведенных ранее примерах (в которых использовалась функция USER) идентификация пользователя работать не будет. Функция USER всегда возвращает CONNPOOL, потому что к базе данных подключен именно этот пользователь. На помощь приходят контексты приложения. Предположим, имеется контекст с именем WEB_CTX и атрибутом WEBUSER. Клиент присваивает ему имя фактического пользователя (например, MARTIN) при отправке запроса к пулу подключений:

BEGIN
   set_web_ctx ('WEBUSER', 'MARTIN');
END;

Политика RLS может базироваться на этом значении вместо имени пользователя базы данных. В таком случае функция политики будет выглядеть немного иначе:


 1    FUNCTION authorized_emps (
 2       p_schema_name   IN   VARCHAR2,
 3       p_object_name   IN   VARCHAR2
 4    )
 5       RETURN VARCHAR2
 6    IS
 7       l_deptno       NUMBER;
 8       l_return_val   VARCHAR2 (2000);
 9    BEGIN
10       IF (p_schema_name = USER)
11       THEN
12          l_return_val := NULL;
13       ELSE
14          SELECT deptno
15            INTO l_deptno
16            FROM emp
17           WHERE ename = SYS_CONTEXT ('WEB_CTX', 'WEBUSER');
18
19          l_return_val := 'DEPTNO = ' || l_deptno;
20       END IF;
21
22       RETURN l_return_val;
23    END;

Обратите внимание на строку 17. В исходной версии кода она выглядела так:

WHERE ename = USER;

А теперь выглядит так:

WHERE ename = SYS_CONTEXT ('WEB_CTX','WEBUSER');

Это условие получает имя веб-пользователя и сравнивает его со столбцом ENAME.

 

Основные принципы контекстов приложений

 

Вас заинтересует / Intresting for you:

Управление приложениями PL/SQL...
Управление приложениями PL/SQL... 4651 просмотров Stas Belkov Thu, 16 Jul 2020, 06:20:48
Встроенные методы коллекций PL...
Встроенные методы коллекций PL... 14849 просмотров sepia Tue, 29 Oct 2019, 09:54:01
Символьные функции и аргументы...
Символьные функции и аргументы... 18590 просмотров Анатолий Wed, 23 May 2018, 18:54:01
Тип данных BOOLEAN в PL/SQL
Тип данных BOOLEAN в PL/SQL 10920 просмотров Doctor Thu, 12 Jul 2018, 08:40:40
Войдите чтобы комментировать