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

Источник: Oracle Magazine RE
Том Кайт

Часть 1

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

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

  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


    Страница сайта http://test.interface.ru
    Оригинал находится по адресу http://test.interface.ru/home.asp?artId=3527