Детальный контроль доступа и контексты приложения. Часть 2
Том Кайт
Часть 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, а лучше сконцентрируемся на ее конкретном использовании - для защиты данных.
Необходимо, например, реализовать политику безопасности в подсистеме "Кадры" (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
|