Контексты приложений в PL/SQL: CREATE CONTEXT, SYS_CONTEXT

Стас Белков

Стас Белков

Автор статьи. Известный специалист в мире IT. Консультант по продуктам и решениям Oracle. Практикующий программист и администратор баз данных. Подробнее.

Контексты приложений в PL/SQL для безопасностиПри обсуждении безопасности уровня строк  было сделано очень важное допущение: предикат (то есть условие, ограничивающее набор видимых строк таблицы) оставался неизменным. В рассмотренных примерах он базировался на коде отдела пользователя. Допустим, в системе вводится новое требование: теперь пользователи просматривают записи работников на основании не кодов отделов, а специально ведущихся для этой цели списков привилегий. В таблице 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 может базироваться на этом значении вместо имени пользователя базы данных. В таком случае функция политики будет выглядеть немного иначе:

/* Файл в Сети:: authorized_emps_4.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          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...
Встроенные методы коллекций PL... 14734 просмотров sepia Tue, 29 Oct 2019, 09:54:01
Управление приложениями PL/SQL...
Управление приложениями PL/SQL... 4633 просмотров Stas Belkov Thu, 16 Jul 2020, 06:20:48
Общие сведения о безопасности ...
Общие сведения о безопасности ... 1816 просмотров Stas Belkov Fri, 12 Apr 2019, 05:54:15
Определение строковых констант...
Определение строковых констант... 3688 просмотров Дэйзи ак-Макарова Wed, 16 May 2018, 17:31:46
Войдите чтобы комментировать