Rambler's Top100
"Knowledge itself is power"
F.Bacon
Поиск | Карта сайта | Помощь | О проекте | ТТХ  
 Подземелье Магов
  
 

Фильтр по датам

 
 К н и г и
 
Книжная полка
 
 
Библиотека
 
  
  
 


Поиск
 
Поиск по КС
Поиск в статьях
Яndex© + Google©
Поиск книг

 
  
Тематический каталог
Все манускрипты

 
  
Карта VCL
ОШИБКИ
Сообщения системы

 
Форумы
 
Круглый стол
Новые вопросы

 
  
Базарная площадь
Городская площадь

 
   
С Л С

 
Летопись
 
Королевские Хроники
Рыцарский Зал
Глас народа!

 
  
ТТХ
Конкурсы
Королевская клюква

 
Разделы
 
Hello, World!
Лицей

Квинтана

 
  
Сокровищница
Подземелье Магов
Подводные камни
Свитки

 
  
Школа ОБЕРОНА

 
  
Арсенальная башня
Фолианты
Полигон

 
  
Книга Песка
Дальние земли

 
  
АРХИВЫ

 
 

Сейчас на сайте присутствуют:
 
  
 
Во Флориде и в Королевстве сейчас  11:59[Войти] | [Зарегистрироваться]

Пример использования Private Object Security в Delphi

Набережных С. Н.
дата публикации 27-03-2008 12:23

Пример использования Private Object Security в Delphi

Введение

При создании серверных приложений зачастую встает вопрос разграничения доступа различных групп пользователей к функциям этого приложения. Если сервер работает с системой, имеющей собственную реализацию конторля доступа (сервер баз данных, файловая система и т.п.), то решение очевидно — переложить эту задачу на саму целевую систему. Однако в некоторых случаех этот способ оказывается неприменим — либо в целевой системе нет собственного механизма, либо существует необходимость реализовать алгоритм разграничения, отличный от существующего. В этом случая, по большому счету, у программиста есть два пути:

  • реализовать весь механизм защиты самостоятельно
  • использовать предоставляемый операционной системой Windows механизм "Private Object Security"

На мой взгляд, второй вариант имеет следующие важные преимущества:

  • Он реализован командой профессионалов в данной, весьма деликатной, области
  • Код его реализации протестирован огромным количеством пользователей, а выявленные ошибки оперативно устраняются командой разработчиков, то есть без нашнего участия.
  • Системный механизм черезвычайно гибок, удобен и, что весьма важно, хорошо знаком системным администраторам.

Этот путь и станет предметом обсуждения в данной статье.

Этапы большого пути

Не надо пугаться:) На самом деле путь этот не такой уж и большой, и код, написанный однажды, может быть с успехом использован повторно.

Весь процесс реализации защиты на основе "Private Object Security" достаточно четко разбивается на несколько вполне обособленных этапов, и это, на мой взгляд, является дополнительным плюсом, так как такая обособленность в значительной степени снижает вероятнось появления "перекрестных" ошибок. Этапы эти следующие:

  1. Определить, какие именно функции сервера требуют управления доступом
  2. На основе предыдущего этапа определить набор прав, дающий доступ к тем или иным функциям или их группам и оформить их в виде набора констант.
  3. Определить, какие определенные Вами и, опционально, стандартные права и их комбинации буду соответствовать базовым правам GENERIC_READ, GENERIC_WRITE, GENERIC_EXECUTE и GENERIC_ALL.
  4. Создать и проиницилизировать дескрипторы защиты, после чего, при необходимости, сохранить их в постоянном хранилище.
  5. Реализовать механизм, позволяющий системному администратору изменять настройки Вашей защиты.
  6. При каждом вызове клиентом защищаемой функции проверять, имеет ли данный клиент необходимый для ее вызова набор прав.

Теперь настало время рассмотреть каждый из этапов отдельно. Мы это будем делать, опираясь на код прилагаемого к статье примера.

Замечание: Для повышения наглядности в публикуемых здесь цитатах кода полностью опущена вся обработка ошибок. В реальности, конечно же, так поступать нельзя, и в самом прилагаемом примере такая обработка присутствует.

Этап первый. Что будем защищать?

Вряд ли данный этап может вызвать затруднения у автора серверного приложения. Как правило, сама задача дает вполне четкий ответ на этот вопрос. И скорее всего этот ответ будет "все".

Для реализации примера к статье мною был выбран достаточно, на мой взгляд, абстрактный объект. Его абрактность, как мне кажется, являлась очень важным параметром для демонстрации именно общих принципов построения системы, без привязки к каким-либо конкретным условиям.

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

  • Получение количества строк.
  • Чтение конкретной строки по индексу.
  • Перезапись существующей строки.
  • Добавление новой строки.
  • Удаление строки.

Кроме того, для администрирования сервер предоставляет следующие функции:

  • Считывание существующих настроек защиты.
  • Изменение настроек защиты.

И здесь на вопрос "что защищать?", мы отвечаем — "Всё!"

Этап второй. Определяемся с правами.

Итак, список защищаемых функций у нас есть. Теперь на его основе определим набор прав для работы с этими функциями:

// Чтение строки
  PL_READ_LINE     = 1;
  // Перезапись строки
  PL_WRITE_LINE    = 2;
  // добавление строки
  PL_ADD_LINE      = 4;
  // Удаление строки
  PL_DELETE_LINE   = 8;
  // Сервисная константа, введенная для удобства и включающая в себя
  // все вышеперечисленные права
  PL_ALL           = PL_READ_LINE or PL_WRITE_LINE or
                     PL_ADD_LINE  or PL_DELETE_LINE;

Вобщем-то, этот этап, как и предыдущий, достаточно самоочевиден, за исключением, пожалуй, нескольких моментов. Остановимся на них чуть подробнее.

Здесь мы определили собственные константы для всех действий (почти для всех, об этом позже). В принципе, это делать не обязательно. Например, для операции чтения вполне можно было использовать стандартное права STANDARD_RIGHTS_READ, но это право одновременно дает и право на просмотр настройки безопасности, я же предпочел разделить эти права. Конечно, можно было бы определить собственное право для чтения безопасности, но более логично сделать это для права на чтение строки. Так я и поступил.

Вы, конечно же, заметили отсутствие специальных прав на чтение количества строк и для работы с защитой. Объясняется это просто. При ближайшем рассмотрении очевидно, что считывание количества строк — не более чем частный случай операции чтения, и вполне логично будет их объединить. Что касается управления защитой, то для этих целей вполне достаточно уже предопределенных стандартных прав, и заводить новые константы просто не имеет смысла.

Этап третий. Базовые права.

Базовые права — это то, что Вы видите на первой вкладке диалога настройки безопасности объектов Windows. Это разделение всех применимых к объекту прав на четыре большие группы:

  • GENERIC_READ
  • GENERIC_WRITE
  • GENERIC_EXECUTE
  • GENERIC_ALL

Как я понимаю, основное назначение такого разделения — повышение удобства как администрирования, так и программирования. В самом деле, администратору гораздо удобнее выбрать среди 4-х больших групп, нежели создавать комбинацию из всех применимых к объекту прав. Тем более, что в большинстве случаев такой "грубой" настройки вполне достаточно. Так же и при программировании, гораздо проще при открытии, например, файла указать базовое право GENERIC_READ, чем вспоминать присущий объектам файловой системы набор прав. И опять-же, в большинстве реальных случаев базового права или их комбинации бывает вполне достаточно.

В любом случае, хотим мы или нет, но соответствие наших прав базовым нам определить придется. В примере для этого объявлена типизированная константа:

RightMapping: TGenericMapping =
  (
    GenericRead    : READ_CONTROL or PL_READ_LINE;
    GenericWrite   : PL_WRITE_LINE;
    GenericExecute : PL_DELETE_LINE or PL_ADD_LINE;
    GenericAll     : READ_CONTROL or WRITE_DAC or WRITE_OWNER or PL_ALL;
  );

Все здесь вполне очевидно, остановиться, видимо, имеет смысл только на последнем члене, GenericAll. Для этого базового права я определил не только все специальные права (PL_ALL), но и стандартные права, дающие доступ к настройке безопасности — READ_CONTROL (чтение настроек), WRITE_DAC (изменение списка контроля доступа — DACL) и WRITE_OWNER (смена владельца объекта). Разумеется, такой подход не является обязательным и Вы вольны избрать иную стратегию, лишь бы она не выходила за рамки разумного:)

Этап четвертый. Дескриптор защиты.

Любому программисту, имевшему опыт работы с WinApi в операционных системах линейки Windows NT, понятие дескриптора защиты должно быть в той или иной мере знакомо. Это один из базовых элементов системы безопасности Windows. Дескрипторы защиты создаются для защищаемых объетов и содержат всю необходимую информацию по разграничению доступа к защищаемому объекту. Такой дескриптор защиты необходимо создать и нам, при этом все обязанности по сохранению этого дескриптора между сеансами работы также ложатся на нас.

Когда нужно создавать дескриптор защиты? Очевидно тогда же, когда и сам защищаемый объект. А какие настройки безопасности должен содержать только что созданный дескриптор? А вот ответ на этот вопрос уже не столь очевиден. Однако, немного подумав, мы легко придем у выводу, что настройки "по умолчанию" должны, как минимум, давать возможность администратору эти самые настройки изменить. И желательно, без лишних действий с его стороны. Из этого следует вывод, что в дескрипторе должны присутствовать соответствующие разрешения для учетной записи администратора. Однако на этапе написания приложения мы не можем знать, под каким именем будет входить в систему администратор. Можно было бы спросить это при инсталляции сервера, или даже создать специальную учетную запись. Но в данном случае есть путь получше. Как известно, в Windows существует группировка учетных записей. Если мы дадим необходимые права группе администраторов, то эти же права автоматически получат все учетные записи, входящие в группу "Администраторы". Так мы и поступим.

Дополнительно, в примере полный доступ по умолчанию дается учетной записи "LocalSystem". Так как наш сервер выполнен в виде службы и предназначен для работы от имени этой учетной записи, то и вполне разумно дать самому себе полный доступ к объекту. Строго говоря это не является необходимым, сервер вполне может работать с объектом без оглядки на настройки защиты. Но лично мне представляется правильным сделать доступ к объету строго унифицированным, без оглядки на текущую учетную запись.

Итак, при первом запуске сервера создаем защищаемый объект и дескриптор защиты для него. В создании объекта ничего примечательного нет, а вот на создании дескриптора стоит остановиться.

function TACService.CreateDefaultSD(out pSD: PSecurityDescriptor): boolean;
var
  SD: TSecurityDescriptor;
  Acl: PACL;
  SzAcl: Cardinal;
  SidAdmins, SidSystem: PSID;
  H: THandle;
  LastErr: Cardinal;
//      ADMINS:      Sid: 'S-1-5-32-544'
//      LOCALSYSTEM: Sid: 'S-1-5-18'
begin
  AllocateAndInitializeSid(SECURITY_NT_AUTHORITY, 2,
      SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS,
      0, 0, 0, 0, 0, 0, SidAdmins);
  AllocateAndInitializeSid(SECURITY_NT_AUTHORITY, 1,
      SECURITY_LOCAL_SYSTEM_RID, 0, 0, 0, 0, 0, 0, 0, SidSystem);
  SzAcl:= sizeof(Acl^) + GetLengthSid(SidAdmins)
      + GetLengthSid(SidSystem) + 2 * ACE_ALLOWED_SIZE;
  Acl:= Pointer(LocalAlloc(LMEM_FIXED, SzAcl));
  InitializeAcl(Acl^, SzAcl, ACL_REVISION);
  AddAccessAllowedAce(Acl^,
      ACL_REVISION, RightMapping.GenericAll, SidAdmins);
  AddAccessAllowedAce(Acl^,
      ACL_REVISION, RightMapping.GenericAll, SidSystem);
  InitDescriptor(SD, [isdDacl, isdOwner, isdGroup], Acl, nil, SidSystem, SidAdmins);
  H:= GetCurrentToken(TOKEN_QUERY);
  Result:= CreatePrivateObjectSecurity(nil, @SD, pSD, false, H, RightMapping);
end;

Большая часть этого кода — самая обычная работа по созданию дескриптора. Вначале мы создаем SID для учетных записей группы администраторов и LocalSystem, затем инициализируем Access Control List, который будет использован в качестве DACL для нового дескриптора. Далее вставляем в ACL два разрешающих ACE, предоставив им все права (RightMapping.GenericAll). После этого с помощью вспомогательной функции InitDescriptor мы инициализируем новый дескриптор и назначаем ему DACL, владельца и первичную группу.

Наконец, выполнив все подготовительные действия, мы подошли к созданию дескриптора защиты приватного объекта. Делается это с помощью функции

function CreatePrivateObjectSecurity(
  ParentDescriptor, CreatorDescriptor: PSecurityDescriptor;
  var NewDescriptor: PSecurityDescriptor; 
  IsDirectoryObject: BOOL;
  Token: THandle; 
  const GenericMapping: TGenericMapping
): BOOL; stdcall;

Первый ее параметр — ParentDescriptor — предназначен для тех случаев, когда у нашего объекта есть родитель-контейнер. Если этот параметр указан и содержит в себе наследуемые элементы, то они будут унаследованы вновь создаваемым дескриптором.

Параметр CreatorDescriptor — дескриптор, используемый в качестве шаблона для нового приватного дескриптора. Содержащаяся в нем информация будет скопирована в новый дескриптор защиты. Этот параметр используется для инициализации нового дескриптора, подобно тому, как поступили мы.

В параметре NewDescriptor система, в случае успеха, возвращает указатель на вновь созданный приватный дескриптор. Этот дескриптор имеет так называемый "самоотносительный" (Self-relative) формат, пригодный для сериализации.

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

Последний параметр GenericMapping принимает уже обсужденную нами на третьем этапе структуру GENERIC_MAPPING, устанавливающую соответствие между базовыми правами и комбинациями стандартных и специальных прав для нового дескриптора.

Этап пятый. А где мой любимый напильник?!

Итак, дескриптор защиты у нас есть, но пока что от него мало проку. И чтобы он стал действительно полезен, нам необходимо реализовать средства для изменения его настроек.

В примере для этой цели есть отдельное приложение, выполненное в виде апплета панели управления.

Все, что делает данный апплет — устанавливает связь с сервером и,в случае успеха, создает пользовательский интерфейс, позволяющий изменить дескриптор. Наибольший интерес здесь представляет именно пользовательский интерфейс, на нем и задержим внимание.

Для данной цели система предоставляет нам замечательную функцию

function EditSecurity(
  hwndOwner: HWND; 
  const psi: ISecurityInformation
): BOOL; stdcall;

Эта функция принимает параметром интерфейс ISecurityInformation и создает хорошо всем знакомый стандартный диалог для редактирования защиты объекта. От нас же требуется корректно реализовать этот интерфейс. В прилагаемом примере его реализация находится в файле CPL\SRC\untEditSec.pas. Данный интерфейс включает 7 функций, реальный интерес для нас представляют четыре из них.

function TEditSecurityDlg.GetObjectInformation(
  out ObjectInfo: TSiObjectInfo): HRESULT;
begin
  with ObjectInfo do
  begin
    dwFlags:= SI_ADVANCED or SI_EDIT_AUDITS or SI_EDIT_OWNER;
    if not FEditOwner then dwFlags:= dwFlags or SI_OWNER_READONLY;
    hInst:= HInstance;
    pszServerName:= nil;
    pszObjectName:= PWideChar(FObjectName);
  end;
  Result:= S_OK;
end;

В этой функции мы должны корректно заполнить структуру ObjectInfo: TSiObjectInfo, которая в дальнейшем будет использована системой для создания диалогового окна. Как видите, заполнение ее трудностей не представляет, упоминания достойно только поле dwFlags. Для этого поля определен достаточно большой набор флагов, их описание подробно дано в MSDN. Советую ознакомиться с этим описанием, а также поэкспирементировать с различными их комбинациями — это будет нагляднее любого описания. Я же остановлюсь только на тех из них, которые импользованы в примере. Флаг SI_ADVANCED означает включение в диалог кнопки "Дополнительно", дающей доступ к детальной информации дескриптора. Флаг SI_EDIT_AUDITS делает доступной вкладку "Аудит", позволяющую изменить параметры аудита для данного дескриптора. Я включил эту вкладку, хотя в коде сервера аудит не задействован. Третий флаг, SI_EDIT_OWNER, включает вкладку изменения фладельца объекта.

Следующая интересующая нас функция:

function TEditSecurityDlg.GetSecurity(RequestedInfo: SECURITY_INFORMATION;
  out pSecDesc: PSecurityDescriptor; fDefault: BOOL): HRESULT;
type
  TPlSendCmd = packed record
    Header: TPipeQueryHeader;
    Cmd: TPlSimpleCmd;
  end;
var
  Len, Sz: Cardinal;
  PSD: PSecurityDescriptor;
  SendRec: TPlSendCmd;
  RetRec: TPipeAnswerHeader;
begin
  SendRec.Header.PackageSize:= SizeOf(SendRec) - SizeOf(TPipeQueryHeader);
  SendRec.Cmd.OpCode:= OPL_GET_SECURITY;
  WriteFile(FPipe, SendRec, SizeOf(SendRec), Len, nil);
  ReadFile(FPipe, RetRec, SizeOf(RetRec), Len, nil);

  if RetRec.ResultValue <> ERROR_SUCCESS then
  begin
    Result:= MakeResult(SEVERITY_ERROR, FACILITY_WIN32, RetRec.ResultValue);
    Exit;
  end;

  Len:= RetRec.PackageSize;
  GetMem(PSD, Len);
  try
    ReadFile(FPipe, PSD^, Len, Len, nil);
    Sz:= 0;
    GetPrivateObjectSecurity(PSD, RequestedInfo, nil, 0, Sz);
    pSecDesc:= Pointer(LocalAlloc(LMEM_FIXED, Sz));
    GetPrivateObjectSecurity(pSD, RequestedInfo, pSecDesc, Sz, Sz);
    Result:= S_OK;
  finally
    FreeMem(PSD);
  end;
end;

Данная функция вызывается всякий раз, когда диалогу необходимо обновить информацию дескриптора. Это происходит при инициализации диалога, а также после каждого успешного внесения изменений, если пользователь задействовал для этого кнопку "Применить". Апплет связывается с сервером и запрашивает у него текущий актуальный дескриптор. В случае успеха, апплет извлекает из полученного дескриптора ту информацию, которую запросила система (параметр RequestedInfo). Извлечение производится с помощью функции GetPrivateObjectSecurity, избавляющей нас от низкоуровневых манипуляций с дескриптором. Первым вызовом мы запросили у системы требуемый размер памяти, затем выделили эту память, и вторым вызовом GetPrivateObjectSecurity получили дескриптор, содержащий ту информацию, которая и была у нас запрошена.

function TEditSecurityDlg.GetAccessRights(pGuidObjectType: PGUID;
  dwFlags: DWORD; out pAccess: PSiAccess; out Accesses,
  DefaultAccess: ULONG): HRESULT;
begin
  pAccess:= @SiAccessTable[0];
  Accesses:= Length(SiAccessTable);
  DefaultAccess:= 0;
  Result:= S_OK;
end;

Эта функция должна вернуть адрес массива структур TSiAccess, устанавливающих связь между применимыми к объекту правами, их комбинациями с одной стороны, и их отображаемыми именами с другой. Кроме того, поле TSiAccess.dwFlags управляет тем, на какой вкладке будет показано данное право или комбинация. Этот массив инициализируется при создании объекта TEditSecurityDlg в вызываемой в его конструкторе процедуре InitSiAccessTable.

Наконец, функция

function TEditSecurityDlg.SetSecurity(
  SecurityInformation: SECURITY_INFORMATION;
  pSecDesc: PSecurityDescriptor): HRESULT;
type
  TSendCmd = packed record
    Header: TPipeQueryHeader;
    Cmd: TPlCmdLine;
    SD: TSecurityDescriptor;
  end;
  PSendCmd = ^TSendCmd;
var
  SendRec: PSendCmd;
  RetRec: TPipeAnswerHeader;
  SDLen, Needed: Cardinal;
  PackLen: Cardinal;
begin
  SDLen:= GetSecurityDescriptorLength(pSecDesc);
  PackLen:= SizeOf(SendRec.Header) + SizeOf(SendRec.Cmd) + SDLen;
  GetMem(SendRec, PackLen);
  try
    SendRec.Header.PackageSize:= SizeOf(SendRec.Cmd) + SDLen;
    SendRec.Cmd.OpCode:= OPL_SET_SECURITY;
    SendRec.Cmd.Index:= integer(SecurityInformation);
    SendRec.Cmd.DataSize:= SDLen;
    Needed:= SDLen;
    if not MakeSelfRelativeSD(pSecDesc, @SendRec.SD, Needed)
    then Move(pSecDesc^, SendRec.SD, SDLen);
    WriteFile(FPipe, SendRec^, PackLen, PackLen, nil);
    ReadFile(FPipe, RetRec, SizeOf(RetRec), PackLen, nil);
    if RetRec.ResultValue <> ERROR_SUCCESS then
    begin
      Result:= MakeResult(SEVERITY_ERROR, FACILITY_WIN32, integer(RetRec.ResultValue));
      Exit;
    end;
  finally
    FreeMem(SendRec);
  end;

  Result:= S_OK;
end;

вызывается, когда пользователь нажал кнопку "OK" или "Применить" в окне диалога. Параметром в нее передается указатель на дескриптор, содержащий изменения, а также набор флагов, указывающий какая именно информация подлежит изменению. Первым делом мы выясняем размер переданного дескриптора и выделяем под него память. Далее мы пытаемся вызвать функцию MakeSelfRelativeSD. Если нам был передан дескриптор в "абсолютном" формате, то этот вызов закончится успешно, вернув нам дескриптор в "самоотносительном" формате, пригодном для сериализации. Если же вызов MakeSelfRelativeSD закончится неудачей, то нам был передан дескриптор в "самоотносительном" формате, и тогда мы его просто копируем процедурой Move. Далее сериализованный дескриптор вместе с флагами отправляется серверу, а от него получаем ответ об успешном или неудачном внесении изменений.

Необсужденными остались три функции интерфейса ISecurityInformation, остановимся вкратце на них. В функции MapGeneric нужно вернуть уже обсуждавшуюся структуру GENERIC_MAPPING. Функция PropertySheetPageCallback вызывается при создании и уничтожении вкладок диалога, делать в ней что-либо нужды, как правило, нет. Функция GetInheritTypes ожидает информацию о наследуемых ACE. В рассматриваемом примере не используется.

Это все, что касается клиентской части. Теперь настало время рассмотреть серверную часть. На сервере в редактировании защиты участвуют две функции — TProtectedList.GetSecurity и TProtectedList.ChangeSecurity. Первая из них совсем проста:

function TProtectedList.GetSecurity(
  out PSecDesc: PSecurityDescriptor): cardinal;
const
  DesiredAccess = READ_CONTROL;
begin
  PSecDesc:= nil;
  if not CheckCallerAccess(DesiredAccess) then
  begin
    Result:= ERROR_ACCESS_DENIED;
    Exit;
  end;
  
  PSecDesc:= FSecDesc;
  Result:= ERROR_SUCCESS;
end;

Первым делом здесь проверяется, имеет ли право текущий клинт на просмотр информации о защите объекта. Это делается функцией CheckCallerAccess, которая будет рассмотрена на следующем этапе. В случае успеха функция возвращает адрес дескриптора защиты. Далее этот дескриптор сериализуется и отправляется клиенту.

Вторая функция более интересна.

function TProtectedList.ChangeSecurity(PSecDesc: PSecurityDescriptor;
  SecInfo: SECURITY_INFORMATION): Cardinal;
var
  DesiredAccess: ACCESS_MASK;
  HUser: THandle;
begin
  DesiredAccess:= 0;
  SecInfo:= SecInfo and
    (OWNER_SECURITY_INFORMATION or DACL_SECURITY_INFORMATION or
     SACL_SECURITY_INFORMATION);
  if OWNER_SECURITY_INFORMATION and SecInfo <> 0 then
    DesiredAccess:= DesiredAccess or WRITE_OWNER;
  if DACL_SECURITY_INFORMATION and SecInfo <> 0 then
    DesiredAccess:= DesiredAccess or WRITE_DAC;
  if SACL_SECURITY_INFORMATION and SecInfo <> 0 then
    DesiredAccess:= DesiredAccess or ACCESS_SYSTEM_SECURITY;

  if not CheckCallerAccess(DesiredAccess) then
  begin
    Result:= ERROR_ACCESS_DENIED;
    Exit;
  end;
  
  HUser:= GetCurrentToken(TOKEN_QUERY);
  if 0 = HUser then Result:= GetLastError
  else begin
    RevertToSelf;
    if SetPrivateObjectSecurity(SecInfo, PSecDesc, FSecDesc, RightMapping, HUser)
    then Result:= ERROR_SUCCESS
    else Result:= GetLastError;
    CloseHandle(HUser);
  end;
end;

На входе мы имеем адрес полученного от клиента дескриптора защиты, содержащего изменения, и маску, указывающую, какую именно информацию клиент намерен изменить. На основании этой маски мы формируем переменную DesiredAccess, заполняя ее набором прав, необходимых для внесения изменений в дескриптор. Далее проверяется наличие у клиента необходимых прав и уже после этого производится само внесение изменений в дескриптор защиты. Делается это функцией

function SetPrivateObjectSecurity(
  SecurityInformation: SECURITY_INFORMATION;
  ModificationDescriptor: PSecurityDescriptor; 
  var ObjectsSecurityDescriptor: PSecurityDescriptor;
  const GenericMapping: TGenericMapping; 
  Token: THandle
): BOOL; stdcall;

Параметр SecurityInformation определяет, какие именно части дескриптора подлежат замене. В ModificationDescriptor предается адрес дескриптора, содержащего новую информацию. Параметр ObjectsSecurityDescriptor на входе содержит адрес дескриптора защиты, подлежащего модификации. На выходе система помещает в него адрес нового дескриптора, созданного данной функцией, и содержащего модифицированную информацию. При этом старый дескриптор уничтожается, для чего система использует функцию DestroyPrivateObjectSecurity. Это значит, что в этот параметр функции можно передавать дескриптор, возвращенный либо функцией CreatePrivateObjectSecurity, либо функцией SetPrivateObjectSecurity. Параметры GenericMapping и Token имеют тот-же смысл, что и в функции CreatePrivateObjectSecurity.

Этап шестой. "Сударь, защищайтесь!"

Ну вот мы и добрались до того момента, ради которого все и затевалось. И тут оказывается, что говорить-то уже практически не о чем. Для проверки прав доступа сервер вызывает функцию

function TProtectedList.CheckCallerAccess(
  DesiredAccess: ACCESS_MASK): boolean;
var
  hUser: THandle;
  PrivBuf: array[0..2047] of Byte;
  PrivSet: TPrivilegeSet absolute PrivBuf;
  szPrivSet, Gradient: Cardinal;
  Status: bool;
begin
  Result:= OpenThreadToken(GetCurrentThread, TOKEN_QUERY, false, hUser);
  if not Result then exit;
  try
    szPrivSet:= sizeof(PrivBuf);
    Result:= AccessCheck(FSecDesc, hUser, DesiredAccess, RightMapping, PrivSet,
      szPrivSet, Gradient, Status);
    Result:= Result and Status;
  finally
    CloseHandle(hUser);
  end;
end;

Здесь мы прежде всего получаем маркер имперсонации текущего клиента, и проверяем возможность выполнения запрошенных действий с помощью

function AccessCheck(
  pSecurityDescriptor: PSecurityDescriptor;
  ClientToken: THandle; 
  DesiredAccess: DWORD; 
  const GenericMapping: TGenericMapping;
  var PrivilegeSet: TPrivilegeSet; 
  var PrivilegeSetLength: DWORD;
  var GrantedAccess: DWORD; 
  var AccessStatus: BOOL
): BOOL; stdcall;

Назначение ее первых 4-х параметров самоочевидно и не нуждается в комментариях. Единственно, о чем стоит упомянуть, это о том, что в параметр ClientToken обязательно должен передаваться маркер имперсонации. В сервере это не является проблемой, так как именно такой маркер вернет функция OpenThreadToken после вызова ImpersonateNamedPipeClient или подобной. Если же у Вас есть только первичный маркер, то Вы всегда можете воспользоваться функцией DuplicateToken для получения маркера имперсонации.

В параметре PrivilegeSet функция вернет Вам набор привилегий, которые ею были рассмотрены при принятии решения о предоставлении доступа. В параметре GrantedAccess функция сообщит, какими именно из запрошенных прав обладает текущий клиент. А в параметре AccessStatus Вы получите общий результат проверки — разрешено Вам или нет выполнять все запрошенные действия.

Несколько слов о примере

Прилагаемый к статье пример включает группу из трех проектов:

  • Сервер AccCtrlSvc.dpr(папка Server)
  • Клиент AcsClient.dpr (папка Client)
  • Апплет панели управления AcsCntl.dpr (папка CPL)

Кроме перечисленных, в архив также входят папки DCU (для DCU-файлов), BIN (сюда попадают иполняемые модули), Shared (здесь лежат модули, не входящие в проекты, но используемые ими) и MsgLibrary, содержащая отдельный проект AcsMsg.dpr, единственное назначение которого — быть контейнером для таблицы сообщений, используемой при логгировании действий сервера. Надо заметить, что включенная в него таблица сообщений предназначена скорее для отладки сервера, а никак не для информирования системного администратора, как это должно быть на практике. Объясняется это самим назначением данного сервера — служить учебным пособием.

Об апплете уже сказано достаточно, о клиенте же говорить вобщем-то нечего, там все слишком просто. А вот о сервере еще немного сказать стоит.

Сервер выполнен в виде службы. При инсталляции он вносит необходимую информацию в реестр, при деинсталляции он ее удаляет. При первом запуске сервер создает файл ACService.dat в директории "Application data" пользователя Localsystem. При деинсталляции этот файл также удаляется. В этом файле сервер хранит в зашифрованном виде дескриптор защиты объета и сами данные защищаемого объекта.

В качестве транспорта сервер использует именованные каналы. Выбор их объясняется простотой реализации в сочетании с легкостью имперсонации клиента сервером. Кроме того, у меня уже была готовая реализация этого транспорта. Правда, в целях сокращения не относящегося к теме статьи кода, код транспорта подвергся весьма серьезному сокращению. В первую очередь, "под нож" попало все связанное с оптимизацией производительности. Так-же был до предела упрощен протокол обмена и сведен к жесткой схеме "запрос-ответ". Все это делает данную реализацию транспорта малопригодной к использованию в реальных проектах, зато он вполне годится в качестве отправной точки для самостоятельной реализации Вами такого транспорта.

Домашнее задание

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

  1. Добавить возможность создания объектов клиентами, так чтобы сервер поддерживал работу с несколькими объектами с разными установками защиты.
  2. Добавить поддержку аудита для Ваших объектов.
  3. Добавить поддержку дерева объектов с возможностью наследования настроек дочерними объектами.

Заключение

В заключении не могу не воспользоваться случаем публично выразить глубокую и искреннюю признательность авторам замечательной книги "Программирование серверных приложений для Microsoft Windows 2000", господам Джеффри Рихтеру и Джейсону Кларку. Эта книга давно уже является для меня настольным пособием. Она же послужила первичным и основным источником информации при написании данной статьи. И хотя код прилагаемого примера нельзя назвать портированием примеров к этой книге, влияние, ими оказанное, глубоко и несомненно.

Литература

  1. Дж.Рихтер, Дж.Кларк, "Программирование серверных приложений для Microsoft Windows 2000", "Питер", ИТД "Русская редакция", 2001 г.
  2. 2. MSDN http://msdn2.microsoft.com/en-us/library/default.aspx


К материалу прилагаются файлы:


Смотрите также материалы по темам:
[Безопасность системы]

 Обсуждение материала [ 17-04-2015 04:15 ] 8 сообщений
  
Время на сайте: GMT минус 5 часов

Если вы заметили орфографическую ошибку на этой странице, просто выделите ошибку мышью и нажмите Ctrl+Enter.
Функция может не работать в некоторых версиях броузеров.

Web hosting for this web site provided by DotNetPark (ASP.NET, SharePoint, MS SQL hosting)  
Software for IIS, Hyper-V, MS SQL. Tools for Windows server administrators. Server migration utilities  

 
© При использовании любых материалов «Королевства Delphi» необходимо указывать источник информации. Перепечатка авторских статей возможна только при согласии всех авторов и администрации сайта.
Все используемые на сайте торговые марки являются собственностью их производителей.

Яндекс цитирования