СТАТЬЯ |
11.01.02
|
Детальный контроль доступа и контексты приложения (Часть 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 onSQL> -- отменим установку контекста - присвоим 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, а лучше сконцентрируемся на ее конкретном использовании - для защиты данных.
Необходимо, например, реализовать политику безопасности в подсистеме “Кадры” (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> -- необходимо быстро конвертировать имя пользователя в empnoSQL> 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.
Дополнительную информацию Вы можете получить в компании Interface Ltd.
Обсудить на форуме
Oracle
Отправить ссылку на страницу по e-mail
Interface Ltd. Отправить E-Mail http://www.interface.ru |
|
Ваши
замечания и предложения отправляйте
автору По техническим вопросам обращайтесь к вебмастеру Документ опубликован: 11.01.02 |