Проводим реализацию тонкого делегирования прав в Active Directory::Журнал СА 7.2009
www.samag.ru
     
Поиск   
              
 www.samag.ru    Web  0 товаров , сумма 0 руб.
E-mail
Пароль  
 Запомнить меня
Регистрация | Забыли пароль?
Журнал "Системный администратор"
Журнал «БИТ»
Наука и технологии
Подписка
Где купить
Авторам
Рекламодателям
Магазин
Архив номеров
Вакансии
Контакты
   

  Опросы

Какие курсы вы бы выбрали для себя?  

Очные
Онлайновые
Платные
Бесплатные
Я и так все знаю

 Читать далее...

1001 и 1 книга  
20.12.2019г.
Просмотров: 5068
Комментарии: 0
Dr.Web: всё под контролем

 Читать далее...

04.12.2019г.
Просмотров: 6312
Комментарии: 0
Особенности сертификаций по этичному хакингу

 Читать далее...

28.05.2019г.
Просмотров: 7569
Комментарии: 2
Анализ вредоносных программ

 Читать далее...

28.05.2019г.
Просмотров: 7893
Комментарии: 1
Микросервисы и контейнеры Docker

 Читать далее...

28.05.2019г.
Просмотров: 6944
Комментарии: 0
Django 2 в примерах

 Читать далее...

Друзья сайта  

Форум системных администраторов  

sysadmins.ru

 Проводим реализацию тонкого делегирования прав в Active Directory

Архив номеров / 2009 / Выпуск №7 (80) / Проводим реализацию тонкого делегирования прав в Active Directory

Рубрика: Администрирование /  Продукты и решения

ВАДИМ АНДРОСОВ, ассистент ВНУ, специалист 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. Приложения начала перемещения пользователя

Рисунок 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. Приложение для отмены перевода пользователя

Рисунок 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. Управление очередью входящих пользователей

Рисунок 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. Дополнительный пункт меню для объектов пользователей

Рисунок 4. Дополнительный пункт меню для объектов пользователей

Рисунок 5. Дополнительные элементы меню для организационных единиц

Рисунок 5. Дополнительные элементы меню для организационных единиц

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

  1. Don Jones, Jefferey Hicks «Advanced VBScript for Microsoft Windows Administrators». – Washington: Microsoft Press, 2006. – 537 p.
  2. Чарли Рассел, Шарон Кроуфорд, Джейсон Джеренд. «Windows server 2003 +SP1 и R2. Справочник администратора». – М.: Издательство «ЭКОМ», 2006. – 1424 с.

Комментарии отсутствуют

Добавить комментарий

Комментарии могут оставлять только зарегистрированные пользователи

               Copyright © Системный администратор

Яндекс.Метрика
Tel.: (499) 277-12-41
Fax: (499) 277-12-45
E-mail: sa@samag.ru