Детальный контроль доступа и контексты приложения. Часть 3
Том Кайт
Часть 2
-
Теперь, когда для приложения созданы таблицы EMP, DEPT и HR_REPS, напишем процедуру, которая позволит установить значения контекста приложения. Контекст приложения будет состоять из двух элементов данных - EMPNO пользователя, присоединенного в данный момент, и его роль (одна из EMP, MGR или HR_REP). Процедура формирования динамического предиката будет использовать роль, хранящуюся в контексте приложения для того, чтобы определить, как должно выглядеть условие where для конкретного пользователя.
Для осуществления этой задачи используются таблицы EMP_BASE_TABLE и HR_REP. Тогда возникает вопрос "зачем использовать таблицу EMP_BASE_TABLE и представление EMP, если можно просто выполнить select * from emp_base_table?" По двум причинам:
-
Данные таблицы служащих используются для реализации политики безопасности.
-
Эта таблица считывается при создании контекста приложения.
Для того, чтобы прочитать данные о служащем, необходимо установить значения контекста приложения, для того, чтобы установить значения контекста приложения, необходимо прочитать данные о служащем. Это проблема курицы и яйца. Одним из вариантов ее решения является создание представления, которым будут пользоваться все приложения (представление EMP) и реализация безопасности этого представления. Для осуществления этой задачи политика безопасности будет пользоваться исходной таблицей EMP_BASE_TABLE. Из таблицы EMP_BASE_TABLE можно узнать, кто является менеджером некоторого отдела, и кто подчиняется некоторому пользователю. Приложение и конечные пользователи никогда не будут обращаться к таблице EMP_BASE_TABLE - она будет использоваться только политикой безопасности. После этого создается процедура установки значений контекста:
SQL> -- это процедура, "несущая ответственность" за SQL> -- создание контекста приложения. Она реализует SQL> -- политику путем установки привилегий, SQL> -- выданных пользователю в контексте приложения.
SQL> create or replace 2 procedure set_role( p_roleName in varchar2 default null ) 3 as 4 l_empno number; 5 l_cnt number; 6 l_roleName varchar2(40) default upper(p_roleName); 7 begin 8 if ( sys_context( 'Hr_App_Ctx', 'RoleName' ) is NOT NULL ) 9 then 10 -- прерывание запроса. Изменение роли требует изменения 11 -- предиката, связанного с запросом. Из-за кеширования курсора 12 -- в клиентском приложении нельзя гарантировать, что 13 -- не существует других уже разобранных запросов с 14 -- предикатами из текущей роли. 15 -- Если, например, роль уже установлена в значение MGR и 16 -- разобрано несколько запросов, а теперь попытаться 17 -- изменить роль на EMP, то разобранные запросы все еще 18 -- будут использовать предикаты MGR, а не EMP. 19 raise_application_error( -20001, 'Рольужеустановлена' ); 20 end if; 21 22 -- Так как используется пользователь СЕССИИ, а не ТЕКУЩИЙ 25 -- пользователь и, кроме того, empno текущего пользователя 23 -- используется достаточно часто, то он будет храниться в этом 24 --Контексте. Пользователь СЕССИИ - это имя присоединенного в 26 -- данный момент пользователя. ТЕКУЩИЙ пользователь - это имя 27 -- пользователя, который обладает привилегиями на выполнение 28 -- запроса, им может быть владелец процедуры, 29 -- т.е. не присоединенный пользователь!!! 30 select empno into l_empno 31 from emp_base_table 32 where ename = sys_context( 'userenv', 'session_user'); 33 34 dbms_session.set_context( 'Hr_App_Ctx', 'Empno', l_empno ); 35 36 if ( l_roleName = 'EMP' ) 37 then 38 -- любой может воспользоваться ролью EMP 39 dbms_session.set_context( 'Hr_App_Ctx', 'RoleName', 'EMP' ); 40 elsif ( l_roleName = 'MGR' ) 41 then 42 -- проверим, является ли пользователь MGR (менеджером), 43 -- если нет, то выдадим сообщение об ошибке и прервем выполнение. 44 -- Пользователь может попытаться снова. 45 select count(*) into l_cnt 46 from dual 47 where exists 48 ( select NULL 49 from emp_base_table 50 where mgr = to_number(sys_context('Hr_App_Ctx','Empno')) 51 ); 52 if ( l_cnt = 0 ) 53 then 54 raise_application_error( -20002, 'Вынеменеджер' ); 55 end if; 56 dbms_session.set_context( 'Hr_App_Ctx', 'RoleName', 'MGR' ); 57 elsif ( l_roleName = 'HR_REP' ) 58 then59 -- проверим, является ли пользователь HR_REP, если нет, 60 -- выдадим сообщение об ошибке и прервем выполнение. 61 -- Пользователь может попытаться снова. 62 select count(*) into l_cnt 63 from dual 64 where exists 65 ( select NULL 66 from hr_reps 67 where username = sys_context( 'userenv', 'session_user' ) 68 ); 69 70 if ( l_cnt = 0 ) 71 then 72 raise_application_error( -20002, 'Вынеконтролер' );
73 end if; 74 dbms_session.set_context( 'Hr_App_Ctx', 'RoleName', 'HR_REP' ); 75 else 76 raise_application_error( -20003, 'Роль ' // l_roleName // 77 ' невозможно распознать' ); 78 end if; 79 end; 80 / Procedure created.
SQL> grant execute on set_role to public 2 / Grant succeeded.
Итак, до этого момента сделано: создана процедура, которая принимает имя роли в виде параметра. В начале этой процедуры обеспечивается, чтобы атрибут RoleName еще не был установлен. Так как в политике безопасности будут возвращаться различные предикаты, зависящие от значения RoleName, то нельзя разрешать пользователю изменять его роль, если она уже установлена. Если допустить изменение роли, то может возникнуть проблема, связанная с кэшированием курсора и ‘old’-предикатами. Далее посмотрим на EMPNO текущего пользователя. Эта процедура выполняет две операции:
- Проверяет, является ли пользователь служащим - при получении ошибки "NO DATA FOUND", становится известным, что он не служащий. Поэтому значения его контекста никогда не будут установлены, а сам пользователь не увидит никаких данных.
-
Помещает часто используемое значение в контекст приложения. Теперь через EMPNO текущего пользователя можно быстро получить доступ к таблице EMP - это будет реализовано в предикатной функции, описанной ниже.
Далее процедура переходит к проверке, разрешено ли текущему пользователю получить роль, которую он запрашивает. Каждый, кто запрашивает роль EMP, может установить ее. Только тот, кто действительно управляет другими людьми, может установить роль MGR. Только тот, о ком есть данные в таблице HR_REPS, может установить роль HR_REP.
Далее создадим объект контекста приложения базы данных и свяжем его с только что созданной процедурой SET_HR_APP_DEPT:
SQL> -- Создание контекста приложения. Имя контекста - SQL> -- HR_APP_CTX. Процедура, с которой он связан в данном случае - SQL> -- это SET_ROLE
SQL> create or replace context Hr_App_Ctx using SET_ROLE 2 / Context created.
Итак, теперь созданы контекст с именем Hr_App_Ctx и процедура для его установки. Важно иметь в виду, что, так как контекст связан с процедурой Set_Role, то она является единственным средством установки значений контекста. Если, например, попытаться установить в этом контексте RoleName с целью получения доступа к запрещенным данным, то обнаружится, что сделать это нельзя:
SQL> REM Выполнение следующих далее операций будет ПРЕРВАНО. SQL> REM Это показывает, что процедура dbms_session.set_context SQL> REM может установить контекст Hr_App_Ctx только через процедуру SQL> REM SET_ROLE
SQL> exec dbms_session.set_context( 'Hr_App_Ctx', 'RoleName', 'MGR' ); BEGIN dbms_session.set_context( 'Hr_App_Ctx', 'RoleName', 'MGR' ); END
* ERROR at line 1: ORA-01031: привилегийнедостаточно ORA-06512: at "SYS.DBMS_SESSION", line 55 ORA-06512: at line 1
Этот атрибут контекста отвечает за безопасность. Значения контекста можно установить только через процедуру. В этом случае гарантируется, что, если значение и существует, то оно проверено и установлено. Теперь для тестирования логики процедуры попытаемся выполнить хранимую процедуру под различными пользователями и посмотрим, какие роли можно установить и какие значения существуют в контексте.
SQL> grant select on sys.v_$context to rls_smith; Grant succeeded.
SQL> connect rls_smith/rls_smith Connected.
SQL> set serveroutput on SQL> show user USER is "RLS_SMITH"
SQL> exec rls.set_role( 'Mgr' ) BEGIN rls.set_role( 'Mgr' ); END;
* ERROR at line 1: ORA-20002: Вы не менеджер ORA-06512: at "RLS.SET_ROLE", line 53 ORA-06512: at line 1
До настоящего времени можно было увидеть, что процедура доступна RLS_SMITH, но не позволяет ему установить контекст ‘MGR’ до тех пор, пока он фактически не станет менеджером. Если теперь посмотреть на этот контекст через динамическое представление v$context, то можно увидеть:
SQL> select * from v$context;
NAMESPACE ATTRIBUTE VALUE ---------- ---------- ----- HR_APP_CTX EMPNO 7369
что RLS_SMITH действительно мог получить список номеров его служащих, но не атрибут RoleName. Теперь повторим операцию с RoleName, задав правильное значение для RLS_SMITH:
SQL> exec rls.set_role( 'emp' ) PL/SQL procedure successfully completed.
SQL> select * from v$context;
NAMESPACE ATTRIBUTE VALUE ---------- ---------- ----- HR_APP_CTX ROLENAME EMP HR_APP_CTX EMPNO 7369
Можно увидеть, что теперь все работает, как ожидалось. RLS_SMITH может получить номер его служащего и атрибут RoleName, установленный в контексте HR_APP_CTX. Для дальнейшего тестирования логики выполним:
SQL> exec rls.set_role( 'emp' ) BEGIN rls.set_role( 'emp' ); END;
* ERROR at line 1: ORA-20001: Роль уже установлена ORA-06512: at "RLS.SET_ROLE", line 18 ORA-06512: at line 1
SQL> select * from v$context;
NAMESPACE ATTRIBUTE VALUE ---------- ---------- ----- HR_APP_CTX ROLENAME EMP HR_APP_CTX EMPNO 7369
Результат показывает, что логика не позволяет пользователю изменить его роль после того, как она уже установлена, далее будет видно, что в контексте остаются прежние значения. Это сообщение об ошибке не критично, оно значит только то, что нельзя изменить роль во время сессии.
Далее присоединимся различными пользователями, просмотрим результаты работы процедуры и рассмотрим различные способы получения значений контекста сессии:
SQL> connect rls_blake/rls_blake Connected.
SQL> exec rls.set_role( 'mgr' ) PL/SQL procedure successfully completed.
SQL> set serveroutput on SQL> declare 2 l_AppCtx dbms_session.AppCtxTabTyp; 3 l_size number; 4 begin 5 dbms_session.list_context( l_AppCtx, l_size ); 6 for i in 1 .. l_size loop 7 dbms_output.put( l_AppCtx(i).namespace // '.' ); 8 dbms_output.put( l_AppCtx(i).attribute // ' = ' ); 9 dbms_output.put_line( l_AppCtx(i).value ); 10 end loop; 11 end; /
HR_APP_CTX.ROLENAME = MGR HR_APP_CTX.EMPNO = 7698
PL/SQL procedure successfully completed.
На этот раз присоединимся как RLS_BLAKE, заведующий отделом 30. Когда RLS_BLAKE вызывает процедуру Set_Role с параметром RoleName = ‘MGR’, видно, что контекст установлен правильно: он - менеджер, и количество его служащих установлено. Кроме того, этот тест показывает, как просмотреть пары значений атрибутов в контексте сессии с помощью пакета dbms_session.list_context. Функции этого пакета может вызывать любой пользователь (так как происходит обращение к представлению sys.v$context, которое использовалось ранее), поэтому все пользователи могут использовать такой метод для проверки значений контекста сессии.
Теперь, так как контекст сессии установлен надлежащим образом, можно подготовиться к созданию процедуры политики безопасности. Эта процедура будет вызываться средствами базы данных во время их работы для формирования динамического предиката. Динамический предикат ограничивает такие возможности пользователя, как чтение и запись.
SQL> -- создадим предикатный пакет (пакет для генерации условия where) SQL> -- приложения HR. Для создания уникального условия where каждой SQL> -- операции SELECT/INSERT/UPDATE/DELETE должна соответствовать SQL> -- отдельная функция.
SQL> create or replace package hr_predicate_pkg 2 as 3 function select_function( p_schema in varchar2, 4 p_object in varchar2 ) return varchar2; 5 6 function update_function( p_schema in varchar2, 7 p_object in varchar2 ) return varchar2; 8 9 function insert_delete_function( p_schema in varchar2, 10 p_object in varchar2 ) return varchar2; 11 end; 12 /
Для каждой операции Языка Манипулирования Данными (Data Manipulation Language - DML) напишем предикаты, несколько отличающиеся друг от друга. Каждая DML-операция будет подчиняться своим правилам. Это позволит предложению DELETE видеть набор данных (в данном примере меньший), отличающийся от набора данных, видимого при выполнении предложения SELECT. Далее показана реализация тела пакета HR_PREDICATE_PKG:
SQL> create or replace package body hr_predicate_pkg 2 as 3 4 g_app_ctx constant varchar2(30) default 'Hr_App_Ctx'; 5 6 g_sel_pred varchar2(1024) default NULL; 7 g_upd_pred varchar2(1024) default NULL; 8 g_ins_del_pred varchar2(1024) default NULL; 9 10 11 function select_function( p_schema in varchar2, 12 p_object in varchar2 ) return varchar2 13 is 14 begin 15 16 if ( g_sel_pred is NULL ) 17 then 18 if ( sys_context( g_app_ctx, 'RoleName' ) = 'EMP' ) 19 then 20 g_sel_pred := 'empno = sys_context(''Hr_App_Ctx'',''EmpNo'')'; 21 22 elsif ( sys_context( g_app_ctx, 'RoleName' ) = 'MGR' ) 23 then 24 g_sel_pred := 25 ' empno in ( select empno 26 from emp_base_table 27 start with empno = 28 sys_context(''Hr_App_Ctx'',''EmpNo'') 29 connect by prior empno = mgr)'; 30 31 elsif ( sys_context( g_app_ctx, 'RoleName' ) = 'HR_REP' ) 32 then 33 g_sel_pred := 'deptno in 34 ( select deptno 35 from hr_reps 36 where username = 37 sys_context(''userenv'',''session_user'') )'; 38 39 else 40 raise_application_error( -20005, 'Рольнеустановлена' ); 41 end if; 42 end if; 43 44 return g_sel_pred; 45 end; 46 47 function update_function( p_schema in varchar2, 48 p_object in varchar2 ) return varchar2 49 is 50 begin 51 if ( g_upd_pred is NULL ) 52 then 53 if ( sys_context( g_app_ctx, 'RoleName' ) = 'EMP' )
54 then 55 g_upd_pred := '1=0'; 56 57 elsif ( sys_context( g_app_ctx, 'RoleName' ) = 'MGR' ) 58 then 59 g_upd_pred := 60 ' empno in ( select empno 61 from emp_base_table 62 where mgr = 63 sys_context(''Hr_App_Ctx'',''EmpNo'') )'; 64 65 elsif ( sys_context( g_app_ctx, 'RoleName' ) = 'HR_REP' ) 66 then
67 g_upd_pred := 'deptno in 68 ( select deptno 69 from hr_reps 70 where username = 71 sys_context(''userenv'',''session_user'') )'; 72 73 else 74 raise_application_error( -20005, 'Рольнеустановлена' ); 75 end if; 76 end if; 77 78 return g_upd_pred; 79 end; 80 81 function insert_delete_function( p_schema in varchar2, 82 p_object in varchar2 ) return varchar2 83 is 84 begin 85 if ( g_ins_del_pred is NULL ) 86 then 87 if ( sys_context( g_app_ctx, 'RoleName' ) in ( 'EMP', 'MGR' ) )
88 then 89 g_ins_del_pred := '1=0'; 90 elsif ( sys_context( g_app_ctx, 'RoleName' ) = 'HR_REP' ) 91 then 92 g_upd_pred := 'deptno in 93 ( select deptno 94 from hr_reps 95 where username = 96 sys_context(''userenv'',''session_user'') )'; 97 else 98 raise_application_error( -20005, 'Рольнеустановлена' ); 99 end if; 100 end if; 101 return g_ins_del_pred; 102 end; 103 104 end; / Package body created.
Ранее, без Детального Контроля Доступа использование одной таблицы с описанными выше тремя предикатами можно было достичь при помощи 3-х представлений - по одному на каждую операцию SELECT, UPDATE и INSERT/DELETE. Детальный Контроль Доступа позволяет сократить количество объектов до одной таблицы с динамическим предикатом.
Вспомните логику, описанную ранее:
11 function select_function( p_schema in varchar2, 12 p_object in varchar2 ) return varchar2 13 is 14 begin 15 16 if ( g_sel_pred is NULL ) 17 then -- логика для присвоения значения g_sel_pred, -- глобальной переменной, объявленной в теле пакета 42 end if; 43 44 return g_sel_pred; 45 end;
В этой функции переменная g_sel_pred устанавливается в непустое значение точно один раз за сессию. Если при предыдущем вызове этой предикатной функции предикат уже установлен - он просто возвращается снова. В этом есть два преимущества:
-
Это быстрее, чем вычисление и создание предиката в процедуре для каждого разбираемого предложения. Размер исполняемого кода уменьшается.
-
Предотвращается возможность возврата различных значений предиката в пределах одной сессии. Как отмечено в первом рассмотренном выше примере, результаты возврата различных предикатов в пределах одной сессии могут быть противоречивыми, а при использовании этой технологии предотвращается возникновение такой ситуации.
Последний этап этого процесса заключается в связывании предикатов с каждой DML-операцией и самой таблицей EMP. Далее показана реализация этой операции:
SQL> -- Добавим политику к представлению EMP. Каждая функция пакета SQL> -- HR_PREDICATE_PKG связывается с таблицей для выполнения операций SQL> -- SELECT/INSERTUPDATE/DELETE. При INSERT и UPDATE установим флаг SQL> -- 'update_check' взначение TRUE. Это очень похоже на создание SQL> -- представления с 'CHECK OPTION' SQL> -- В этом случае обеспечивается, что данные, создаваемые в базе SQL> -- данных, - это те данные, которые пользователь может SQL> -- увидеть.
SQL> begin 2 dbms_rls.add_policy 3 ( object_schema => 'RLS', 4 object_name => 'EMP', 5 policy_name => 'HR_APP_SELECT_POLICY', 6 function_schema => 'RLS', 7 policy_function => 'HR_PREDICATE_PKG.SELECT_FUNCTION', 8 statement_types => 'select' ); 9 end; 10 /
PL/SQL procedure successfully completed.
SQL> begin 2 dbms_rls.add_policy 3 ( object_schema => 'RLS', 4 object_name => 'EMP', 5 policy_name => 'HR_APP_UPDATE_POLICY', 6 function_schema => 'RLS', 7 policy_function => 'HR_PREDICATE_PKG.UPDATE_FUNCTION', 8 statement_types => 'update' , 9 update_check => TRUE ); 10 end; 11 /
PL/SQL procedure successfully completed.
SQL> begin 2 dbms_rls.add_policy 3 ( object_schema => 'RLS', 4 object_name => 'EMP', 5 policy_name => 'HR_APP_INSERT_DELETE_POLICY', 6 function_schema => 'RLS', 7 policy_function => 'HR_PREDICATE_PKG.INSERT_DELETE_FUNCTION', 8 statement_types => 'insert, delete' , 9 update_check => TRUE ); 10 end; 11 /
PL/SQL procedure successfully completed.
Таким образом, с каждой DML-операцией связывается различная предикатная функция. Когда пользователь запрашивает данные таблицы EMP, к запросу будет присоединяться предикат, сформированный пакетной функцией hr_predicate_pkg.select. Когда пользователь обновляет таблицу, будет использоваться пакетная функция update и так далее.
Теперь протестируем приложение. Для этого создадим пакет HR_APP. Этот пакет представляет собой приложение. В него входят функции для выполнения:
-
Запроса данных
- Обновления данных
- Удаления данных
- Вставки новых данных
Присоединимся различными пользователями с различными ролями и проследим за поведением нашего приложения. Это позволит увидеть, как работает детальный контроль доступа .
Ниже показана спецификация приложения:
SQL> create or replace package hr_app 2 as 3 procedure listEmps; 4 5 procedure updateSal; 6 7 procedure deleteAll; 8 9 procedure insertNew( p_deptno in number ); 10 end; 11 /
Телопакета:
SQL> create or replace package body hr_app 2 as 3 4 procedure listEmps 5 as 6 l_AppCtx dbms_session.AppCtxTabTyp; 7 l_size number; 8 begin 9 dbms_output.put_line( '------ КонтекстСессии ----------' ); 10 dbms_session.list_context( l_AppCtx, l_size ); 11 for i in 1 .. l_size loop 12 dbms_output.put( l_AppCtx(i).namespace // '.' ); 13 dbms_output.put( l_AppCtx(i).attribute // ' = ' ); 14 dbms_output.put_line( l_AppCtx(i).value ); 15 end loop; 16 17 dbms_output.put_line( '------ Данныетаблицы Emp, которыеможноувидеть -----' ); 18 for x in ( select ename, sal, dname 19 from emp, dept 20 where emp.deptno = dept.deptno ) 21 loop 22 dbms_output.put_line( x.ename // ',' // x.sal // ',' // x.dname ); 23 end loop; 24 end; 25 26 27 procedure updateSal 28 is 29 begin 30 update emp set sal = 9999; 31 dbms_output.put_line( sql%rowcount // ' строкобновлено' ); 32 end; 33 34 procedure deleteAll 35 is 36 begin 37 delete from emp where empno <> sys_context( 'Hr_app_Ctx', 'EMPNO' ); 38 dbms_output.put_line( sql%rowcount // ' строкудалено' ); 39 end; 40
41 procedure insertNew( p_deptno in number ) 42 as 43 begin 44 insert into emp ( empno, deptno, sal ) values ( 123, p_deptno, 1111 ); 45 end; 46 47 end hr_app; 48 /
SQL> grant execute on hr_app to public 2 / Grant succeeded.
Итак, "приложение" создано. Процедура listEmps показывает все записи, которые можно увидеть через представление EMP. Процедура updateSal обновляет каждую запись, к которой можно получить доступ. Процедура deleteAll удаляет каждую запись, к которой можно получить доступ, за исключением записи, идентифицирующей пользователя. Процедура insertNew создает нового служащего в заданном отделе. Это приложение просто тестирует все DML-операции над представлением EMP, которые можно было бы выполнить.
Часть 4
|