1. Постановка задачи
На компьютерах с операционными системами Windows NT x.x при установке
создается учетная запись локального администратора, которая имеет
неограниченные права на данном компьютере. Если компьютер предполагается
использовать в домене, то, как правило, технический персонал
устанавливает один и тот же пароль для данной учетной записи. И как
правило он не очень сложный. При наличии физического доступа к рабочей
станции пароль администратора может быть легко подобран со всеми
вытекающими отсюда последствиями. Задача администратора сети -
установить достаточно сложный пароль для данной учетной записи и
периодически его менять. Если в домене несколько десятков компьютеров,
это может занять много времени. Если же в домене несколько сот
компьютеров, а часто они еще и географически разнесены, то без
автоматизации данного процесса не обойтись.
Определимся, что должна делать программа - утилита. Т.е. составим простой алгоритм работы:
- получить список имен компьютеров в домене (возможно отфильтрованный по заданному критерию);
- подключиться к каждому компьютеру из списка и сменить пароль.
Для более комфортной работы утилиты необходимо обеспечить должную обработку ошибок, а результат работы записать в базу.
2. ADSI
После того как определились, что надо сделать (в данном случае это не
составляет труда), встает вопрос о реализации. Первой мыслью было
использовать технологию WMI, но после краткого исследования проблемы
решено было остановиться на ADSI. Далее вольный перевод нескольких
предложений из MSDN:
ADSI - Active Directory Service Interfaces. Микрософт создала набор
COM-интерфейсов, предназначенных для доступа к различным службам
каталогов.
Служба каталогов - это распределенная система, которая предоставляет
средства для поиска и использования сетевых ресурсов различных типов.
Объектная модель ADSI базируется на COM - объектах. Программа клиент
управляет объектами через интерфейсы. Следующая таблица перечисляет
фундаментальные элементы ADSI.
Интерфейсы | Описание |
IADs | Используется для идентификации
объекта. Как фундаментальный интерфейс, поддерживаемый всеми ADSI
объектами, позволяет получить доступ к метаданным объекта, включая
описание объекта в схеме Active Directory . |
IADsContainer | Используется для
извлечения и управления объектом. Все ADSI объекта - контейнеры требуют
использование этого интерфейса для доступа к объектам в контейнере и
манипулирования ими. |
IADsPropertyList | Используется для работы со свойствами объекта. |
|
Сложные ADSI объекты могут поддерживать дополнительные интерфейсы.
3. VBS
Первая реализация задачи была сделана на VBS. И это понятно.
Достаточно зайти на сайт Микрософт и скачать готовые скрипты. И немного
их подправить под свои нужды. Кроме того, на VB код получается очень
короткий и легкий для восприятия. Вот пример создания списка
компьютеров из домена, расположенных в определенном organization unit в
Active Directory (AD):
Set objDictionary = CreateObject("Scripting.Dictionary")
strDomain = "LDAP://ou=Test, ou=Mine, dc=mydomain, dc=com"
Set objDomain = GetObject(strDomain)
objDomain.Filter = Array("computer")
i = 0
For Each objComputer In objDomain
objDictionary.Add i, Mid(objComputer.Name,4)
i = i + 1
Next
Рисунок 1
Для получения доступа к пространству имен каталога необходимо связаться с нужным объектом ADSI.
Set objDomain = GetObject(strDomain)
strDomain - строка связывания.
Первая часть строки связывания определяет, к какой именно службе каталогов мы обращаемся.
Примеры обращения к различным службам
"LDAP://" | Служба каталогов, созданная на основе протокола LDAP (Active Directory в том числе) |
"WinNt://" | Служба каталогов в сети Windows NT 4.0 или на рабочей станции Windows XP/2000 |
|
Вторая часть строки связывания определяет положение объекта в каталоге.
В следующих таблицах приводятся примеры строк связывания:
LDAP
LDAP: | Связь с корнем пространства имен LDAP |
LDAP://server01 | Связь с конкретным сервером |
LDAP://server01:390 | Связь с конкретным сервером через указанный порт |
LDAP://CN=Jeff Smith,CN=users,DC=fabrikam,DC=com | Связь с конкретным объектом |
LDAP://server01/CN=Jeff Smith,CN=users,DC=fabrikam,DC=com | Связь с конкретным объектом через указанный сервер |
|
WinNT
WinNT://<domain name> |
WinNT://<domain name>/<server> |
WinNT://<domain name>/<path> |
WinNT://<domain name>/<object name> |
WinNT://<domain name>/<object name>,<object class> |
WinNT://<server> |
WinNT://<server>/<object name> |
WinNT://<server>/<object name>,<object class> |
|
Устанавливаем фильтр для выделения объектов - компьютеров.
objDomain.Filter = Array("computer")
И затем перебираем элементы коллекции.
Главный минус данной реализации (на мой взгляд) - это низкая скорость
работы. Для перебора ~150 рабочих станция и смены на них пароля
понадобилось около часа времени.
Основные задержки приходятся на операцию связывания. Особенно большие
таймауты при попытке связывания с выключенным или не существующим
компьютером ( или если по какой-то причине отказано в доступе). Решением
данной проблемы является организация многопоточности. Поэтому от VBS
пришлось отказаться.
4. Реализация на Delphi.
Задача была реализована на Delphi6 sp2. В процессе работы оказалось,
что необходимые функции не описаны в библиотеке. Далее в статье будут
приведены описания всех необходимых функций.
Первым этапом попытаемся установить связь AD. Для этого воспользуемся функцией ADsGetObject. Описание из MSDN:
HRESULT ADsGetObject(
LPWSTR lpszPathName,
REFIID riid,
VOID** ppObject);
lpszPathName - строка связывания;
riid - идентификатор интерфейса;
ppObject - указатель на указатель интерфейса, возвращаемый функцией.
Эта функция эквивалентна функции GetObject из VB (в данном
контексте).Она берет строку связывания и возвращает указатель на
запрашиваемый интерфейс. Связывание производится в контексте защиты
вызывающего потока, используя опции ADS_SECURE_AUTHENTICATION. Если
требуется указать конкретного пользователя, необходимо использовать
функцию ADsOpenObject (прошу прощения за корявый перевод).
Далее пример использования ADsGetObject для связывания с AD:
interface
Uses :. , ActiveDs_TLB;
:
function ADsGetObject(lpszPathName: WideString; const riid: TGUID; out ppObject: Pointer): HRESULT; stdcall;
implementation
function ADsGetObject; external 'activeds.dll';
Procedure TForm1.Test
Var hr: HResult;
objDomain: Pointer;
begin
hr:= ADsGetObject('LDAP://ou=test, ou=mine, dc=mydomain, dc=com', IID_IADsContainer,
objDomain);
if Failed(hr) then Exit;
end;
Чтобы данный пример мог быть откомпилирован необходимо импортировать библиотеку типов Activeds.tlb, как показано на рисунке 2:
Рисунок 2
Замечание:
При работе с ADsGetObject бывали ситуации, когда при попытке
прочитать какое-либо свойство полученного объекта выходила ошибка 'The
directory property cannot be found in cache'. К сожалению, это было
достаточно давно, и восстановить ситуацию не удалось. Тем не менее
ошибка была. Обойти ее удалось при использовании функции ADsOpenObject .
Вот пример использования данной функции:
interface
Uses :. , ActiveDs_TLB;
:
function ADsOpenObject(lpszPathName: WideString; lpszUserName: WideString; lpszPassword: WideString;
dwReserved: DWORD; const riid: TGUID; out ppObject: Pointer): HRESULT; stdcall;
implementation
function ADsOpenObject; external 'activeds.dll';
Procedure TForm1.Test
Var hr: HResult;
objDomain: Pointer;
begin
hr:= ADsOpenObject('LDAP://ou=test, ou=mine, dc=mydomain, dc=com', '', '',
DS_SECURE_AUTHENTICATION, IID_IADsContainer, objDomain);}
if Failed(hr) then Exit;
end;
Далее в статье будет использоваться только ADsGetObject.
В данных примерах мы пытаемся получить ссылку на интерфейс IID_IADsContainer.
IID_IADsContainer используют для получения коллекции ADSI объектов.
Полный список интерфейсов, с которыми можно работать при помощи
ADsGetObject, и их описание можно найти в MSDN.
После того, как мы получили ссылку на контейнер, осталось перебрать
его объекты и считать их имена. Для этого нам понадобятся еще две
функции - AdsBuildEnumerator и ADsEnumerateNext.
AdsBuildEnumerator- создает объект Enumerator (перечеслитель) для конкретного объекта контейнера ADSI.
function ADsBuildEnumerator(pADsContainerL: IADsContainer; ppEnumVariant: PIEnumVARIANT): HRESULT; stdcall;
function ADsBuildEnumerator; external 'activeds.dll';
pADsContainerL - указатель на IADsContainer;
ppEnumVariant - указатель на указатель
IEnumVariant интерфейс, который связывает создаваемый объект Enumerator с
соответствующим объектом контейнером.
Интерфейс IEnumVARIANT описан в модуле ActiveX.
ADsEnumerateNext - позволяет перемещать указатель по элементам коллекции.
function ADsEnumerateNext(pEnumVariant: IEnumVARIANT; cElements: ULONG; pvar: POleVariant;
pcElementsFetched: PULONG): HRESULT; stdcall;
function ADsEnumerateNext; external 'activeds.dll';
pEnumVariant - получаем после вызова ADsBuildEnumerator;
cElements - количество элементов, которые мы хотим извлечь из коллекции за один раз;
pvar - указатель на массив, в который помещаются извлеченные из коллекции объекты;
pcElementsFetched - указатель на фактическое количество найденных элементов.
Далее, собственно, пример, демонстрирующий как получить список компьютеров домена из AD:
procedure TForm1.Button1Click(Sender: TObject);
var objDomain: Pointer;
objChild: Pointer;
hr: HResult;
s: String;
i: Integer;
iArr : OleVariant;
iEnum: IEnumVARIANT;
iFetch: ULONG;
iAPath: String;
begin
ListBox1.Clear;
hr:= ADsGetObject('LDAP://ou=test, ou=mine, dc=bogatyr, dc=kz', IID_IADsContainer, objDomain);
if Failed(hr) then Exit;
hr:=ADsBuildEnumerator(IADsContainer(objDomain), @iEnum);
if Failed(hr) then Exit;
hr := ADsEnumerateNext(iEnum, 1, @iArr, @iFetch);
while (S_OK = hr) and (1 = iFetch) do
begin
hr:=IDispatch(iArr).QueryInterface(IADs,objChild);
if Failed(hr) then Exit;
if AnsiLowerCase(IAds(objChild).Class_)='computer' then
begin
s:=IAds(objChild).Name;
System.Delete(s,1,3);
ListBox1.Items.Add(s);
end;
if AnsiLowerCase(IAds(objChild).Class_)='organizationalunit' then
begin
Continue;
end;
if AnsiLowerCase(IAds(objChild).Class_)='container' then
begin
Continue;
end;
iArr:=null;
hr := ADsEnumerateNext(iEnum, 1, @iArr, @iFetch);
end;
end;
Часть кода в примере закомментирована. Код взят из рабочей программы и
слегка исправлен. В закомментированных частях видно, что подпрограмма
вызывается рекурсивно. Это было сделано что бы просканировать всю
указанную ветку из AD, включая содержащиеся внутри ветки.
Здесь все просто. Формируем строку связывание для доступа к объекту с
именем "Администратор". Класс объекта - "user". Объект расположен на
рабочей станции "Computer01".
iPath:='WinNT://'+NameWs+'/Администратор,user';
И, собственно, реализация.
procedure ChangePassword;
var objUser: Pointer;
hr: HResult;
iPath: String;
i: Integer;
begin
iPath:='WinNT://Computer01/Администратор,user';
hr:= ADsGetObject(iPath, IID_IADsUser, objUser);
if hr<>S_OK then Exit;
IADsUser(objUser).SetPassword('anykey');
end;
Если вызов ADSI функции завершился неудачей, функция вернет код
ошибки стандартным для COM объектов способом. Коды ошибок делятся не
четыре группы:
- Универсальные коды ошибок COM;
- Универсальные коды ошибок ADSI;
- Коды ошибок Win32 для ADSI;
- Коды ошибок LDAP для ADSI;
Кроме того, некоторые интерфейсы предоставляют дополнительные
сведения об ошибке, которые могут быть получены при помощи функции
ADsGetLastError.
function ADsGetLastError(lpError: LPDWORD; lpErrorBuf: LPWSTR; dwErrorBufLen: DWORD;
lpNameBuf: LPWSTR; dwNameBufLen: DWORD): HRESULT; stdcall;
lpError - указатель на код ошибки;
lpErrorBuf - указатель на буфер, куда будет передано описание ошибки;
dwErrorBufLen - размер буфера;
lpNameBuf - указатель на буфер, куда будет передано имя провайдера, который возбудил эту ошибку;
dwNameBufLen - размер буфера;
Простой пример использования этой функции можно будет посмотреть в исходных кодах, прилагаемых к статье.
Скачать исходный код
|