Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

NetAPI: Работа с учетными записями пользователей.


Информационный Канал Subscribe.Ru


Программирование для Windows NT на Delphi
Выпуск номер : 1 [ 8 июля 2002 года]

Добрый день.

С прошлого выпуска рассылки пришло только одно письмо. В этот раз подписчиков побольше, поэтому хотелось бы услышать ваши отклики.
Сегодня в рассылке:
- Управление учетными записями пользователей
- FAQ : Посылаем сообщение (как net send)
- Новый взгляд на завершение других процессов

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

Для начала несколько слов о Net API и Delphi.
Во-первых, все функции используют Unicode набор символов. И хотя ходят слухи о всемогущем дельфевом компиляторе, якобы самостоятельно преобразующем обычные (String,PChar) строки в длинные (WideString,PWideChar), но я буду в примерах переконвертировать их самостоятельно (чего и вам советую). Это можно сделать так.
Во-вторых, к сожалению, в стандартной поставке Delphi описаний этих функций нет. Поэтому тут есть два выхода - описать их самим или скачать отсюда - ftp://delphi-jedi.org/api/LanManager.zip [330 Kb]

Все названия функций имеют вид Net[имя объекта][действие]. Например, функция получения информации (GetInfo) о пользователе (User) - NetUserGetInfo.
В Net API для многих функций используются многоуровневые типы данных. Какой уровень надо использовать зависит от детальности запрашиваемой информации и наличия прав. Для примера возьмем NetServerGetInfo. Вторым параметром у нее level : DWORD, а третьим указатель на буфер. Для данной функции возможны 3 уровня - 100,101 и 102. Уровень 100 дает минимум информации о сервере - только имя и тип платформы. Уровень 101 уже побольше. Но больше всех инфы дает уровень 102, но ... эту информацию уже могут получить только пользователи с административными привилегиями. Так что совет - не жадничайте и запрашивайте только тот уровень, который вам необходим. Итак, вторым параметром мы поставим уровень 101 и в третьем получим указатель на какой-то буфер. Но что будет в этом буфере? А будет в нем структура SERVER_INFO_101. Для уровня 102 соответственно будет структура SERVER_INFO_102 и т.д.

Теперь о буфере - буфер выделяется нам системой и она же должна будет его очистить. Так что не забываем использовать, где надо NetApiBufferFree. Выделять буфер самостоятельно не надо (такую ошибку я допустил в одном из примеров в FAQ).

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

Статья
Работа с учетными записями пользователей

Для начала получим список всех учетных записей пользователей на сервере.(Под словом сервер здесь и далее понимается компьютер - сервер или рабочая станция) Делается это функцией NetUserEnum.


Uses LmAccess, LmCons,LmApiBuf,...; // Это описания нужных функций из архива

type
 PUserArray = ^TUserArray;
 TUserArray = array of USER_INFO_10;
 PUSER_INFO_10 = ^USER_INFO_10;

procedure EnumUsers;
Var
 I : Integer;
 pBuffer : Pointer;
 cbRead,cbTotal : Cardinal;
 hRes : Cardinal;
 lpwSrv : PWideChar;
Begin
 UsrList.Clear; // Очищаем TListBox от записей

 lpwSrv := Str2Wide(Comp.Text); // делаем Unicode копию строки

 cbRead :=0;   //кол-во прочитанных учеток
 cbTotal := 0; //общее кол-во учеток
 hRes :=0;     //об этом попозже

 // Получаем список ВСЕХ учетных записей на сервере lpwSrv
 // pBuffer - указатель на массив элементов типа USER_INFO_*
 // Cardinal(-1) обозначает ВСЕ учетные записи
 if NetUserEnum(lpwSrv,10,FILTER_NORMAL_ACCOUNT,
        pBuffer,Cardinal(-1),cbRead,cbTotal,@hRes)=0
  // теперь у нас в cbRead - кол-во элементов
  // а в cbTotal- их общее кол-во (в данном случае они должны совпадать)
  then begin
   for i:=0 to cbRead-1 do
    // Выводим список пользователей в ListBox
    UsrList.Items.Add(TUserArray(pBuffer)[i].usri10_name);
   end;

  // Очищаем буффер
  NetApiBufferFree(pBuffer);

  // Очищаем буффер после Str2Wide
  FreeMem(lpwSrv);
End;
и все бы было хорошо, если бы не Cardinal(-1). Если количество учеток не велико, то система выделит небольшой буфер и поместит туда массив структур. Но если у нас несколько тысяч учетных записей, то я бы не рекомендовал использовать эту величину. Вместо этого мы будем получать их по частям. Как раз для этого и используется последний параметр функции.

procedure EnumUsers2;
Var
 I : Integer;
 pBuffer : Pointer;
 cbRead,cbTotal : Cardinal;
 hRes : Cardinal;
 lpwSrv : PWideChar;
 Res :  Cardinal;
Begin
 UsrList.Clear;

 lpwSrv := Str2Wide(Comp.Text);

 cbRead :=0;
 cbTotal := 0;
 hRes :=0;

 repeat
  Res:=NetUserEnum(lpwSrv,10,FILTER_NORMAL_ACCOUNT,
                      pBuffer,100,cbRead,cbTotal,@hRes);
  if (Res=0) or (Res=ERROR_MORE_DATA)
   then begin
    for i:=0 to cbRead-1 do
     UsrList.Items.Add(TUserArray(pBuffer)[i].usri10_name);
    NetApiBufferFree(pBuffer);
   end;
 until Res<>ERROR_MORE_DATA;

  FreeMem(lpwSrv);
End;
Функция возвращает ERROR_MORE_DATA если еще остались записи, которые не поместились в буфер. И пока они остались, мы будем вызывать функцию NetUserEnum снова и снова. В hRes помещается описатель, который необходим для продолжения перечисления.

Пока мы не перешли к другой функции - Код Str2Wide, переводящей ANSI символы в Unicode.

function Str2Wide(lpStr: String):PWideChar;
var
 dwStrlen : Cardinal;
begin
 dwStrLen := lstrlen(PChar(lpStr));
 GetMem(Result,(dwStrLen+1)*2);
 StringToWideChar(lpStr,Result,dwStrLen+1);
end;
Не забывайте делать FreeMem!!! Wide := Str2Wide(Str);... FreeMem(Wide);

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

function AddUser(UserName,Password,Comment:String):boolean;
var
 Size  : Cardinal;
 Res   : Cardinal;
 User  : USER_INFO_1; // Уровня 1 вполне достаточно
 dwErr : Cardinal;
 lpwSrv : PWideChar;
begin
 ZeroMemory(@User,SizeOf(USER_INFO_1)); //Обнуляем структуру
 // Это делать не обязательно, если мы заполняем все ее поля.
 User.usri1_name:=Str2Wide(UserName);
 User.usri1_password:=Str2Wide(Password);
 User.usri1_comment:=Str2Wide(Comment);

 User.usri1_priv:=1; // Уровень привилегий - ДОЛЖЕН БЫТЬ 1

 lpwSrv := Str2Wide(Comp.Text); // Имя компьютера

 Res:= NetUserAdd(nil,1,@User,@dwErr);
 // Res - код ошибки. Для перевода кода ошибки в строку можно
 // использовать SysErrorMessage(Res) из SysUtils
 Result:=(Res=0);
end;
 
Ну и осталось научиться получать информацию о пользователях и менять ее. NetUserGetInfo получает информацию о аккаунте пользователя.

procedure GetUserInfo(Server,User: String);
var
 UserInfo : ^USER_INFO_2;
 lpwUser  : PWideChar;
 lpwSrv   : PWideChar;
begin
 lpwSrv  := Str2Wide(Server);
 lpwUser := Str2Wide(User);

 NetUserGetInfo(lpwSrv,lpwUser,2,Pointer(UserInfo));
  // Куда - то выводим информацию
 // При желании изменяем и сохраняем новую инфу
 NetUserSetInfo(nil,lpwUser,2,UserInfo,nil);

 NetApiBufferFree(UserInfo);
end;   
Из всех уровней самый интересный, имхо, второй. (Правда туда пускают только админов, а неадминов пускают на 0 уровень, не содержащий ничего полезного). Вот его то мы и разберем.

usri2_name - имя пользователя - это думаю понятно
usri2_password - пароль - всех надеющихся увидеть здесь пароль огорчу - здесь его нет и не будет (поле используется только при установке информации)(W)
usri2_password_age - "возраст" пароля - кол-во секунд, прошедших с момента последней смены пароля (R)
usri2_priv - уровень привилегий - 1 - обычный пользователь, 0 - гость, 2 - админ (определяется группами, в которых находится пользователь) (R)
usri2_home_dir - основной каталог пользователя - обычно используется для подключения сетевого каталога пользователя (например, \\server\users\thisuser)
usri2_comment - описание
usri2_flags - флаги состояния аккаунта -
UF_ACCOUNTDISABLE = $0002 - отключена
UF_PASSWD_CANT_CHANGE = $0040 - пароль не может быть сменен пользователем
UF_DONT_EXPIRE_PASSWD = $10000 - срок действия пароля не ограничен

usri2_script_path - скрипт, выполняемый при входе в систему
usri2_auth_flags - административные флаги - какие привилегии имеет пользователь (R)
AF_OP_PRINT = $1; // Print Operator
AF_OP_COMM = $2; // Communications Operator
AF_OP_SERVER = $4; // Server Operator
AF_OP_ACCOUNTS= $8; // Account Operator

usri2_full_name - полное имя пользователя
usri2_usr_comment - пользовательский комментарий
usri2_workstations - до восьми компьютеров, на которых может работать пользователь
usri2_last_logon и usri2_last_logoff - время последнего входа и выхода пользователя (R)
usri2_acct_expires - время (кол-во секунд с 1.01.1970 года) истечения срока действия пароля
usri2_max_storage - макс. размер дискового пространства, который может использовать юзер
usri2_units_per_week и usri2_logon_hours определяют разрешенное время входа
usri2_bad_pw_count - кол-во вводов неправильного пароля (R)
usri2_num_logons - кол-во входов в систему (R)
usri2_logon_server - имя сервера, обрабатывающего вход в систему.
usri2_country_code и usri2_code_page - коды региона и кодовой страницы языка.
(R) и (W) - поля только для чтения и только для "записи".

Устанавливается информация функцией NetUserSetInfo.
NetUserSetInfo(nil,lpwUser,2,UserInfo,nil);

UserInfo - указатель на структуру User_Info_2 полученную функцией NetUserGetInfo.

В принципе, все поля устанавливаются очень просто, кроме usri2_logon_hours.
Что же это за поле? Это 168 битная строка, каждый бит которой - час недели. Первый бит - с 000 до 059 Воскресения, Второй с 100 до 159 и т.д. и все это для нулевого часового пояса (GMT). Для 3 часового пояса нулевой бит - время с 300 до 359.
Реализуем процедуру, разрешающую вход в определенный час


type
 TLogonHours = array [0..20] of Byte;

procedure SetHour(hour,day : Byte;var mas : TLogonHours);
var
 ByteNum: Cardinal;
 dwHour : Integer;
begin
 // Вычисляем номер часа в неделе по GMT
 //TimeZone - часовой пояс (разница времени по Гринвичу (GTM) и в вашей местности)
 dwHour := hour+24*day-TimeZone;

 if dwHour<0 then dwHour := 168+dwHour;
 ByteNum:= dwHour div 8; // Номер байта в массиве
 // Устанавливаем соответствующий бит в байте
 mas[bytenum] := mas[bytenum] or (1 shl (dwHour mod 8));
end;

var
 hours : TLogonHours;

 SetHour(12,1); // разрешаем вход в пн. с 12 до 13
 UserInfo.usri2_logon_hours:=@hours;
И еще - проверить включен определенный флаг или нет можно так
// Заблокирован ли юзер (UF_ACCOUNTDISABLE = $0002)
Result:= ((UserInfo.usri2_flags and UF_ACCOUNTDISABLE)=0);
Вопросы и ответы
Как послать сообщение пользователю или на другой компьютер (как net send ..) ?

Странно, но этот вопрос оказался самым популярным в FAQ (а раньше самым популярным был про перезагрузку :)), поэтому я его публикую здесь. Кстати, вопрос тоже на тему NetAPI.

Function NetMessageBufferSend (
  servername: LPCWSTR; msgname: LPCWSTR;
  fromname: LPCWSTR; buf: Pointer;
  buflen: DWORD
): longint; stdcall; external 'netapi32.dll';

Implementation

// процедура посылки сообщения
Procedure SendLanMessage;
Var
 HostName: LPCWSTR;
 RName: LPCWSTR;
 buf: Pointer;
 text: String;
 size: Integer;
Begin
 // Посылаем сообщение с локального компьютера для User фразу
 // "We are testing lan manager"
 HostName := Nil;
 RName := 'User';
 text := 'We are testing lan manager';

 // Перевод String в WideString
 size := SizeOf (WideChar) * Length (text)+1;
 GetMem (buf, Size);
 PWideChar (Buf) := StringToWideChar (text, buf, Length (text) + 1);

 If NetMessageBufferSend (HostName, RName, Nil, buf, size) <> NO_ERROR
  then ShowMessage ('Error');
End;
Напоследок
Новый взгляд на завершение других процессов

Интересный способ завершения других процессов, которым поделился Ворническу Владимир (за что ему огромное спасибо).
Обычно процесс завершают TerminateProcess'ом. При этом, как рассказывается в MSDN, пользоваться им стоит только в экстренных случаях, т.к. DLL'ки используемые процессом, не получают уведомления о завершении процесса :(. А вот при ExitProcess все нормально :) Ну вот и будем использовать ExitProcess...

function NTKillProcess(iProcessID: Integer): Integer;
var
 hProcess:    Integer;
 pfnExitProcess: Pointer;
 hInstance:   Integer;
 hThread:    DWORD;
begin
 Result:=ERROR_SUCCESS;

 hProcess:=OpenProcess(PROCESS_ALL_ACCESS, FALSE, iProcessID);

 if hProcess > 0 then begin
  hInstance:=GetModuleHandle('KERNEL32.DLL');
  pfnExitProcess:=GetProcAddress(hInstance, 'ExitProcess');
  hThread:=CreateRemoteThread(hProcess, nil, 0, pfnExitProcess, nil, 0,hThread);
  if hThread>0 then begin
     WaitForSingleObject(hThread, 40000);
     if not CloseHandle(hThread) then Result:=GetLastError;
   end else Result:=GetLastError;
   CloseHandle(hProcess);
  end else Result:=GetLastError;
end;
Добавлю, что работать, скорее всего, будет только для Win32 процессов (а другие редко бывают).


На сегодня это все. Очень бы хотелось попросить вас писать мне на flint@xonix.ru если увидите ошибку, хотите поделиться информацией или задать вопрос, высказать мнение о рассылке ... в общем, по любому поводу.


http://subscribe.ru/
E-mail: ask@subscribe.ru
Отписаться
Убрать рекламу

В избранное