СТАТЬЯ
11.01.02

<< Часть 1

Детальный контроль доступа и контексты приложения (Часть 2)

© Том Кайт
Статья была опубликована в журнале OracleMagazine

Важное предупреждение

Существует одна важная особенность реализации вышеописанной предикатной функции безопасности - фактически во время заданной сессии эта функция обязана возвращать постоянный предикат - это существенно. Если посмотреть на описанную выше функцию еще раз, видно логику:

  3  as
4  begin
5      if ( user = 'RLS_ADMIN' ) then
6          return '';
7      else
8          return 'owner = USER';
9      end if;
10  end;

Эта предикатная функция либо не возвращает никакого предиката, либо возвращает "owner = USER". Во время заданной сессии она постоянно будет возвращать один и тот же предикат. Ситуация, когда получен предикат "owner = USER", а затем в этой же сессии - пустой предикат "", возникнуть не может. Для того, чтобы понять, почему это крайне необходимо для корректного проектирования приложений с детальным контролем доступа, следует понять, когда предикат связывается с запросом и как различные среды обрабатывают эту ситуацию.

Предположим, что написана следующая предикатная функция:

SQL> create or replace function rls_examp
 
2  ( p_schema in varchar2, p_object in varchar2 )
 
3  return varchar2
 
4  as
 
5  begin
 
6          if ( sys_context( 'myctx', 'x' ) is not null )
 
7          then
 
8                  return 'x > 0';
 
9          else
 
10                  return '1=0';
 
11          end if;
 
12  end;
/

Это показывает, что если атрибут "x" контекста установлен, то предикат должен иметь значение "x > 0". Если атрибут "x" контекста не установлен, то предикат должен быть "1=0". При создании таблицы T, добавьте в нее данные, политику и контекст так, как показано ниже:

SQL> create table t ( x int );
Table created.
SQL> insert into t values ( 1234 );
1 row created.
SQL> begin
 
2     dbms_rls.add_policy
 
3     ( object_schema   => user, object_name => 'T',
 
4       policy_name => 'T_POLICY', function_schema => user,
 
5       policy_function => 'rls_examp', statement_types => 'select' );
 
6  end;
 
7  /
PL/SQL procedure successfully completed.
SQL> create or replace procedure set_ctx( p_val in varchar2 )
 
2  as
 
3  begin
 
4          dbms_session.set_context( 'myctx', 'x', p_val );
 
5  end;
 
6  /
Procedure created.
SQL> create or replace context myctx using set_ctx;
Context created.

Такая политика означает, что если контекст установлен, можно будет увидеть 1 строку. Если контекст не установлен, ни одной строки не будет видно. Действительно, если провести тест в SQLPLUS, непосредственно выполняя SQL, то получится следующий результат:

SQL> exec set_ctx( null );
PL/SQL procedure successfully completed.
SQL> select * from t;
no rows selected
SQL> exec set_ctx( 1 ) ;
PL/SQL procedure successfully completed.
SQL> select * from t;
     X
---------
  1234

Таким образом, выбрались те данные, которые ожидались. Динамический предикат работает так, как ожидалось. В действительности же, если использовать PL/SQL (Pro*C или приложения, написанные на OCI, а также многие другие исполняемые среды) обнаруживается, что вышеописанный результат неверен. Создадим, например, небольшую PL/SQL-процедуру:

SQL> create or replace procedure dump_t
 
2  ( some_input in number default NULL )
 
3  as
 
4  begin
 
5          dbms_output.put_line
 
6          ('*** Результат работы SELECT * FROM T' );
 
 7 
 
 8          for x in (select * from t ) loop
 
9                  dbms_output.put_line( x.x );
 
10          end loop;
 
11 
 
12 
 
13          if ( some_input is not null )
 
14          then
 
15                  dbms_output.put_line
 
16                  ('*** Результат работы другого SELECT * FROM T' );
 
17 
 
18                  for x in (select * from t ) loop
 
19                          dbms_output.put_line( x.x );
 
20                  end loop;
 
21          end if;
 
22  end;
 
23  /
Procedure created.

В первый раз простой "select * from T" в этой процедуре выполняется, когда входной параметр не задан, и во второй раз, когда задано его некоторое значение. Давайте выполним эту процедуру и посмотрим результат:

SQL> -- Включим вывод на экран результат dbms_output.put_line
SQL> set serveroutput on
SQL> -- отменим установку контекста - присвоим X значение NULL
SQL> exec set_ctx( NULL )
PL/SQL procedure successfully completed.
SQL> -- выполним процедуру. Заметьте, что
SQL> -- some_input по умолчанию может быть NULL.
SQL> -- Выполнится только 1-ый select * from t.
SQL> -- Как и ожидалось, выбрано НОЛЬ строк, так как
SQL> -- использовался предикат 1=0
SQL> exec dump_t
*** Результат работы SELECT * FROM T
PL/SQL procedure successfully completed.
SQL> -- Теперь установим значение контекста в ненулевое значение.
SQL> exec set_ctx( 1 )
PL/SQL procedure successfully completed.
SQL> -- Так как таблица t содержит 1 строку со значением 1234, а
SQL> -- предикат должен быть "x > 0", когда этот атрибут установлен, то
SQL> -- для получения данных можно выполнить запрос к таблице T.
SQL> --
SQL> -- Чтобы убедиться в том, что результат может оказаться неверным,
SQL> -- выполним процедуру dump_t с некоторым НЕНУЛУВЫМ входным параметром.
SQL> -- В этом случае выполнятся оба select * from T
SQL> --
SQL> -- Следует обратить внимание на то, что при первом выполнении
SQL> -- "select * from T" никакие данные не возвращаются!
SQL> -- А при втором - возвращаются!
SQL> --
SQL> -- Почему? Смотрите далее
SQL> exec dump_t( 0 )
*** Результат работы SELECT * FROM T
*** Результат работы другого SELECT * FROM T
1234
PL/SQL procedure successfully completed.

Итак, при запуске процедуры с атрибутом контекста "x", установленным в значение null, получен ожидаемый результат (так как в этой сессии процедура была запущена первый раз). Затем контекстный атрибут "x" был установлен в ненулевое значение, и результат получился "противоречивый". Первый select * from t в процедуре снова не возвратил ни одной строки - он, скорее всего, все еще использует предикат "1=0". Второй запрос (тот, что в первый раз не выполнялся) возвратил, казалось бы, корректный результат - он, как и ожидалось, использует предикат "x > 0",.

Почему первый запрос в этой процедуре не использовал предикат, который предполагался? Это произошло из-за оптимизации, называемой "кэширование курсора". На самом деле PL/SQL и многие другие исполняемые среды не закрывают курсор по команде “закрыть”. Вышеописанный пример может быть легко воспроизведен, например, в Pro*C, если опцию предкомпилятора "release_cursor" оставить в значении по умолчанию NO. Если тот же самый код перекомпилировать с опцией release_cursor=YES, то программа Pro*C будет вести себя более похоже на запросы в SQLPLUS. Предикат, используемый DBMS_RLS, связывается с запросом во время фазы PARSE. Первый запрос "select * from T" разбирается во время первого выполнения хранимой процедуры - когда предикат действительно был равен "1=0". Инструмент PL/SQL кэширует этот разобранный курсор. Во второй раз при выполнении хранимой процедуры PL/SQL просто повторно использует разобранный курсор из первого "select * from T", при этом разобранный запрос имеет предикат "1=0" - предикатная функция в этот момент вообще не вызывалась. Так как процедуре передаются также некоторые входные данные, PL/SQL выполнил второй запрос. Этот запрос, однако, уже не является открытым и разобранным, поэтому он разбирается во время его выполнения - когда контекстный атрибут НЕ ПУСТОЙ. Второй "select * from t" использует связанный предикат "x>0". Отсюда и противоречивость. Так как в общем случае контроль за кешированием этих курсоров не осуществляется, то предикатную функцию безопасности, возвращающую более 1 предиката за сессию, следует, во что бы то ни стало, избегать. В противном случае в будущем придется с большим трудом отыскивать ошибки приложения. В следующем примере я продемонстрирую, как построить предикатную функцию безопасности, которая не сможет возвратить более одного предиката за сессию. В этом случае гарантируется, что:

Я настоятельно советую создавать все нетривиальные предикатные функции так же, как показано в следующем примере. Это позволит создавать хорошо работающие, предикабельные приложения, поддерживающие детальный контроль доступа.

Следует добавить, что в некоторых случаях изменение предиката в середине сессии желательно. Для достижения наилучших результатов клиентские приложения, имеющие доступ к объектам, которые поддерживаются политикой, позволяющей изменять предикаты в середине сессии, должны быть написаны в особой форме. Например, во избежание кэширования курсора в PL/SQL необходимо написать приложение, полностью использующее динамический sql. Если используется этот динамический предикатный метод, то необходимо иметь в виду, что результаты будут зависеть от того, как написано клиентское приложение. Поэтому не следует применять политику безопасности с использованием этой возможности. Мы не будем рассматривать использование всех возможностей DBMS_RLS, а лучше сконцентрируемся на ее конкретном использовании - для защиты данных.

Пример 2. Использование контекстов приложения

Необходимо, например, реализовать политику безопасности в подсистеме “Кадры” (Human Resources Security Policy ). В этом примере будут использоваться таблицы EMP и DEPT демонстрационного пользователя SCOTT/TIGER и добавится еще одна таблица, которая позволит назначить человека на должность контролера. Далее перечислены требования:

Как было сказано ранее, приложение будет использовать существующие таблицы EMP и DEPT пользователя SCOTT и добавочную таблицу HR_REPS для связи контролера с отделом. Схема будет выглядеть следующим образом:

SQL> -- создадим демонстрационную схему.  Она основывается на таблицах
SQL> -- EMP и DEPT, владелец которых scott.  Добавим в схему
SQL> -- описатель RI (идентификатор контролера) и переименуем
SQL> -- значения поля ENAME в таблице EMP
SQL> -- так, чтобы они соответствовали именам пользователей тестируемой
SQL> -- базы данных (например: пользователю RLS_KING соответствует имя
SQL> -- пользователя RLS_KING в таблице emp)
SQL> create table dept as select * from scott.dept;
Table created.
SQL> alter table dept add constraint dept_pk primary key(deptno);
Table altered.
SQL> create table emp_base_table as select * from scott.emp;
Table created.
SQL> update emp_base_table set ename = 'RLS_' || ename;
14 rows updated.
SQL> alter table emp_base_table add constraint emp_pk primary key(empno);
Table altered.
SQL> alter table emp_base_table add constraint emp_fk_to_dept
 
 2  foreign key (deptno) references dept(deptno);
Table altered.
SQL> -- создадим индексы, которые будут использоваться функцией
SQL> -- контекста приложения для повышения производительности.
SQL> -- Необходимо быстро определить, является ли
SQL> -- некоторый пользователь mgr (менеджером) отдела. Кроме того,
SQL> -- необходимо быстро конвертировать имя пользователя в empno
SQL> create index emp_mgr_deptno_idx on emp_base_table(mgr);
Index created.
SQL> create unique index emp_ename_idx on emp_base_table(ename);
Index created.
SQL> -- Кроме того, создадим представление EMP на основе запроса 
SQL> -- select * from emp_base_table. К этому ПРЕДСТАВЛЕНИЮ
SQL> -- будет применена политика, и через него
SQL> -- приложения будут запрашивать/вставлять/обновлять и так далее.
SQL> create view emp as select * from emp_base_table;
View created.
SQL> -- создадим таблицу для управления HR_REPS.  Для этого будет 
SQL> -- использоваться INDEX ORGANIZED TABLE, так как всегда будет
SQL> -- выполняться запрос только такого типа, как
SQL> -- "select * from hr_reps where username = X and deptno = Y".
SQL> -- В использовании таблицы нет необходимости, достаточно
SQL> -- использовать только индекс.
SQL> create table hr_reps
 
2  (   username    varchar2(30),
 
3      deptno        number,
 
4      primary key(username,deptno)
 
5  )
 
6  organization index;
Table created.
SQL> -- Свяжем HR Reps с отделами.  KING может видеть все отделы.
SQL> insert into hr_reps values ( 'RLS_JONES', 10 );
SQL> insert into hr_reps values ( 'RLS_BLAKE', 20 );
SQL> insert into hr_reps values ( 'RLS_CLARK', 30 );
SQL> insert into hr_reps values ( 'RLS_KING', 10 );
SQL> insert into hr_reps values ( 'RLS_KING', 20 );
SQL> insert into hr_reps values ( 'RLS_KING', 30 );
SQL> insert into hr_reps values ( 'RLS', 10 );
SQL> commit;
Commit complete.

 Часть 3 >>

Дополнительную информацию Вы можете получить в компании Interface Ltd.

Обсудить на форуме Oracle
Отправить ссылку на страницу по e-mail


Interface Ltd.
Тel/Fax: +7(095) 105-0049 (многоканальный)
Отправить E-Mail
http://www.interface.ru
Ваши замечания и предложения отправляйте автору
По техническим вопросам обращайтесь к вебмастеру
Документ опубликован: 11.01.02