Обычно, распределение прав доступа
пользователей моделируется еще на ранних
этапах проектирования приложения. При
этом выясняются функции, которые должны
исполнять те или иные группы
пользователей, разрабатываемой системы и,
исходя из этого, определяются данные, к
которым пользователи различных групп
будут иметь доступ.
При проектировании
многопользовательских приложений,
требования к организации
пользовательских прав оказывают
существенное влияние не только на
реализацию структур данных, но и на выбор
СУБД. Естественно, что различные СУБД
поддерживают различные механизмы
ограничения доступа к данным. И если,
скажем, Oracle предоставляет очень богатый
набор средств управления
пользовательскими правами, то такие СУБД
как MySQL достаточно ограничены в этом
отношении. Отдельно стоит упомянуть так
называемые тонкие БД, где встроенных
механизмов ограничения доступа к данным
вообще может не быть. Здесь разработчикам
стоит понимать, что выбор «тяжеловесных»
СУБД помимо очевидных преимуществ, имеет и
существенные недостатки. И дело даже не
только в ценовом факторе. Для
сопровождения, настройки и обеспечения
устойчивой работы таких СУБД как Oracle
просто необходимо привлечение
профессиональных администраторов баз
данных, а их труд стоит весьма не дешево.
Обычный пользователь, не обладающий
специальными навыками, просто не в
состоянии будет грамотно настроить СУБД и,
очевидно, раздать пользователям системы
нужные права и привилегии. Как следствие,
существенно возрастает стоимость
сопровождения, и система теряет гибкость.
Учитывая, что необходимость в
администраторах БД может возникать на
протяжении всего жизненного цикла
продукта, такой подход может стать
неприемлемым.
Возможно, в некоторых приложениях имеет
смысл полностью или частично отказаться
от распределения пользовательских
прав на уровне БД, а перенести реализацию
этих функций в само приложение. Давайте
рассмотрим небольшой пример, который
позволит лучше понять избранный мною
вариант реализации такого подхода. На
рисунке приведена упрощенная UML модель
системы.
Согласно модели, каждому пользователю
системы соответствует набор компонентов (далее
речь пойдет о реализации в RAD Delphi, что
обуславливает выбор имен классов в модели).
Собственно говоря, это компоненты, которые
используются в разрабатываемом
приложении. Нас интересуют формы (наследники
TForms), элементы управления (наследники TControl)
и компоненты типа TAction (именно они отвечают
за те действия, которые пользователь
производит над данными). Каждому
пользователю предоставляются права на
использование каждого из компонентов.
Далее под правами пользователя я буду
понимать значения атрибутов enabled и visible.
Именно, основываясь на значениях этих
атрибутов, в приложении будут
устанавливаться значения соответствующих
свойств компонентов в пределах каждой
пользовательской сессии. Сразу замечу, что
класс TComponent не имеет свойств enabled и visible, и
при реализации нам придется работать с
наследниками этих классов.
Теперь давайте рассмотрим, как подобную
модель можно реализовать на практике. Для
этого я воспользовался средой разработки
Delphi 7, и обычными таблицами Paradox.
Подключение к таблицам я организовал
через BDE. Прежде всего, я создал простейшее
приложение, управляющее таблицей Customers,
входящей в состав BDE в качестве примера.
Найти эту таблицу вы сможете в каталоге «...Program
Files\Common Files\Borland Shared\Data». Далее я создал три
таблицы, соответствующие классам
приведенной выше модели. В таблице Controls
будет храниться информация о компонентах
всех интересующих нас типов. Поле Id_Form
предназначено для обозначения
идентификатора (Id) формы – владельца
компонента. Структуры остальных таблиц
практически полностью соответствуют
модели.
Структура таблицы Controls
Имя поля | Тип | Размер | Ключ | |
1 | Id | + | * | |
2 | Id_form | S | ||
3 | ControlName | A | 64 | |
4 | Description | A | 128 |
Структура таблицы UserList
Имя поля | Тип | Размер | Ключ | |
1 | Id | + | * | |
2 | UserName | A | 22 |
Структура таблицы ControlRights
Имя поля | Тип | Размер | Ключ | |
1 | Id | + | * | |
2 | Id_User | S | ||
3 | Id_Control | S | ||
4 | Enabbled | L | ||
5 | Visible | L |
Для таблицы ControlRights я создал вторичный
индекс по полю Id_User, необходимый для того,
что бы организовать связь Master-Details в
приложении. Я не буду детально описывать
процесс подключения таблиц. На
приведенной ниже диаграмме показаны
логические связи между компонентами
доступа к данным в модуле данных (DataModule)
нашего приложения. Обратите внимание на
LookUp поле Control в таблице ControlRights, оно служит
для отображения имени компонента в Grid’е
отображающем права пользователя. Запрос
qSelectControl будет рассмотрен чуть позднее.
В приложении создаем форму
администрирования, где размещаем
страничный переключатель (PageControl) с двумя
страницами. На первой странице – таблица
пользователей и связанная с ним таблица
прав на компоненты приложения.
Прежде всего, сформируем список всех
компонентов приложения с указанием формы,
являющейся владельцем этого компонента. В
созданном на главной форме ActionList’е,
добавляем соответствующий пункт. Вот код
обработчика события OnExecute для него. В
начале из таблицы tControls, удаляются все
записи. Затем производится перебор
компонентов приложения и если это
наследник класса TForm, то он попадает в
таблицу и перебираются компоненты,
владельцами которых является это форма.
Для этих компонентов значением поля Id_form
становится Id формы-владельца.
procedure TfMain.Action4Execute(Sender: TObject);
var formId, j,i: integer;
begin
// открываем таблицу
if not DM.tControls.Active then
DM.tControls.Open;
// очищаем таблицу, что бы
не было дубликатов
DM.tControls.Close;
DM.tControls.EmptyTable;
DM.tControls.Open;
for i := 0 to Application.ComponentCount-1 do
begin
// вписываем в таблицу
формы
if Application.Components[i].ClassParent=TForm then
begin
DM.tControls.Insert;
DM.tControlsId_form.AsInteger:=-1;
DM.tControlsControlName.AsString:=Application.Components[i].Name;
DM.tControls.Post;
formId:=DM.tControlsId.AsInteger;
for j := 0 to (Application.Components[i] as TForm).ComponentCount-1 do
begin
DM.tControls.Insert;
DM.tControlsId_form.AsInteger:=formId;
DM.tControlsControlName.AsString:=(Application.Components[i] as TForm).Components[j].Name;
DM.tControls.Post;
end;
end;
end;
end;
Замечу, что DataModule нашего приложения
обрабатываться не будет – он не наследник
класса TForm. Если бы мы ставили целью
обрабатывать свойства компонентов TTable, то
нам бы пришлось слегка изменить этот
фрагмент программы.
Далее, обрабатываем событие AfterInsert для
таблицы TUserList таким образом, что бы при
добавлении нового пользователя, в таблицу
tControlRights добавлялись записи,
соответствующие всем записям из таблицы
tControls. Теперь при создании нового
пользователя для него сразу же
формируется список компонентов
приложения, и, соответственно, в этом
списке присутствуют поля Enabled и Visible.
procedure TDM.tUserListAfterInsert(DataSet: TDataSet);
var i: integer;
begin
// проверяем, если запись
находится в состоянии dsInsert,
// то вызываем метод Post, для
того, что бы обновить Id
// для некоторых других
СУБД, возможно придется изменить этот
фрагмент кода
if DataSet.State in [dsInsert] then
begin
DataSet.Post;
end;
DM.tControls.First;
while not DM.tControls.Eof do
begin
DM.tControlRights.Insert;
DM.tControlRightsId_user.AsInteger:=tUserListId.AsInteger;
DM.tControlRightsId_Control.AsInteger:=DM.tControlsId.AsInteger;
DM.tControlRights.Post;
DM.tControls.Next;
end;
end;
Таким образом, получая доступ к форме
администрирования, мы можем устанавливать
права на работу компонентами (а точнее с
некоторыми их наследниками). Далее создаем
форму для ввода логина. В данном примере я
не рассматриваю проблемы аутентификации,
поэтому на форме необходимо просто ввести
имя пользователя. Это имя попадет в
глобальную переменную CurrentUser.
public
{ Public declarations }
CurrentUser: integer;
Код нажатия кнопки закрывающей форму
аутентификации мы обрабатываем следующим
образом:
procedure TfLogin.Button1Click(Sender: TObject);
begin
if (Sender as TButton).ModalResult=mrOk then
begin
DM.tUserList.Filter:='UserName = '+QuotedStr(Edit1.Text);
DM.tUserList.Filtered:=true;
if DM.tUserList.RecordCount>0 then
begin
DM.tUserList.First;
fMain.CurrentUser:= DM.tUserListId.AsInteger;
DM.tUserList.Filtered:=false;
end
else
Application.Terminate;
end
else Application.Terminate;
end;
И теперь нам остается лишь написать
функцию, которая будет устанавливать
необходимые значения свойств enabled и visible
для нужных нам наследников классов TComponent.
Код этой функции выглядит следующим
образом:
function TfMain.SetControlStates(UserID: integer;
CurForm: TForm): boolean;
var a,CurFormId,i: integer;
Query1: TQuery;
ClassRef: TClass;
ClName: string;
begin
result:=false;
Query1:=TQuery.Create(self);
Query1.DatabaseName:='DB1';
Query1.SQL.Clear;
Query1.SQL.Add('select Id from Controls where (ControlName='+QuotedStr(CurForm.Name)+')
and (Id_Form=-1)');
Query1.Open;
CurFormId:=Query1.fieldbyName('Id').asInteger;
DM.qSelectControls.Close;
DM.qSelectControls.ParamByName('U'). AsInteger:=UserID;
DM.qSelectControls.ParamByName('f'). AsInteger:=-1;
DM.qSelectControls.ParamByName('n'). AsString:=CurForm.Name;
DM.qSelectControls.Open;
if DM.qSelectControlsEnabled.AsBoolean then
result:=true
else result:=false;
for i := 0 to CurForm.ComponentCount-1 do
begin
DM.qSelectControls.Close;
DM.qSelectControls.ParamByName('U'). AsInteger:=UserID;
DM.qSelectControls.ParamByName('f'). AsInteger:=CurFormId;
DM.qSelectControls.ParamByName('n'). AsString:=CurForm.Components[i].Name;
DM.qSelectControls.Open;
ClassRef := CurForm.Components[i].ClassType;
while ClassRef <> nil do
begin
//
if (ClassRef.ClassName='TControl') then
begin
if (CurForm.Components[i] as TControl).Enabled<>
DM.qSelectControlsEnabled.AsBoolean then
(CurForm.Components[i] as TControl).Enabled:=
DM.qSelectControlsEnabled.AsBoolean;
if (CurForm.Components[i] as TControl).Visible<>
DM.qSelectControlsVisible.AsBoolean then
(CurForm.Components[i] as TControl).Visible:=
DM.qSelectControlsVisible.AsBoolean;
end;
if ClassRef.ClassName='TAction' then
begin
(CurForm.Components[i] as TAction).Enabled:=
DM.qSelectControlsEnabled.AsBoolean;
end;
if ClassRef.ClassName='TDataSetAction' then
begin
if not DM.qSelectControlsEnabled.AsBoolean then
begin
(CurForm.Components[i] as TDataSetAction). OnExecute:=DataSetDelete1Execute;
end;
(CurForm.Components[i] as TDataSetAction). Enabled:=DM.qSelectControlsEnabled.AsBoolean;
end;
//
ClassRef := ClassRef.ClassParent;
end;
end;
end;
Здесь, очевидно, потребуются некоторые
разъяснения. Прежде всего, что передается
в качестве параметров. Это код
пользователя (у нас он храниться в
переменной CurrentUser) и форма, для которой
производиться действие. Последний
параметр введен по той причине, что на
разных формах могут присутствовать
компоненты с одинаковыми именами, а нам
однозначно определить, свойства какого
компонента мы изменяем. Запрос Query1
позволяет определить идентификатор
нужной формы (перечни идентификаторов
хранятся в поле id_form таблицы Controls). Запрос
несложный, и мы его формируем динамически.
Попутно проверяем, имеет ли пользователь
права просматривать эту форму, в
зависимости от чего возвращаем результат
функции. Определив идентификатор формы, мы
передаем параметры запросу qSelectControls. Этот
запрос вызывается в цикле, в котором мы
делаем последовательный перебор всех
компонентов формы. Вот текст этого запроса:
select * from controlRights r, controls c
where (Id_User=:u) and (Id_Form=:f) and (c.id=Id_Control) and (ControlName=:n)
Собственно говоря, здесь просто ищется
нужный компонент. В следующем фрагменте
кода мы пытаемся определить, что это
компонент. Напомню, что в соответствии с
моделью нас интересуют TActions и TControls (визуальные
компоненты). Естественно, нам необходимо
найти всех наследников этих классов, что
мы и делаем. В программе изначально мы
использовали экземпляры класса TDBAction, для
которого значения свойства Enabled
изменяются в зависимости от состояния (state)
таблицы. Ввиду этого мы не можем просто
поменять значение этого свойства в нашей
функции, так как оно все–равно измениться
в ходе работы с данными. В этом случае мы
просто переопределяем событие OnExecute для
этого компонента.
Теперь, при вызове формы нам следует
проверять имеет–ли право текущий
пользователь ее просматривать:
if SetControlStates(CurrentUser,fAdmin ) then
fAdmin.ShowModal
else
ShowMessage(' Нет прав!!!');
Есть еще один немаловажный вопрос, который
следует учесть. Система не должна остаться
без администратора. Здесь можно
предложить несколько вариантов. Самым
простым является создание
администраторской записи при вводе
определенной комбинации имени и пароля в
форме аутентификации. Естественно, что при
выходе администратора из системы
соответствующая запись должна удаляться.
Таким образом, администратор не останется
«бесправным». И, наконец, в форме
администрирования имеет смысл сделать
несколько «косметических эффектов».
Скажем, значительно удобней будет
использовать списки компонентов, если
компоненты каждого из трех (а теоретически
их может быть гораздо больше) типов будут
выделяться цветом.
procedure TfAdmin.DBGrid3DrawColumnCell (Sender: TObject;
const Rect: TRect;
DataCol: Integer; Column: TColumn; State: TGridDrawState);
begin
if gdFocused in State then exit;
if DM.tControlsId_form.AsInteger=-1 then
begin
DBGrid3.Canvas.Font.Color := clRed;
DBGrid3.DefaultDrawColumnCell (Rect,DataCol,Column,State);
end;
end;
Конечно же, это всего лишь
демонстрационный пример, иллюстрирующий
данный метод. Тем не менее, код получился
не слишком объемный и при некоторых
модификациях его можно внедрять в
реальные приложения. Делать это будет
значительно проще, если все
пользовательские операции будут
храниться в компоненте TActionList.
Хочу обратить внимание на то, что,
динамически изменяя свойство visible, мы
рискуем получить «артефакты», в основном
связанные с использованием свойства align
визуальных компонентов Delphi.
Сам метод, естественно, не следует
использовать в чистом виде. По меньшей
мере пароли на таблицы все же следует
поставить.
Теперь давайте подумаем, для каких видов
приложений приемлем такой подход. Прежде
всего, это нераспределенные
многопользовательские приложения,
использующие СУБД, не обеспечивающие
градации пользовательских прав на уровне
базы. Также, на мой взгляд, можно
использовать этот метод в малобюджетных
проектах, где средства на сопровождение
системы весьма ограничены. Несмотря на то,
что такое решение может показаться
надуманным, в нем есть несколько очевидных
преимуществ. В последнее время особо
популярными стали так называемые
трехуровневые приложения, реализуемые с
использованием таких технологий как MIDAS,
SOAP и т.д. Суть этих технологий сводится к
тому, что бизнес логика приложения со
всеми бизнес правилами выносится на
отдельный уровень. Интерфейс пользователя
и СУБД представляют собой еще два уровня.
При использовании описанного выше подхода
правила распределения доступа попадут на
бизнес уровень. Это существенно упростит
сопровождение системы, а, как следствие,
сэкономит и средства заказчика и время
разработчика. Более того, при переводе
приложения на другую СУБД, код, отвечающий
за распределение прав доступа,
значительной переработке не подвергнется.
Естественно, что для разработки больших
корпоративных систем, в чистом виде
описанный подход может оказаться и не
приемлемым. Но его можно использовать в
комбинации с «традиционными» методами
распределения прав доступа.
В рамках данной статьи также остались не
рассмотренными метода ограничения
доступа пользователей к определенным
группам данных. Однако описанный подход
позволяет решить и эту проблему.
Дополнительные файлы: исходники