Вадим Андросов
Делегируем права на перемещение
учетных записей пользователей в Active Directory.
Часть 2. Реализация основных функций
В этой части статьи на примере создания надстройки нестандартного делегирования административных полномочий для Windows 2003 Server рассмотрим методики разработки классов COM на языках сценариев (VBScript). Также подробно рассмотрим программный доступ к спискам контроля доступа (ACL).
Главный класс надстройки
Итак, было решено разбивать процесс переноса пользователей на этапы: извлечение из целевого отдела, ожидание принятия в целевой отдел, принятие в целевой отдел. Кроме того, важную роль играет еще один необязательный этап: отмена перевода пользователя на этапе ожидания.
В соответствии с этими этапами и будем разрабатывать необходимые функции.
Предлагаемая надстройка является достаточно сложной, и в ходе ее реализации потребуется разработать ряд функций. Этим подпрограммам понадобятся общие данные (например, объект пользователя – менеджера по персоналу).
Одно из решений – использовать глобальные переменные, однако разрабатывается библиотека базовых функций, которые будут использоваться в надстройке. Способ их использования пока до конца не ясен, поэтому реализовывать библиотечные функции, зависящие от контекста, опасно.
Так, потребуется создать функцию инициализации контекста, а потом не забыть ее вызвать. Также не исключена ситуация, что могут потребоваться функции, работающие на основе разных контекстов.
Чтобы прочно и относительно безопасно связать функции с глобальным по отношению к ним контекстом, нужно оформить библиотеку в виде класса. В этом случае можно создавать и затем спокойно использовать любое количество его объектов.
Подробно процесс оформления и регистрации сценариев в виде COM-классов описан в [3].
Так будет выглядеть общий каркас разрабатываемого класса:
Листинг 1. Каркас класса UserMove.Engine
<?xml version="1.0"?>
<component>
<registration
description="User moving routines"
progid="UserMove.Engine"
version="1.00"
classid="{5452eeea-6f5e-42e0-b6ce-b99184ea0f68}"
>
</registration>
<public>
‘методы, доступные пользователям класса
</public>
<reference guid="{97D25DB0-0363-11CF-ABC4-02608C9E7553}"/>
<object id="info" progid="ADSystemInfo"/>
<script language="VBScript">
<![CDATA[
‘объявление глобальных переменных (контекста объекта)
‘вызовы процедур инициализации объекта реализация методов
]]>
</script>
</component>
Все методы, разрабатываемые в статье, будут помещаться в этот класс. Возможно и другое безопасное решение – отказаться от контекста вовсе и передавать все необходимое функции в виде параметров. Однако в этом случае практически все функции требовали бы большого количества параметров, что отрицательно сказалось бы на читабельности кода.
Для начала обратим внимание на тег <reference guid="…"/>. Начав разрабатывать функции, я столкнулся с необходимостью использования большого количества констант ADSI. Раньше, когда их требовалось 2-3 штуки, особой проблемы не было. Достаточно было найти в MSDN [1] значения этих констант и заново объявить их в сценарии в виде:
Const ADS_RIGHT_DS_DELETE_CHILD = 2
Назвав их таким же образом, как это было сделано в стандартных библиотеках, можно было без проблем пользоваться примерами программ из документации. Однако такое переопределение стандартных констант является очень опасной практикой.
Существуют более цивилизованные методы решения этой проблемы. В VBA это делалось посредством подключения необходимой библиотеки (см. рис. 1). Для ADSI она называется Active DS Type Library.
Рисунок 1. Подключение библиотеки типов в VBA
После этой операции можно пользоваться всеми константами ADSI, не объявляя их самостоятельно. То же самое можно сделать и в сценариях WSH с помощью тега reference. Только он требует указания не имени библиотеки, а ее идентификатора (GUID), который можно получить, используя специальные утилиты. Например, для этого можно воспользоваться программой Microsoft OLE/COM Object Viewer, которая поставлялась в комплекте еще с Visual Studio версии 6 (см. рис. 2).
Рисунок 2. Получение GUID библиотеки
Теперь в сценарии также можно пользоваться константами, определенными в библиотеке. Например, следующая строчка кода приведет к выводу на экран числа 2.
msgbox ADS_RIGHT_DS_DELETE_CHILD
Также интерес представляет тег:
<object id="info" progid="ADSystemInfo"/>
С его помощью объявляется и инициализируется глобальная переменная info класса ADSystemInfo. Того же эффекта можно было бы достичь, написав в разделе CDATA:
Dim info
Set info = CreateObject("ADSystemInfo")
Запись в виде тега позволяет наглядно и относительно компактно определить глобально используемые объекты. Ее преимущества проявляются разве что в случае использования различных языков сценариев при создании одного класса, когда всем им необходим доступ к одной переменной. Здесь я использовал ее исключительно в иллюстративных целях, чтобы показать, что так тоже можно.
Общие подпрограммы
Начнем с функции, которая будет отвечать на вопрос, а имеет ли текущий пользователь (предполагаемый менеджер по персоналу) права на манипуляции необходимыми объектами. Такие проверки придется делать достаточно часто, чтобы выбрать правильный режим функционирования надстройки.
Для начала определимся, что это за необходимые объекты. Для нашей надстройки это экземпляры классов пользователь (user), команда (UserMoveCommand), команда отмены операции (UserMoveDenyCommand) и команда начала перемещения (UserMoveStartMoveCommand).
Чтобы избежать трудноуловимых ошибок, имена всех классов запишем в соответствующие константы.
Листинг 2. Константы названий классов надстройки
Const ROOM_CLASS = "UserMoveWaitingRoom"
Const START_MOVE_COMMAND_CLASS = "UserMoveStartMoveCommand"
Const DENY_COMMAND_CLASS = "UserMoveDenyCommand"
Const COMMAND_CLASS = "UserMoveCommand"
Const CHAIR_CLASS = "UserMoveChair"
Const LINK_CLASS = "UserMoveChairLink"
Во-первых, пользователь должен иметь права на добавление и удаление пользователей из организационной единицы. Как выяснилось, для этого необходимо разрешение:
- Добавлять и удалять дочерние объекты перечисленных типов в организационную единицу (см. рис. 3).
- Право записи свойств объектов. Наличия только предыдущего набора прав не достаточно для переноса объектов пользователей из одной организационной единицы в другую. Дело в том, что в объекте хранится ссылка на контейнер, и нужно иметь право на ее изменение. Право на запись всех свойств пользователей является явно избыточным для операции переноса, однако вполне оправдано для других операций, которые входят в обязанности менеджера по персоналу. Поэтому я решил не искать здесь более «тонкого» способа наделения полномочиями (см. рис. 4).
Рисунок 3. Права на создание дочерних объектов пользователей
Рисунок 4. Право изменения свойств объектов пользователей
Поскольку требуется написать подпрограмму проверки наличия прав работы с несколькими типами объектов, их удобно сгруппировать в один массив. Инициализироваться он будет один раз при создании экземпляра класса надстройки.
Рассмотрим метод, который это делает.
Листинг 3. Метод инициализации основного класса надстройки
sub initialize()
timeZoneOffset = "?"
set re = new Regexp
re.ignoreCase = True
re.global = True
delegationClasses = Array(getClassGUID("user"), getClassGUID(COMMAND_CLASS), getClassGUID(DENY_COMMAND_CLASS), getClassGUID(START_MOVE_COMMAND_CLASS))
end sub
Итак, можно видеть, что массив, содержащий классы для проверки, называется delegationClasses. Также в этом методе инициализируется ряд вспомогательных переменных, используемых в других методах класса. Конечно, все они должны быть объявлены выше.
dim timeZoneOffset, re, delegationClasses
Затем нужно не забыть вручную вызвать метод инициализации, потому что автоматически этого, к сожалению, не происходит.
Initialize
Этот вызов просто записывается выше всех объявлений функций. В массиве хранятся идентификаторы классов, так как при анализе списка контроля доступа необходимы будут именно они. GUID класса пользователя можно найти в MSDN [1]: {BF967ABA-0DE6-11D0-A285-00AA003049E2}.
Но для наших классов его там, естественно, нет. Причем это не тот идентификатор, который генерировался при создании классов [4]. Для получения GUID класса по его имени пришлось написать специальную функцию. Рассмотрим ее.
Листинг 4. Определение GUID класса
function getClassGUID(className)
dim classObj
set classObj = getObject("LDAP://schema/" & className)
getClassGUID = GUID2Str(classObj.schemaIDGUID)
end function
Листинг 5. Преобразование Octet String к обычной строке
Function GUID2Str(Guid)
Dim i, b(16)
For i = 1 To 16
b(i - 1) = Right("0" & Hex(Ascb(Midb(Guid, i, 1))), 2)
Next
GUID2Str = "{" & b(3) & b(2) & b(1) & b(0) & "-" & b(5) & b(4) & "-" & b(7) & b(6) & "-" & b(8) & b(9) & "-"
for i = 10 to 15
GUID2Str = GUID2Str & b(i)
next
GUID2Str = GUID2Str & "}"
End Function
Функция выглядит сумбурно, потому что писалась «методом тыка». С помощью первого цикла строка Octet String превращается в массив байт b. Для этого с помощью функции Midb из нее выделяется один байт в заданном месте, который потом преобразуется в число функцией Ascb (возвращает первый байт переданного ей символа). Полученное число переводится в шестнадцатеричный формат (функция Hex), которому в начало добавляется 0. Последнее нужно потому, что каждый байт должен представляться двузначным шестнадцатеричным числом (1A, FF, D5). Поэтому когда получилось, скажем, F, то ему в начало добавляется 0. Если число сразу было двузначным, то ноль станет третьим лишним символом, который будет отсечен с помощью функции right.
Эксперименты с системными классами (для которых мне был известен правильный идентификатор) показали, что порядок полученных байт в результате немного меняется.
Получаемая строка состоит из 4-х групп. В первую входят первые четыре байта исходной строки в обратном порядке, во вторую – 5-й и 6-й байты тоже в обратном (в программе это 4-й и 5-й элементы массива, поскольку нумерация начинается с нуля), затем еще два байта в обратном порядке, далее два байта в прямом порядке и, наконец, последние 6 снова в прямом. Группы должны быть разделены дефисами. Весь идентификатор заключается в фигурные скобки.
Несмотря на способ написания и внешний вид, функция корректно работает. Полученные с ее помощью идентификаторы дополнительных классов без проблем воспринимались системой.
Переменная re будет использоваться для работы с регулярными выражениями, а timeZoneOffset – содержать смещение временной зоны для текущего часового пояса. Но это потом, а сейчас перейдем к методу проверки полномочий.
Листинг 6. Проверка прав управления подразделением
function canHeManageOU(ou, user)
dim canChange, canMoveChild, i
canHeManageOU = true
for i = 0 to UBound(delegationClasses)
canChange = canDo(ou, user, ADS_RIGHT_DS_WRITE_PROP, delegationClasses(i), true)
canMoveChild = canDo(ou, user, ADS_RIGHT_DS_DELETE_CHILD Or ADS_RIGHT_DS_CREATE_CHILD, delegationClasses(i), false)
canHeManageOU = canHeManageOU and (canChange And canMoveChild)
next
end function
Функции передается два параметра – объекты «Организационная единица» и пользователя, права которого проверяются. Функция очень простая, поскольку вся реальная работа сосредоточена в более низкоуровневых подпрограммах, которые будут рассмотрены позже.
Вся работа метода сводится к последовательной проверке необходимых прав для всех объектов массива delegationClasses посредством вызовов функции canDo. Сначала проверяется возможность изменения свойств объектов (константа ADS_RIGHT_DS_WRITE_PROP), а затем – добавления и удаления пользователей из контейнера (комбинация констант ADS_RIGHT_DS_DELETE_CHILD и ADS_RIGHT_DS_CREATE_CHILD). То есть всю полезную работу выполняет функция canDo. Она является вспомогательной и в качестве открытого метода класса не используется в отличие от canHeManagePath. Чтобы сделать его доступным пользователям класса, нужно добавить его в раздел public описания (см. листинг 1).
<method name="canHeManagePath">
<PARAMETER name="ouPath"/>
<PARAMETER name="whoPath"/>
</method>
Метод представляет собой простую обертку для функции canHeManageOU (см. листинг 6), которая получает в качестве параметров не сами объекты, а пути к ним (ADSI Path).
Листинг 7. Проверка наличия прав управления организационной единицей
function canHeManagePath(ouPath, whoPath)
dim ou, user
set ou = getObject(ouPath)
set user = getObject(whoPath)
canHeManagePath = canHeManageOU(ou, user)
end function
В подпрограмме создаются экземпляры необходимых объектов и вызывается canHeManageOU. Также в классе определено еще два метода-обертки для canHeManageOU, позволяющих проверять права доступа к организационной единице текущего пользователя.
Листинг 8. Дополнительные функции проверки прав
function canCurrentManagePath(ouPath)
dim ou
set ou = getObject(ouPath)
canCurrentManagePath = canCurrentManageOU(ou)
end function
function canCurrentManageOU(ou)
dim user
set user = getObject("LDAP://" & info.userName)
canCurrentManageOU = canHeManageOU(ou, user)
end function
Здесь info – глобальный объект типа ADSystemInfo (см. листинг 1). Его свойство username содержит путь к объекту текущего пользователя.
Далее перейдем к реализации функций для работы с ACL. Список контроля доступа (Access Control List) сопоставлен каждому объекту Active Directory, он состоит из набора записей (Access Control Entry или ACE), которые собственно и определяют уровень доступа пользователя или группы к ресурсу. Эта тема достаточно обширна, при необходимости получить дополнительную информацию можно из соответствующих источников [1, 2]. Windows предоставляет графические средства редактирования ACL (см. рис. 3).
Подробно рассмотрим работу функции canDo. Ей передается три параметра:
- oper. Проверяемая операция. То есть то действие, возможность выполнения которого требуется проверить. Для обозначения действий используются константы из библиотеки Active DS Type Library.
- targetClass. Идентификатор целевого класса, допустимость работы с объектами которого проверяется.
- isInherited. Параметр логического типа. Запись контроля доступа может описывать как разрешения, относящиеся непосредственно к объекту, к которому прикреплен ACL, так и его непосредственным потомкам. Разрешение на добавление и удаление пользователей из организационной единицы, это относится к контейнеру, поэтому значение этого параметра будет истинно. В то же время право на изменение свойств вложенных объектов уже относится скорее к ним, чем к организационной единице. В этом случае значение – ложь. На что влияет параметр, станет понятно из реализации.
function canDo(oper, targetClass, isInherited)
canDo = false
Dim sec, acl, ace
Set sec = ou.Get("ntSecurityDescriptor")
Set acl = sec.DiscretionaryAcl
dim result
Затем нужно проверить все записи (ace) списка. Удобнее всего это делать с помощью оператора цикла For Each, основное предназначение которого как раз обход коллекций.
Проверка каждой записи осуществляется с помощью функции checkACE, которая возвращает одну из трех констант (эти константы определяются в классе, их конкретные значения могут быть любыми, лишь бы разными, у меня это было 1, 2 и 3):
- CHECK_ACE_SKIP. Проверенная запись контроля доступа не относится ни к проверяемому пользователю, ни к содержащей его группе. Или же запись относится к нужному пользователю, но управляет несущественными в данном контексте правами, то есть не соответствующими параметру oper. Такие записи при проверке игнорируются.
- CHECK_ACE_YES. Запись относится к проверяемому пользователю и разрешает действие, заданное в oper.
- CHECK_ACE_NO. Запись относится к проверяемому пользователю и запрещает действие, заданное в oper.
Встретив разрешение или запрет операции, функция сразу завершает работу, возвращая соответствующий результат. Дальше проверять список нет необходимости. Операционной системой гарантируется, что запись о явном запрете встретится раньше разрешения. Так что, встретив запись с разрешением, можно быть уверенным, что далее записи с запретом этой же операции уже не будет.
For Each ace In acl
result = checkACE(ace, oper, targetClass, isInherited)
if result <> CHECK_ACE_SKIP then
canDo = (result = CHECK_ACE_YES)
exit function
end if
Next
end function
Функция checkACE в качестве первого параметра принимает проверяемую запись списка контроля доступа, остальные параметры передаются от canDo.
Приведенная функция также не относится к самому низкому уровню реализации, она использует подпрограмму проверки записи контроля доступа checkACE. Рассмотрим теперь и эту функцию.
Логика ее работы и возвращаемые значения уже были описаны выше, поэтому расскажу об особенностях реализации.
function checkACE(ace, oper, targetClass, isInherited)
checkACE = CHECK_ACE_SKIP
if not isTrusteeInteresting(ace.Trustee) then exit function
Сначала функция проверяет свойство Trustee записи контроля доступа. Оно определяет субъект доступа, к которому относится запись. Функция продолжает работу только в том случае, если это или пользователь, права которого сейчас проверяются, или группа, содержащая этого пользователя.
В противном случае функция возвращает константу CHECK_ACE_SKIP, извещающую о том, что текущую запись нужно пропустить.
dim classGUID
if isInherited then
classGUID = ace.InheritedObjectType
else
classGUID = ace.ObjectType
end if
Затем в зависимости от значения параметра isInherited определяется объект доступа. Это или дочерние объекты (используется при проверке прав на добавление и удаление объектов пользователей в организационной единице), или текущий объект (проверка возможности изменения свойств пользователей).
Объект доступа в данном случае – это идентификатор класса, права на манипуляции экземплярами которого предоставляются.
if isMask(ace.accessMask, oper) then
Дальнейшая работа будет выполнена только в том случае, если текущая запись контроля доступа описывает проверяемые права. Нужно убедиться с помощью функции isMask, работа которой будет рассмотрена ниже, что запись содержит информацию о необходимой операции (параметр функции oper).
if ace.AceType = ADS_ACETYPE_ACCESS_DENIED then
checkACE = CHECK_ACE_NO
exit function
end if
if ace.AceType = _
ADS_ACETYPE_ACCESS_ALLOWED then
checkACE = CHECK_ACE_YES
exit function
end if
Далее проверяем, что целевой объект представляет собой интересующий нас класс (его идентификатор передается в качестве параметра функции).
if classGUID = targetClass then
if ace.AceType = _
ADS_ACETYPE_ACCESS_DENIED_OBJECT then
checkACE = CHECK_ACE_NO
exit function
end if
if ace.AceType = _
ADS_ACETYPE_ACCESS_ALLOWED_OBJECT then
checkACE = CHECK_ACE_YES
exit function
end if
end if
end if
end function
Основная логика работы уже рассмотрена, осталось только несколько вспомогательных функций. isMask – функция логического типа, возвращает истину, если в поле (первый параметр) установлен заданный флаг (второй параметр):
function isMask(mask, flag)
isMask = (mask and flag) <> 0
end function
Рассмотрим последнюю вспомогательную функцию – isTrusteeInteresting. Она возвращает истину, если субъект доступа записи контроля доступа относится к проверяемому пользователю.
function isTrusteeInteresting(trustee)
isTrusteeInteresting = false
if trustee = (info.domainShortName & "\" & user.samAccountName) then
isTrusteeInteresting = true
exit function
end if
Пользователь содержится в поле trustee записи контроля доступа в виде «ДОМЕН\ПОЛЬЗОВАТЕЛЬ». Проверку этого случая и осуществляет первый условный оператор. Если пользователь совпал с проверяемым, функция завершает работу, возвращая истину.
Однако одной такой проверки недостаточно: запись может относиться не только к пользователю, но и к содержащей его группе.
dim trusteeObj
if instr(1, Trustee, info.domainShortName) <> 1 then exit function
Однако сначала нужно убедиться, что запись содержит в поле trustee объект, относящийся к проверяемому домену. Дело в том, что кроме записей для пользователей и групп существуют специальные экземпляры, описывающие доступ для различных системных объектов (например, NT AUTHORITY\ENTERPRISE DOMAIN CONTROLLERS), которые при решении текущей задачи нужно пропускать. Функция завершает работу и возвращает ложь (это значит, что текущая запись «не интересна»), если в значении Trustee не содержится имени домена.
set trusteeObj = getObject("WinNT://" & replace(trustee, "\", "/"))
Затем происходит привязка к объекту Trustee. Обратите внимание, здесь я использую провайдер WinNT. Он менее функционален, чем LDAP, однако в данном случае более удобен, так как позволяет привязываться к объектам, игнорируя их положение в структуре предприятия. В наличии имеется только имя Trustee в домене. Чтобы воспользоваться провайдером LDAP потребовалось бы сначала произвести поиск объекта в иерархии, чтобы установить его отличительное имя (DN), что в данной ситуации обернулось бы только неоправданным разрастанием кода. Функция replace требуется здесь для того, чтобы привести имя Trustee к виду, пригодному для привязки с помощью провайдера WinNT. Собственно оно почти подходит, за исключением того, что нужно заменить обратный слеш на прямой.
if trusteeObj.class = "Group" then
Далее, если Trustee – группа (еще это может быть просто другой пользователь), нужно проверить с помощью метода isMember принадлежит ли ей текущий пользователь, если принадлежит, то функция возвращает истину.
if trusteeObj.isMember("WinNT://" & info.domainShortName & "/" & user.samAccountName) then
isTrusteeInteresting = true
exit function
end if
end if
end function
Итак, в этой части статьи были рассмотрены вопросы создания класса COM на языке программирования сценариев. Также были освещены базовые аспекты работы со списками контроля доступа. В следующей статье будет продолжена разработка надстройки. Будет реализована программная модификация списков контроля доступа и автоматическое делегирование полномочий.
- msdn.microsoft.com.
- Чарли Рассел, Шарон Кроуфорд, Джейсон Джеренд. «Windows server 2003 +SP1 и R2. Справочник администратора». – М.: Издательство «ЭКОМ», 2006 г. – 1424 с. Washington: Microsoft Press, 2006 г.
- Андросов В. Делегируем права на перемещение учетных записей пользователей в Active Directory. Часть 1. Постановка задачи. //Системный администратор, №2, 2009 г. – С. 16-21.