ВАДИМ АНДРОСОВ, ассистент ВНУ, специалист MCP. Занимается анализом архитектур организаций
с защищаемыми бизнес-процессами
Проводим реализацию
тонкого делегирования прав в Active Directory
Создаем надстройку, позволяющую перемещать учетные записи пользователей между подразделениями только усилиями администраторов организационных единиц.
Графический интерфейс сценариев
Пришло время разработать оставшиеся элементы надстройки (начало см. в СА №3,4,5,6 за 2009 г.). Поскольку речь здесь идет уже о работе с очередями переводимых пользователей, стандартный интерфейс сценариев, основанный на простейших диалоговых окнах, мало подходит. В то же время надстройка слишком проста для того, чтобы ради ее создания использовать сложную коммерческую интегрированную среду разработки.
Существует возможность создания для сценариев более изощренных пользовательских графических интерфейсов, основанных на языке разметки html. Это гипертекстовые приложения (hyper-text applications). По сути это обычная html-страничка со сценарием, которая сохранена с расширением hta. При запуске таких приложений используется графический движок (rendering engine) Internet Explorer. Однако по своей сути они гораздо ближе к обычным программам, чем к веб-страницам. Стандартная модель безопасности IE не распространяется на hta-приложения, что дает возможность свободно работать со всеми объектами операционной системы.
Кроме особого расширения hta-приложения должны иметь в разделе заголовка специальный тег hta:application. Все остальное оформляется как обычный html-документ. Рассмотрим каркас hta-приложения.
Листинг 1. Каркас hta-приложения
<head>
<title>Move</title>
<hta:application
contextmenu = "no"
minimizebutton = "no"
/>
<script language = "VBScript">
‘Текст сценария
</script>
</head>
<body>
<h1>Some caption<h1>
<i>Some text<i>
</body>
Тег hta:application может содержать параметры, на самом деле их гораздо больше [1], но для текущей надстройки этих вполне достаточно. Рассмотрим подробнее используемые параметры.
contextmenu – позволяет отключить стандартное контекстное меню Internet Explorer, так как его содержание (выбор кодировки, просмотр исходного кода приложения и др.) обычно неуместно для программы;
minimizebutton – с помощью этого свойства включается и отключается кнопка минимизации окна.
Const PREFERRED_WIDTH = 400
Const PREFERRED_HEIGHT = 300
Сначала с помощью констант задаем предпочтительные размеры окна. Далее определяется специальная функция window_onLoad. Подпрограмма с таким именем будет автоматически вызвана при загрузке приложения.
function window_onLoad()
dim x, y, w, h, scrW, scrH
Set objWMIService = GetObject("winmgmts:" & "{impersonationLevel=impersonate}!\\.\root\cimv2")
Set colItems = objWMIService.ExecQuery("Select * from Win32_DesktopMonitor")
Затем подключаемся с помощью WMI-запроса к объектам типа Win32_DesktopMonitor. Теоретически мониторов может быть несколько, здесь было решено пользоваться первым. То есть цикл ниже выполнится только один раз для первой записи благодаря директиве exit for. Конечная цель – получить разрешение монитора, которое запоминается в переменных scrW (ширина) и scrH (высота).
For Each objItem in colItems
scrW = objItem.screenWidth
scrH = objItem.screenHeight
exit for
next
Переменные x, y будут содержать итоговое положение окна (координаты его левого верхнего угла), w, h – соответственно его ширину и высоту. Изначально эти значения устанавливаются в соответствии с предпочтительными.
w = PREFERRED_WIDTH
h = PREFERRED_HEIGHT
x = (scrW - PREFERRED_WIDTH) / 2
y = (scrH - PREFERRED_HEIGHT) / 2
Но предпочтительный размер окна может не поместиться в экран. Для обработки этой ситуации предназначены два условных оператора. Если ширина приложения превосходит ширину рабочего стола, то размер окна устанавливается равным ширине экрана, а координата x – началу экрана (т.е. значению 0). Аналогично решается проблема слишком большой высоты.
if scrW < PREFERRED_WIDTH then
w = scrW
x = 0
end if
if scrH < PREFERRED_HEIGHT then
h = scrH
y = 0
end if
В конце концов окно устанавливается в заданную позицию посредством метода moveTo и масштабируется (метод resizeTo).
window.moveTo x, y
window.resizeTo w, h
end function
Окно с такой стартовой функцией будет всегда или располагаться по центру экрана, или занимать максимум возможного места при превышении размеров экрана. Отдельно упоминать эту функцию я больше не буду, но она будет применяться во всех hta-приложениях надстройки.
Как можно увидеть из этой подпрограммы сценарии в hta-приложениях имеют обычный доступ к инструментам операционной системы (включая WMI). В обычных веб-страницах подобные операции существенно ограничены, несмотря на возможность применения того же языка программирования.
Начало перемещения пользователя
Рассмотрим первое приложение (см. рис. 1). Оно будет вызываться из контекстного меню пользователя оснастки Active Directory Users and Computers для начала операции перемещения. Программа должна позволять пользователю указать целевую организационную единицу, т.е. предоставить средства перемещения по каталогу. Итак, интерфейс достаточно прост. Сверху написано имя перемещаемого пользователя (Gomer J.Simpson), ниже – текущий контейнер (отображается полный путь). Далее идет основной управляющий элемент. Список организационных единиц, входящих в текущую. Нажатие на <ENTER> или двойной щелчок должны производить вход в контейнер. Выбор самого верхнего элемента (<..) позволяет подняться к родительскому контейнеру, если таковой существует. Выбор второй строчки (> <) обозначает команду «Переместить сюда». Как только она выбрана, пользователь ставится в очередь на перевод, а приложение закрывается. Реализуем эту логику.
Рисунок 1. Приложения начала перемещения пользователя
Общий каркас hta-приложения уже рассматривался. Сначала приведем описание внешнего вида на языке разметки HTML. Все, что нам нужно, это два поля под текст и список. Выглядеть это может так:
Листинг 2. Пользовательский интерфейс, созданный с помощью HTML
<body>
<div id = "userToMove"></div>
<hr>
<div id = "curPathShow"></div>
<form name = 'st artMove'>
<select style="width: 350px; height: 200px"
size=2 name=ouList ondblclick = "itemSelected()"
onkeypress = "onKey()">
</select>
</form>
</body>
Обоим разделам, куда будет выводиться текст (тег div), сопоставляются идентификаторы (id), благодаря которым к элементам можно будет обращаться из сценария. Для списка (тег select) назначаются обработчики двух типов событий: двойного нажатия кнопки мыши (функция itemSelected) и нажатия клавиши (функция onKey). Но сначала рассмотрим инициализацию приложения. Для работы программы понадобится ряд глобальных переменных:
dim curPath, backPath, prevPath, user, engine
curPath – путь к текущей организационной единице;
backPath – путь к родительской организационной единице, используется для выхода «наверх»;
prevPath – предыдущая организационная единица, т.е. та, из которой мы попали в текущую;
user – перемещаемый пользователь;
engine – экземпляр основного класса надстройки User Move.Engine, который был создан в предыдущих частях статьи.
Функция инициализации создает экземпляр класса надстройки, устанавливает фокус ввода на список и подключается к объекту перемещаемого пользователя. Что перемещать передается приложению через командную строку и извлекается из нее с помощью функции extractArg, которая будет рассмотрена далее. Затем имя пользователя выводится над списком. В конце устанавливается текущий каталог – тот, в котором находится пользователь.
Листинг 3. Функция инициализации приложения
function window_onLoad()
set engine = createObject("UserMove.Engine")
startMove.ouList.focus
set user = getObject(extractArg)
userToMove.innerhtml = "<b>" & user.cn & "</b>"
setCurPath(engine.getParent(user.distinguishedName))
end function
Для hta-приложений не существует удобного способа получения параметров командной строки, аналогичного обычным сценариям. Однако можно использовать свойство приложения commandline, содержащее полную командную строку вызова: название приложения и параметры. Все что нужно – отделить параметр. Следующая функция возвращает только выделенный параметр.
Свойство commandline имеет вид, подобный следующему:
"\\marklar.ua\UserMoveSupport\exec\enqueue.hta" "LDAP://main.marklar.ua/cn=Gomer J. Simpson, OU=South Park,DC=marklar,DC=ua" user
Командная строка состоит из трех частей: полный путь к приложению, выбранный объект пользователя (тот объект, контекстное меню которого использовалось для вызова сценария) и класс объекта. Поскольку вызвать приложение можно только для объектов типа user, проверку типа можно не делать. То есть нам нужно извлечь вторую часть.
Для выделения параметра используется следующее регулярное выражение:
".+" "?([^"]+)"? .+
Первая часть («.+») обозначает путь к приложению: «один и больше произвольных символов, заключенных в кавычки». Затем следует пробел и вторая часть («?([^»]+)»?), путь к объекту пользователя: «любое количество не кавычек, которое может быть заключено в кавычки». Обратите внимание, второй параметр в случае, когда он не содержит пробелов, передается без кавычек. Именно поэтому в выражении используются знаки вопроса после кавычек, обозначающие один или ноль символов. Затем описывается третья часть после пробела: любое количество любых символов (.+).
Нам необходима вторая часть, поэтому именно ее берем в скобки, чтобы захватить (capture) результат. В VBScript строка не может содержать двойных кавычек, поэтому в программе я написал выражение, используя одинарные, а потом заменил их нужными с помощью функции replace. Символ двойной кавычки был получен по его коду с помощью функции char.
function extractArg
dim re, aMatch, q
set re = new Regexp
re.pattern = _
replace("'.+' '?([^']+)'? .+", "'", chr(34))
re.ignoreCase = True
set aMatch = re.execute(app.commandline)
if aMatch.count > 0 then
if aMatch(0).subMatches.count > 0 then
Извлекаем захваченную часть регулярного выражения, которую я взял в круглые скобки. Поскольку захватывалась только одна часть, то и извлекаем ее как нулевой элемент массива совпадений (aMatch) и вложенных совпадений (subMatches).
extractArg = aMatch(0).subMatches(0)
Путь к объекту передается сценарию в несколько непривычном виде: «LDAP://main.marklar.ua/cn=Gomer….». Обратите внимание на участок между названием протокола и начало отличительного имени. Это имя компьютера, на котором находится пользователь (один из контроллеров домена). Эту часть удаляем с помощью регулярного выражения:
re.pattern = "\/\/[^\/]*/"
Здесь две наклонные, и текст за ними вплоть до следующей наклонной линии заменяется двумя прямыми слешами. То есть в результате мы получаем более привычный путь к объекту: «LDAP://cn=Gomer….». В таком виде результат и возвращается из функции.
extractArg = re.replace(extractArg, "//")
end if
end if
end function
Основная подпрограмма приложения – заполнение списка организационными единицами текущего контейнера. Он должен обновляться каждый раз при смене родителя. Также список должен содержать два особых элемента: для выхода на уровень выше (< ..) и начала перемещения (> <).
sub populateList
Вначале очищаем список, установив количество его элементов равным нулю. Далее создаем два верхних элемента выхода на родителя и начала перемещения. Для этого используется функция newOption. Ей передается два параметра: видимая надпись элемента и путь к нему (т.е. путь к контейнеру). Для начала перемещения в качестве пути задается пустая строка.
startMove.ouList.options.length = 0
startMove.ouList.add newOption("< . .", backPath)
startMove.ouList.add newOption("> <", "")
dim curOU, subOU
В глобальной переменной curPath хранится путь к текущему контейнеру, содержимое которого и требуется отобразить в списке.
set curOU = getObject(curPath)
for each subOU in curOU
Перебираем все организационные единицы текущего контейнера и добавляем их в список с помощью той же функции newOption.
if subOU.class = "organizationalUnit" then startMove.ouList.add newOption(subOU.ou, subOu.ADSPath)
end if
next
Для удобства пользователей выделим контейнер, из которого мы пришли в текущий. Путь к нему хранится в переменной prevPath. Такой подход используется в большинстве файловых менеджеров: при выходе из папки на уровень выше она становится выделенной, чтобы можно было вернуться назад, нажав на <Enter>.
dim selectIt, opt
selectIt = 0
for each opt in startMove.ouList.options
if opt.value = prevPath then selectIt = opt.index
next
Для выделения заданного элемента списка используется свойство selectedIndex.
startMove.ouList.selectedIndex = selectIt
end sub
Вот функция создания нового элемента списка. Сначала создается элемент документа типа option, затем инициализируются его поля: text (надпись на элементе), value (значение элемента, на экране оно не обращается, но может быть получено с помощью свойства списка value).
Листинг 4. Создание нового элемента списка
function newOption(oText, oValue)
set newOption = document.createElement("option")
newOption.value = oValue
newOption.text = oText
end function
Перейдем к функции, которая назначена на событие двойного щелчка мышкой по элементу списка. Если значение выбранного элемента – пустая строка, начинается перемещение пользователя, в противном случае происходит переход в выбранный контейнер.
Листинг 5. Обработка события выбора элемента списка
function itemSelected()
dim newPath
newPath = startMove.ouList.value
if newPath <> "" then
setCurPath(newPath)
else
moveUserTo
end if
end function
Сначала рассмотрим переход в контейнер. Процедура устанавливает переданный ей путь в качестве текущего, запоминает предыдущее положение, если оно существует, отображает новый путь в удобочитаемом виде и обновляет содержимое списка.
Листинг 6. Переход в заданную организационную единицу
sub setCurPath(path)
if not isEmpty(curPath) then
prevPath = curPath
else
prevPath = path
end if
curPathShow.innerhtml = engine.ADSPath2Readable(path)
curPath = path
backPath = engine.getParent(path)
populateList
end sub
Для обработки нажатия клавиш используются те же подпрограммы. При нажатии <Enter> (код клавиши 13) вызывается та же процедура, что и при двойном щелчке мышью. В ответ на нажатие клавиши <Backspace> происходит установка родительского каталога в качестве текущего.
Листинг 7. Обработка нажатий клавиш
function onKey()
select case window.event.keyCode
case 13: itemSelected
case 8: setCurPath(backPath)
end select
end function
Теперь посмотрим на начало перемещения пользователя.
function moveUserTo()
dim comment, ans
Сначала нужно запросить у менеджера подтверждение на начало операции (диалоговое окно, содержащее параметры перемещения и кнопки OK и Cancel). Если положительного ответа не получено, функция завершает работу.
ans = msgbox("Moving " & user.cn & " to " & vbLF & engine.ADSPath2Readable(curPath), vbOKCancel + vbQuestion)
if ans = vbCancel then exit function
Если менеджер подтвердил начало операции, появляется диалоговое окно с просьбой ввести комментарий к перемещению. Затем вызывается метод move основного класса надстройки, который выполняет все необходимые действия.
comment = inputBox("Post your comment here")
msgbox engine.move(user.ADSPath, curPath, comment)
В конце приложение закрывается, поскольку вызов этой функции – единственная его цель.
window.close
end function
Отмена перемещения
Следующее приложение – отмена операции перемещения. Менеджер, поставив пользователя в очередь, должен иметь возможность его вернуть. Для этого потребуется следующее приложение (см. рис. 2).
Рисунок 2. Приложение для отмены перевода пользователя
Слева отображается список исходящих пользователей. То есть тех, которые были перемещены из этого подразделения, но еще не приняты в другие. Все эти операции можно отменить с помощью кнопки RollBack. При выборе конкретного пользователя появляется возможность посмотреть некоторые параметры перемещения (пункт назначения, время операции, кто перемещал и комментарий).
Рассмотрим реализацию основных функций приложения. В правой части окна расположен исходящий список пользователей. Рассмотрим подпрограмму его заполнения.
sub populateList
Сначала список очищается. Затем для текущего контейнера устанавливается фильтр для выбора только комнат ожидания (класс userMoveWaitingRoom).
viewOut.outcomingList.options.length = 0
ou.filter = Array("userMoveWaitingRoom")
dim room, roomFound
roomFound = false
for each room in ou
Получив указатель на первую (и единственную) комнату ожидания текущего контейнера, выходим из списка, установив флаг существования группы в значение «истина».
roomFound = true
exit for
next
Если у текущей организационной единицы нет комнаты ожидания, процедура завершает работу. В этом случае исходящих пользователей также не существует, и список остается пустым.
if not roomFound then exit sub
Затем выбираем из комнаты ожидания ссылки на отправленных пользователей. Тип userMoveChairLink был специально введен в надстройку для упрощения и ускорения выполнения этой операции. Благодаря этому она сводится к простому перебору элементов одного контейнера.
room.filter = Array("userMoveChairLink")
dim li, user, commanded, cmd
for each li in room
set user = getObject(li.userMoveLink)
ou.filter = Array("UserMoveCommand")
Затем просматриваем текущую организационную единицу на предмет наличия в ней невыполненных команд для текущего исходящего пользователя (их существование возможно, поскольку команды выполняются не мгновенно, их появление проверяется раз в 5 секунд, но может быть установлен и больший период).
commanded = false
for each cmd in ou
if cmd.userMoveTarget = user.ADSPath then
commanded = true
exit for
end if
next
Если для пользователя найдена невыполненная команда, то в список исходящих он уже не попадает. Нельзя отменить перемещение пользователя, для которого в очереди ожидания находится невыполненная команда. В противном случае произойдет сбой при попытке ее выполнения позже.
if not commanded then
viewOut.outcomingList.add newOption(user.cn, li.userMoveLink)
end if
next
Теперь список заполнен, но ни один его элемент пока не выбран, поэтому поля с дополнительной информацией очищаем.
viewOut.iDest.value = ""
viewOut.iTime.value = ""
viewOut.iMover.value = ""
viewOut.iComment.value = ""
end sub
Заполнение списка происходит из функции инициализации приложения. Здесь все практически то же, что и для предыдущего приложения: создается экземпляр главного класса надстройки, устанавливается фокус ввода на список, инициализируется текущий каталог.
Листинг 8. Инициализация приложения отмены перемещения
function window_onLoad()
set engine = createObject("UserMove.Engine")
viewOut.outcomingList.focus
set ou = getObject(extractArg)
ouName.innerhtml = "<b>" & ou.ou & "</b>"
populateList
end function
При выборе элемента списка поля справа от него должны отобразить дополнительную информацию.
function selChanged()
dim chair, s, mover, moverCN
Необходимые данные находятся в объекте стула ожидания, поэтому к нему и подключаемся. Стул является родительским контейнером по отношению к перемещаемому пользователю. Практически все свойства записываются в текстовые поля напрямую.
set chair = getObject(engine.getParent(viewOut.outcomingList.value))
viewOut.iDest.value = engine.ADSPath2Readable(engine.getParent(chair.parent))
viewOut.iTime.value = engine.fromUTC(CDate(chair.userMoveWhen))
viewOut.iComment.value = chair.userMoveComment
Объект инициатора перемещения может уже не существовать в системе. Это не должно привести к аварийному завершению подпрограммы, поэтому временно отключаем завершение при ошибке.
on error resume next
Затем делаем попытку подключиться к объекту инициатора.
set mover = getObject(chair.userMoveWho)
Если значение переменной err не равно нулю, операцию выполнить не удалось, и в поле отправителя помещается текст «Не найден» (Not found).
Затем опять включаем стандартную реакцию системы на ошибки:
on error goto 0
viewOut.iMover.value = moverCN
end function
И, наконец, рассмотрим процедуру отмены перемещения. Она вызывается при нажатии на кнопку RollBack.
function doRollBack()
dim userPath, ans
Вначале получаем путь к выбранному в списке перемещаемому пользователю. Если ничего не выбрано, выводится соответствующее сообщение, и процедура завершает работу.
userPath = viewOut.outcomingList.value
if userPath = "" then
MsgBox "Nothing is selected"
Else
Если что-то выбрано, задается уточняющий вопрос менеджеру, при положительном ответе на который и начинается отмена. Отмена перемещения реализована в методе rollback основного класса надстройки.
ans = msgbox("Do you want to rollback the transfer?", vbYesNo + vbQuestion)
if ans = vbNo then exit function
MsgBox engine.rollback(userPath)
end if
В конце заново заполняем список с исходящими пользователями. Отмененной операции там уже быть не должно.
populateList
end function
Приложение для менеджера целевого отдела
Менеджер по персоналу целевого отдела должен иметь возможность просмотра очереди входящих пользователей, подтверждения или отказа в перемещении. Приложение выглядит следующим образом (см. рис. 3).
Рисунок 3. Управление очередью входящих пользователей
Пользовательский интерфейс здесь практически аналогичен предыдущему, поэтому его html-код приводить не буду. Подпрограмма вывода дополнительной информации о выбранном элементе в текстовые поля справа рассмотрена не будет по той же причине. Заполняется список следующим образом.
sub populateList
Сначала подключаемся к комнате ожидания (переменная room) текущей организационной единицы. Делается это так же, как в функции populateList предыдущего приложения, поэтому полный листинг повторно приводиться не будет.
room.filter = Array("UserMoveChair")
dim ch, user, cmd, commanded
Затем из каждого стула ожидания получаем ссылку на объект пользователя (переменная user).
for each ch in room
for each user in ch
exit for
next
Дальнейшая реализация аналогична программе из предыдущего приложения. Нужно проверить, нет ли ожидающей команды для текущего пользователя, и, если нет, добавить его в список.
end sub
Чтобы согласиться на перевод пользователя в свое подразделение, менеджер нажимает кнопку Accept. Вызывается обработчик:
Листинг 9. Функция подтверждения перемещения выбранного пользователя
function doAccept()
dim userPath, ans
userPath = viewOut.incomingList.value
if userPath = "" then
MsgBox "Nothing is selected"
else
ans = msgbox("Do you want to accept the transfer?", vbYesNo + vbQuestion)
if ans = vbNo then exit function
MsgBox engine.accept(userPath)
end if
populateList
end function
Здесь проверяется, выбран ли какой-то элемент списка, задается уточняющий вопрос. Подтверждение осуществляется вызовом метода accept основного класса надстройки. Функция отказа от перевода полностью аналогична, за исключением того, что нужно вызвать метод deny класса UserMove.Engine.
Что в итоге?
Итак, была разработана надстройка, позволяющая разбить процесс перемещения пользователя с одной организационной единицы в другую на два этапа, каждый из которых можно делегировать менеджеру в конкретном подразделении. Надстройка массивна, но в результате вся работа с ней заключается в использовании трех элементов контекстного меню: UserMove: Incoming и UserMove: Outcoming для организационных единиц и UserMove: Start для объектов пользователей (см. рис. 4 и 5).
Рисунок 4. Дополнительный пункт меню для объектов пользователей
Рисунок 5. Дополнительные элементы меню для организационных единиц
В результате отпадает необходимость в использовании администратора с правами в обеих организационных единицах для перемещения профилей пользователей, что снижает общее количество сотрудников с «лишними» правами.
- Don Jones, Jefferey Hicks «Advanced VBScript for Microsoft Windows Administrators». – Washington: Microsoft Press, 2006. – 537 p.
- Чарли Рассел, Шарон Кроуфорд, Джейсон Джеренд. «Windows server 2003 +SP1 и R2. Справочник администратора». – М.: Издательство «ЭКОМ», 2006. – 1424 с.