Информация о модулях из этой главы
Название |
Идентификатор на CPAN |
Версия |
|
User: : pwen t (входит в состав Perl) | |||
File : : stat (входит в состав Perl) | |||
Win32: :AdminMisc (можно найти на
http:// www.roth.net) |
20000117 | ||
Win32: : Perms (можно найт^ на
http: //www.roth.net) |
20000216 | ||
Win32: : Lanman (можно найти на
ftp://ftp.roth.net/pub/ ntperl i 'Others / 'Lanman / ) |
1.05 | ||
1C . : Fi Ifi (входит в состав Perl) | GBARR | 1.20 | |
XML: :Writer | DMEGG | 0.30 | |
xml: : Parser | COOPERCL | 2.27 | |
Data: : Dumper | GSAR | 2.101 | |
XML: :Simple | GRANTM | 1.01 | |
Expect.pm | AUSCHUTZ | 1.07 | |
File: :Path (входит в состав Perl) | 1.0401 | ||
Win32: :FileOp | JENDA | 0.10.4 |
Информация о пользователях в Unix
При обсуждении этой темы мы будем иметь дело лишь с несколькими ключевыми файлами, поскольку в них хранится постоянная информация о пользователе. Говоря «постоянная», я имею в виду атрибуты, которые существуют до тех пор, пока существует пользователь, сохраняясь даже тогда, когда пользователь не зарегистрирован в системе. Иначе я буду называть это учетной записью. Если в системе у вас есть учетная запись, вы можете зарегистрироваться и стать пользователем данной системы.
Существование пользователя в системе начинается с того момента, когда информация о нем впервые заносится в файл паролей (или же служба каталогов обеспечивает аналогичную информацию). Пользователь «уходит со сцены», когда эта запись удаляется. Рассмотрим подробнее, как хранится эта информация.
Классический файл паролей в Unix
Начнем мы с «классического» формата файла паролей, а затем перейдем к более сложным вопросам. Я называю данный формат классическим, потому что на нем основаны все существующие в настоящее время форматы файлов паролей в Unix. Более того, он и сейчас встречается во многих вариантах Unix, включая SunOS, Digital Unix и Linux. Обычно это файл /etc/passwd, содержащий последовательность текстовых ASCII-строк, причем каждая строка соответствует одной учетной записи или является ссылкой на другую службу каталогов. Любая строка файла состоит из нескольких полей, разделенных двоеточиями. Мы внимательно рассмотрим все эти поля, после того как научимся их получать.
Вот пример строки из файла /etc/passwd:
dnb:fMP.olmno4jGA6:6700:520:David N. Blank-Edelman:/home/dnb:/bin/zsh
Существует по крайней мере два способа получать подобную информацию средствами Perl:
1. К файлу можно обратиться «вручную», рассматривать его как обычный текстовый и соответствующим образом анализировать:
Spasswd = "/etc/passwd":
open(PW, Spasswd) or die "Невозможно открыть $passwd:$' '-n":
while (<PW>){
($name,$passwd,$iiid.$gid. $дсоз. $dir. $srelL ,) = split (/:/):
<далее следует ваша программа>
}
close(PW);
2. Другой способ позволяет «предоставить все полномочия системе». В этом случае нам будут доступны некоторые библиотечные вызовы Unix, которые
проанализируют файл за нас. Тогда последний пример можно переписать так:
while( ($пагле, Spasswd, $uid,$gid,$gcos,$dir,$shell) = getpwent( ) ){
<далее следует ваша программа>
}
endpwent();
Употребление системных вызовов содержит еще одно преимущество: их можно автоматически использовать с любой из служб имен (например, NIS). Вскоре мы рассмотрим и другие системные вызовы (включая более простой способ применения getpwent( )), а пока разберемся с полями, которые получили в наших примерах:1
Имя
В этом поле хранится короткое (обычно не длиннее восьми символов), уникальное в пределах системы регистрационное имя пользователя. Функция getpwent( ), которую мы уже видели в предыдущем примере в списочном контексте, возвращает значения данного поля, если вызывается в скалярном контексте:
$name = getpwent( ),
Идентификатор пользователя (UID)
В Unix-системах идентификатор пользователя (UID) зачастую более важен, чем регистрационное имя. Все файлы в системе принадлежат пользователю с каким-либо идентификатором, а не регистрационным именем. Если в файле /etc/passwd
поменять регистрационное имя пользователя, обладающего идентификатором 2397, с danielr на drinehart, то мгновенно владельцем всех его файлов станет пользователь drinehart. Для операционной системы идентификатор пользователя - постоянная информация. При выделении ресурсов и выяснении прав ядро и файловые системы следят за идентификаторами, а не регистрационными именами. Регистрационное имя можно считать информацией, внешней для операционной системы; эта информация существует, чтобы упростить жизнь пользователя.
Вот простой пример, позволяющий установить очередной доступный уникальный идентификатор в файле паролей. Достаточно выяснить максимальный идентификатор и использовать его для создания следующего номера:
Spasswd = "/etc/passwd";
open(PW,$passwd) or die "Невозможно открыть $passwd:$!\n";
while (<PW>){
@fields = splitC/:/);
Shighestuid = (Shighestuid < $fields[2]) ? $fields[2] : $highestuid:
}
close(PW);
print "Следующий доступный идентификатор: " . ++$highestuid . "\n";
Ниже перечислены другие полезные функции и переменные, имеющие отношение к именам и идентификаторам пользователей (табл. 3.1).
Таблица 3.1. Переменные и функции, имеющие отношение к именам и идентификаторам пользователей
Функция/ Переменная |
Использование |
getpwnam($name) |
В скалярном контексте возвращает идентификатор, соответствующий этому регистрационному имени; в списочном контексте возвращает все поля данной записи из файла паролей |
getpwuid(Suid) |
В скалярном контексте возвращает регистрационное имя, соответствующее данному идентификатору; в списочном контексте возвращает все поля данной записи из файла паролей |
$> |
Соответствует эффективному идентификатору пользователя текущей выполняющейся программы на Perl |
$< |
Соответствует реальному идентификатору пользователя текущей выполняющейся программы на Perl |
В многопользовательских системах пользователи их группы часто работают с файлами и другими ресурсами совместно. В Unix существует механизм, позволяющий работать с группами пользователей. Учетная запись в системе может входить в несколько групп, но при этом принадлежать она должна только одной главной группе
(primary group). Поле GID в файле паролей соответствует как раз первичной группе для данной учетной записи.
Имена групп, их идентификаторы и члены группы обычно перечислены в файле /etc/group. Чтобы включить учетную запись в несколько групп, необходимо просто указать ее в нескольких местах данного файла. В некоторых операционных системах существует жесткое ограничение на число групп, которым может принадлежать учетная запись (а значит, и пользователь). Чаще всего ограничение равно 8. Вот пример пары строк из файла /etc/group:
bin : : 2:root,bin,daemon
sys: iSiroot.bin.sys.adu
Первое поле - это имя группы, второе - пароль ( в некоторых системах может употребляться пароль для присоединения к группе), третье - идентификатор группы и последнее поле - список пользователей в группе.
Способы объединения пользователей в группы зависят от конкретного узла, поскольку границы (и административные, и границы проектов) везде разные. Таким образом, группы можно создавать для разделения различных пользователей (студенты, продавцы и т. д.), по выполняемым действиям (операторы резервных копий, сетевые администраторы и т. д.) либо по назначению учетных записей (резервные учетные записи и пр.).
Работа с файлами групп средствами Perl очень похожа на процесс разбора файла passwd из предыдущих примеров. Его можно считать обычным текстовым файлом либо применять специальные функции для выполнения подобной задачи. Посмотрите на функции и переменные, имеющие отношение к группам (табл. 3.2).
Таблица 3.2. Переменные и функции, имеющие отношение к именам и идентификаторам групп
Функция/ Переменная |
Используется |
getgrent() |
В скалярном контексте возвращает имя группы; в списочном контексте возвращает поля: Sname, Soasswd, $gia, Sm'Xiers |
get.grnarfi(Snane) |
В скалярном контексте возвращает идентификатор группы; в списочном контексте возвращает те же поля, что и функция getgi-ent( ) |
getgrgio($gid) |
В скалярном контексте возвращает имя группы; в списочном контексте возвращает те же поля, что и функция дё-^"-' |
$) |
Соответствует эффективному идентификатору группы текущей выполняемой программы |
$( |
Соответствует реальному идентификатору группы текущей выполняемой программы |
Мы уже рассмотрели три основных поля, в которых содержится информация о пользователе в Unix. Следующее поле не является частью хранимой информации, но оно подтверждает права, обязанности и привилегии, присущие пользователю с конкретным идентификатором. Именно так компьютер узнает, что тому, кто выдает себя за пользователя mguerre, позволено присвоить конкретный идентификатор. Существуют и другие, лучшие формы авторизации (например, использование криптографических методов с открытым ключом), но этот способ унаследован от ранних версий Unix.
Очень часто в этом поле в файле паролей можно увидеть лишь звездочку (*). Подобный знак применяется для запрещения регистрации пользователя в системе, без удаления при этом самой учетной записи.
Работа с паролями пользователей - это отдельная тема. Ей будет посвящена глава 10 «Безопасность и наблюдение за сетью».
Поле GCOS
Поле GCOS самое бесполезное (с точки зрения компьютера). Обычно в этом поле записано полное имя пользователя (например «Рой Дж. Бив»). Часто люди добавляют туда должность и/или номер телефона.
Системные администраторы, заботящиеся о приватности пользователей (чему и следует быть), должны проверять содержимое данного поля. Это стандартный путь для определения соответствия между реальным именем пользователя и его регистрационным именем. В большинстве Unix-систем такое поле находится в файле /etc/passwd, доступном всем для чтения, следовательно, эта информация может попасть в руки кого угодно в системе. Многие программы, почтовые клиенты и демоны finger-запросов обращаются к этому полю при добавлении регистрационного имени пользователя к какой-то информации. Если у вас есть необходимость скрыть реальные имена пользователей от других людей (например, если речь идет о политических диссидентах, федеральных свидетелях или известных персонах), вы обязательно должны следить за этим полем.
В качестве дополнительной информации: если вы поддерживаете сайт с менее развитой пользовательской базой, было бы неплохо запретить пользователям изменять их поле GCOS на случайные строки (по тем же причинам, по которым выбранные пользователями регистрационные имена могут вызвать проблемы). Вряд ли вы придете в восторг, увидев в своем файле паролей бранные выражения или иную непрофессиональную информацию.
Домашний каталог
Следующее поле содержит имя
домашнего каталога пользователя. Это тот каталог, откуда начинается работа с системой. Обычно здесь хранятся файлы, определяющие настройки пользователя.
В целях безопасности очень важно, чтобы запись в домашний каталог была разрешена только его владельцу. Домашние каталоги, доступные для записи всем, открывают возможность хакерских действий. Правда, существуют ситуации, когда домашние каталоги, доступные для записи только самим владельцам, тоже вызывают проблемы. Например, в случае с ограниченными интерпретаторами (если пользователи могут регистрироваться в системе только для выполнения определенных задач без права изменять что-либо в системе) домашние каталоги, доступные для записи пользователю, категорически запрещены.
Вот пример кода на Perl, который позволяет убедиться, что все домашние каталоги пользователей принадлежат своим владельцам и недоступны для записи остальным:
use User::pwent; use File::stat;
ft замечание: этот код очень сильно загрузит машину, если
# домашние каталоги монтируются автоматически
while($pwent = getpwent()){
# убеждаемся, что это действительно каталог, даже если
# он спрятан за символическими ссылками
Sdirinfo = stat($pwent->dir."/."); unless (defined $dirinfo){
warn "Невозможно получить информацию о ".$pwent->dir.": $!\n"; next;
}
warn «Домашний каталог пользователя ".$pwent->name." не имеет в
ладельца с корректным uid (". $dirinfo->uid." вместо ".$pwent->uid.")!\n"
# ($dirinfo->uid != $pwent->uid);
# каталог может быть доступным всем для записи, если
# у него установлен «бит-липучка" (т. е. 01000),
# подробности в странице руководства по chraod
warn $pwent->name."'s homedir is world-writable!\n"
if ($dirinfo->mode & 022 and (!$stat->mode & 01000));
}
endpwent();
Этот пример на вид несколько отличается от предыдущих, поскольку в нем используются два замысловатых модуля Тома Кристиансе-на (Tom Christiansen): User: :cweit и FiJe: :stai. Эти модули изменяют функции getpwcrH() и stat(), заставляя их возвращать значения, отличные от ранее упомянутых. Когда загружены модули Fi le: : stat, эти функции возвращают объекты вместо списков или скалярных значений. У каждого объекта есть метод, названный по имени поля, которое было бы возвращено в списочном контексте. Поэтому такой код:
$gid = (stat("filena(ne"))[5]:
можно переписать гораздо понятнее :
use File;:stat;
$stat = stat("filename"):
$gid = $stat->gid:
или даже так:
use File;:stat;
$gid = stat("filename")->gid;
Командный интерпретатор пользователя
Последнее поле классического файла паролей - это поле, соответствующее командному интерпретатору пользователя. Обычно это один из интерпретаторов (sh, csh, tcsh, ksh, zsh), но это может быть и путь к любой исполняемой программе или сценарию. Время от времени, некоторые ради шутки (но наполовину всерьез) устанавливают в качестве своего командного интерпретатора по умолчанию интерпретатор Perl. По крайней мере, в один интерпретатор (zsh) хотят всерьез встроить интерпретатор Perl, но этого пока еще не случилось. Тем не менее, были предприняты серьезные попытки создать командный интерпретатор Perl shell (http:/ /www.focusrese-arch.com/gregor/psh/), а также встроить Perl в редактор Emacs, который легко может заменить целую операционную систему (http:// john-edwin-tobey.org/perlmacs/1).
Бывают ситуации, когда необходимо указать в этом поле нечто отличное от стандартного командного интерпретатора. Например, если вы хотите создать учетную запись, работающую с системой меню, вы можете поместить в данное поле имя такой программы. В этом случае стоит принять некоторые меры предосторожности, чтобы пользователь, применяющий эту учетную запись, не получил бы доступ к командному интерпретатору, иначе не миновать бед. Часто встречаемая ошибка - включение в такое меню почтовой программы, которая позволяет запускать редактор или инструмент постраничного просмотра для чтения или редактирования почты. Оба эти средства могут иметь возможность выхода в интерпретатор.
Список доступных в системе стандартных командных интерпретаторов часто хранится в файле /etc/shells, видимо, для удобства демона FTP. Большинство FTP-демонов не позволят обычному пользователю подсоединиться к системе, если их командный интерпретатор, заданный в /etc/passwd (или сетевом файле паролей), не присутствует в /etc/shells. Вот пример на Perl, который докладывает об учетных записях с неподтвержденными командными интерпретаторами:
use User::pwert:
Sshells = "/etc/shells";
open (SHELLS,Sshells) or die "Невозможно окрыть;
while(<SHELLS>){
chomp:
$oksnell{$_}++;
}
close(SHELLS);
while($pwent = getpwent()){
warn $pwent->name." has a bad shell (".$pwent->shell.")!\n"
unless (exists $okshell{$pwent->shell});
}
endpwent();
Дополнительные поля в файлах паролей в BSD 4.4
При смене BSD (Berkeley Software Distribution) с версии 4.3 на 4.4 к классическому формату файла паролей были добавлены две характерные особенности: дополнительные поля и формат двоичных баз данных, используемых для хранения информации об учетных записях.
В BSD 4.4 в файле паролей между полями GID и GCOS появились новые поля. Первым было добавлено поле class. Оно позволяет системному администратору разбить все учетные записи системы на отдельные классы (например, для различных классов учетных записей могут существовать различные ограничения ресурсов, таких как время использования процессора). Кроме того, были добавлены поля change и expire, в которых хранятся данные о сроке продолжительности пароля и времени действия учетной записи. Подобные поля встретятся также и в формате следующего файла паролей в Unix.
При компиляции в операционной системе, поддерживающей эти дополнительные поля, Perl включает их содержимое в значение, возвращаемое функциями типа getpwent(). Это одна из причин, по которой стоит употреблять в программах getpwent(), а не разбирать файл паролей вручную при помощи sрlit ().
Формат двоичных баз данных в BSD 4.4
Вторая характерная черта BSD - использование баз данных, а не обычного текста для хранения информации о паролях. В BSD файлы паролей хранятся в формате DB - значительно улучшенной версии старых библиотек DBM (Database Management). Это изменение позволяет cиcтеме быстро обращаться к информации о паролях.
Программа pwd mkdb в качестве аргумента принимает имя текстового файла паролей, создает и переносит в нужное место два файла баз данных, а затем перемещает исходный текстовый файл в /etc/mas-ter.passwd. Две базы данных позволяют обеспечить механизм теневых паролей - они отличаются правами на чтение и содержимым поля, в котором хранится зашифрованный пароль. Более подробно мы поговорим об этом в следующем разделе.
Perl может напрямую работать с DB-файлами (операции с самим форматом встречаются в главе 9 «Журналы»), но обычно я не рекомендую напрямую редактировать базы данных, пока система используется. Дело тут в блокировке: необходимо убедиться, что другие программы не читают и не записывают данные в файл паролей в тот момент, когда вы собираетесь редактировать его как базу данных. Стандартные программы, подобные chpasswd, выполняют необходимую блокировку самостоятельно. Ловкий прием, заключающийся в использовании переменной EDITOR, который мы употребили при работе с квотами в главе 2 «Файловые системы», можно применить и при вызове chpasswd.
Теневые пароли
Не следует забывать, насколько важна защита содержимого поля GCOS, т. к. целым рядом различных механизмов эта информация доступна для всех. Другая, менее доступная, но довольно уязвимая информация - это список зашифрованных паролей всех пользователей системы. И хотя эти пароли зашифрованы, одно то, что они хранятся в файле, доступном всем для чтения, вносит изрядную степень риска. Некоторые части файла паролей должны быть доступны всем для чтения (например, связь между регистрационным именем и идентификатором пользователя), но не весь файл. Нет никакой необходимости выставлять на всеобщее обозрение список зашифрованных паролей, т. к. пользователи могут попытаться запустить программы для взлома паролей.
Одна из возможных альтернатив - перенести все пароли в специальный файл, читать который сможет только суперпользователь. Этот второй файл известен как файл «теневых паролей», т. к. в нем хранятся строки, затеняющие записи из обычного файла паролей.
Вот как все это работает: оригинальный файл паролей остается нетронутым, за одним небольшим исключением. Вместо зашифрованного пароля в это поле помещается специальный символ или символы, которые говорят о том, что используется механизм затенения паролей. Обычно это символ х, но в BSD используется *.
Я слышал, что существуют некоторые пакеты (для поддержки теневых паролей), вставляющие в это поле специальную строку символов, которая выглядит обычной. Если ваш файл паролей попадет в руки злоумышленника, то он потратит много времени, взламывая случайные строки, не имеющие ничего общего с настоящими паролями.
Большинство операционных систем используют файл теневых паролей для хранения дополнительной информации об учетной записи. Такой формат включает дополнительные поля, которые мы видели в BSD-файлах, и в них хранится информация об истечении срока действия учетной записи и информация о смене пароля.
В большинстве случаев обычные Perl-функции, подобные getcwenb(), могут работать с файлами теневых паролей. Если стандартные библиотеки С, входящие в состав операционной системы, делают то, что нужно., то Perl тоже будет делать все верно. Говоря «делать то, что нужно», я подразумеваю, что если ваши сценарии на Perl запускаются с подходящими привилегиями (с привилегиями суперпользователя), то эти функции будут возвращать зашифрованный пароль. В остальных случаях пароль этим функциям не доступен.
Значительно хуже, если вы захотите получить дополнительные поля из файла теневых паролей. Perl может и не вернуть их вам. Эрик Истабрукс (Eric Estabrooks) написал модуль Passwd: :Solar is, но он будет полезен только при работе в Solaris. Если эти поля имеют для вас принципиальное значение или вы хотите действовать наверняка, то, как ни грустно (это противоречит моим рекомендациям использовать getpwent()), но часто проще открыть файл shadow и получить нужные значения вручную.
когда мы выяснили, как образована
Теперь, когда мы выяснили, как образована информация о пользователях в Unix-системах, мы можем посмотреть, как это делается в NT/2000. Большая часть этой информации сходна с уже рассмотренной, поэтому следует обратить внимание на различия между двумя операционными системами.
Хранение и доступ к информации о пользователях в Windows NT/2000
NT/2000 хранит постоянную информацию о пользователях в базе данных SAM
(Security Accounts Manager, диспетчер учетных записей в системе защиты). База данных SAM - это часть реестра NT/2000, находящаяся в
%SYSTEMROOT%/system32/config. Файлы, входящие в состав реестра, хранятся в двоичном формате, следовательно, обычные функции для работы с текстом в Perl нельзя применять для чтения или внесения изменений в эту базу данных. Теоретически, если NT/2000 не запущена, можно использовать операторы над двоичными данными (раск() и unpack()) для работы с SAM, но такой способ безумен и мучителен.
К счастью, существуют более удачные методы доступа к этой информации и работы с ней в Perl.
Один из способов - вызвать внешнюю программу, которая обесепечит ваше взаимодействие с операционной системой. На каждой машине с NT/2000 есть команда net, она позволяет добавлять, удалять и просматривать данные о пользователях, net довольно странная и ограниченная команда и, вероятно, к ее использованию стоит прибегать в крайнем случае.
Вот так, например, команда net выполняется на машине с двумя учетными записями:
C:\>net users
User accounts for
\\HOTDIGGITYDOG
------------------------------------------------------
Administrator Guest
The command completed successfully.
При необходимости, вывод этой команды было бы просто разобрать из Perl. Помимо net существуют и другие коммерческие пакеты, в состав которых входят программы, запускаемые из командной строки и выполняющие те же действия.
Другой подход - использовать модуль Wi п32: : Net Admin (входящий в состав дистрибутива ActiveState Perl) или один из модулей, созданных для расширения функциональности Win32:: NetArMn. В их число входят модули Win32: :AdminMisc Дэвида Рота (David Roth, модуль находится на http://www.roth.net) и Win32: :UserAdmiri (описанный Эшли Мэггитом (Ashley Meggitt) и Тимоти Ритчи (Timothy Ritchey) в книге «Windows NT User Administration» (Windows NT: Администрирование пользователей), модуль можно найти на ftp://ftp.oreilly.com/pub/ examples/windows/winuser/).
Для выполнения большинства операций с пользователями я предпочитаю модуль Win32: .-AdrcinMisc, поскольку он предлагает множество инструментов системного администрирования, кроме того, Рот активно поддерживает его в нескольких форумах. И хотя доступная в Сети документация по этому модулю очень хороша, лучшая документация -это книга самого автора «Win32 Perl Programming: The Standard Extensions» (Программирование на Perl для Win32: стандартные расширения) (Macmillan Technical Publishing). Такую книгу всегда полезно иметь под рукой, если вы собираетесь писать на Perl программы для Win32.
Приведу пример, перечисляющий пользователей на локальной машине, а также некоторые сведения об этих пользователях. Выводимые в примере строки похожи на строки из файла
/etc/passwd в Unix:
use Win32::AdminMisc;
и получаем список всех локальных пользователей
Win32::AdminMisc::GetUsers('','',\@users) or
die "Невозможно получить список пользователей: $!\п";
П получаем их атрибуты и выводим их foreach $user (@users){
Win32::AdminMisc::UserGetMiscAttributes('',$user,\%att ribs)
or warn " Невозможно получить атрибуты: $!\п"; print join(":",$user,
' * '
$attribs{USER_USER_ID},
$attribs{USER_PRIMARY_GROUP_ID},
$attribs{USER_COMMENT},
$attribs{USER_FULL_NAME},
$attribs{USER_HOME_DIR_DRIVE}.
$attribs{USER_HOME DIR}, "),"\n";
}
Наконец, вы можете использовать модуль Win32: : OLE для доступа к интерфейсам активных служб каталогов (ADSI, Active Directory Service Interfaces). Данная служба встроена в Windows 2000 и ее можно установить на Windows NT 4.0. Эта тема и соответствующие примеры будут подробно рассмотрены в главе 6 «Службы каталогов».
Другие примеры программ на Perl для работы с пользователями в NT/2000 встретятся позже, а пока вернемся к обсуждению различий между пользователями в Unix и Windows NT/2000.
Идентификаторы пользователей в NT/2000
Идентификаторы пользователей в NT/2000 создаются не простыми смертными и их нельзя использовать повторно. В отличие от Unix, где мы просто берем следующий свободный идентификатор пользователя, в Windows NT/2000 операционная система уникальным образом генерирует эквивалентный идентификатор каждый раз при создании пользователя. Уникальный идентификатор пользователя (который в NT/2000 называется относительным идентификатором или RID, Relative ID) объединяется с идентификатором машины и домена, и вместе они образуют длинный идентификационный номер - идентификатор безопасности (SID, Security ID), который используется в качестве1 идентификатора пользователя (UID). Например, RID равный FOG, является частью длинного идентификатора SID, который выглядит так:
S-1-5-21-2046255566-1111630368-2110791508-500
RID - это то число, которое мы получаем в результате вызова функции UserGetMiscAttributes() в последнем примере. Вот так должен выглядеть код для получения идентификатора RID конкретного пользователя:
use Win32::AdminMisc;
Win32: :AdrninMisc: : UserGetMiscAttriuutest ' '.
$user \%attribs): print $attnbs{USEFLUSER_ID}, "\n";
Вы не сможете (каким бы то ни было нормальным способом) заново создать пользователя после того, как он был удален. И даже если вы создадите пользователя с тем же самым именем, его идентификатор безопасности (SID) все равно будет отличаться. У нового пользователя не будет доступа к файлам и ресурсам его предшественника.
По этой причине в некоторых книгах по NT рекомендуется переименовывать учетные записи, которые наследуются от других людей. Если к новому работнику должны перейти все файлы и привилегии уходящего работника, следует скорее переименовать существующую учетную запись, чтобы сохранить SID, чем создавать новую учетную запись, переписывать все файлы и затем удалять старую. Лично я нахожу такой способ передачи учетных записей несколько грубоватым, поскольку в этом случае новый работник наследует все поврежденные и бесполезные настройки реестра от своего предшественника. Но это самый удобный способ, а иногда это важно.
Частично эта рекомендация связана с мучениями при передаче права собственности на файлы. В Unix привилегированный пользователь может сказать: «Изменить права владения для всех этих файлов так, чтобы они перешли к новому пользователю». В NT, однако, право на владение нельзя дать, его можно только получить. К счастью, существует два способа обойти это ограничение и считать, что мы используем семантику Unix. В Perl мы можем:
Вызвать исполняемый файл, включая:
Программу chown либо из пакета Microsoft NT Resource (коммерческий продукт, упомянутый далее), либо из дистрибутива Cygwin с
http://www.cygnus.com (бесплатный).
Программу setowner, входящую в число утилит NTSEC, продаваемых Pedestal Software на http://www.pedestalsoftware.com. Я предпочитаю ее, т. к. программа отличается гибкостью и при этом требует наименьших затрат.
Использовать модуль Win32: : Perms, написанный Дэвидом Ротом (David Roth), который можно найти на http://www.oth.net/perl/perms. Вот простой пример, изменяющий владельца каталога и его содержимое, включая подкаталоги:
$acl = new Win3?: Pernis();
$acl->0wner($NewAccountName):
Sresult = $aci->SetReci;rse($dir);
$acl->Close():
Пароли в NT/2000
Алгоритмы, применяемые для шифрования паролей, ограничивающих доступ к владениям пользователей в NT/2000 и Unix, криптографически несовместимы. Вы не можете передавать зашифрованные пароли из одной операционной системы в другую, что бывает необходимо для смены пароля или создания учетных записей. В результате, два набора паролей приходится использовать и/или хранить синхронно. Это различие - просто проклятье всех системных администраторов, вынужденных управлять смешанным окружением Unix-NT/2000. Некоторые администраторы обходят эти проблемы, используя специальные модули авторизации - как коммерческие, так и прочие.
Если вы не применяете специальные механизмы авторизации, то единственное, что вы можете сделать как программист на Perl, - создать систему, благодаря которой пользователи смогут представлять свои пароли в виде обычного текста. Такие пароли позволяют выполнять связанные с ними операции (изменение пароля и пр.), различные в каждой операционной системе.
Группы в NT
До сих пор при обсуждении идентификации пользователя я не упоминал о различиях между хранением этой информации на локальной машине и средствами какой-либо сетевой службы, например NIS. Для той информации, о которой шла речь, не было существенно, используется ли она на одной системе, на всех системах в сети или в рабочей группе. Чтобы обоснованно говорить о группах пользователей в NT/2000 и их связи с Perl, мы должны, к сожалению, отойти от этого соглашения. Мы остановимся на группах в Windows NT 4.0. В Windows 2000 был добавлен еще один уровень сложности, поэтому информацию о группах в Windows 2000 я вынес во врезку «Изменения групп в Windows 2000», которую вы найдете в этой главе.
В NT информация о пользователях может храниться в одном из двух мест: в SAM на конкретной машине или в SAM на контроллере домена. Именно здесь проявляется различие между локальным пользователем, который может входить в систему и работать только на одной машине, и пользователем домена, который может регистрироваться на любой из машин в домене.
Группы в NT также бывают двух типов: глобальные и локальные. Разница между ними заключается не совсем в том, чего можно было бы ожидать из названия. Неверно, что первая состоит из пользователей домена, а вторая из локальных пользователей. Так же неверно и то, что один тип пользователей имеет доступ только на одну машину, в то время как другой пользуется всей сетью, как могли ожидать лица, знакомые с Unix. Частично верно и одно определение, и другое, но давайте рассмотрим все подробно.
Если начать с рассмотрения целей, лежащих за названием, и механизмом их реализации, станет немного яснее. Вот чего мы пытаемся достичь:
Учетные записи пользователей всего домена должны обслуживаться централизованно. Администраторы должны иметь возможность определить произвольное подмножество прав и привилегий пользователей, которые можно присвоить всей группе одновременно.
При желании, все машины домена должны иметь возможность использовать преимущества такого централизованного управления. Администратор отдельной машины по-прежнему должен иметь возможность создавать пользователей, «живущих» только на этой машине.
Администратор каждой машины должен иметь возможность решать, каким пользователям разрешать доступ к этой машине. Администратор должен иметь возможность делать это, используя группы, существующие в домене, а не задавать имена пользователей вручную.
Члены этих групп и локальные пользователи должны иметь возможность считаться равными с точки зрения администратора (речь идет о правах и прочем).
Глобальные и локальные группы позволяют нам добиться всего вышеперечисленного. В двух предложениях это можно объяснить так: в глобальные группы входят только пользователи домена. В локальные группы входят локальные пользователи, а также в них входят/импортируются пользователи глобальных групп.
Вот простой пример для объяснения, как все это работает. Скажем, у вас есть домен NT для кафедры в университете, в котором уже созданы пользователи домена - студенты, преподаватели и служащие. Когда появляется новый исследовательский проект под названием Omphalos-kepsis, системные администраторы создают новую глобальную группу Global-Omph People. В данную глобальную группу входят все пользователи домена, занятые в этом проекте. Когда студенты и персонал присоединяются к проекту или выходят из него, они, соответственно, добавляются или удаляются из этой группы.
Для этого проекта используется отдельный компьютерный класс. На машинах такого класса созданы гостевые учетные записи для нескольких представителей факультета с других кафедр (они не являются пользователями домена). Системный администратор этого класса делает следующее (разумеется, на Perl), чтобы запретить использовать компьютеры тем, кто не занят в этом проекте:
Создает на каждой машине локальную группу Local-Autnorized Omphies.
Добавляет в эту локальную группу локальные гостевые учетные записи.
Добавляет глобальную группу Global-Onph People в указанную локальную группу.
Добавляет право (права пользователей мы обсудим в следующем разделе) Log on Locally (регистрироваться локально) локальной группе Local-Authorized Omphies.
Удаляет право Log on Locally для всех остальных неавторизованных групп.
В результате только авторизованные локальные пользователи и пользователи из авторизованных глобальных групп могут регистрироваться на машинах этого класса. Как только в группу Global-Omph People добавляется новый пользователь, он автоматически получает право регистрироваться на этих машинах без каких-либо изменений учета. Как только вы освоитесь с понятием локальных/глобальных групп, такая схема покажется вам удобной.
Подобная схема была бы совсем удобной, если бы не усложняла программирование на Perl. Во всех упомянутых модулях существуют отдельные функции для локальных и глобальных групп. Например, в Win32: :NetAdmin имеем:
GroupCreate() GroupDelete() GroupGetAttributes() GroupSetAttributes() GroupAddUsers() GroupDeleteUsers( ) GroupIsMember( ) GroupGetMembers() |
LocalGroupCreate() LocalGroupDelete( ) LocalGroupGetAttributes( ) LocalGroupSetAttributes( ) LocalGroupAddlisers( ) LocalGroupDeleteUsers( ) LocalGrouoIsMember( ) LocalG-uijpGetMeTibers() |
Отличия групп в Windows 2000
Практически все, что мы говорили о локальных и глобальных группах в NT, также относится и к Windows 2000, но существует несколько характерных особенностей, о которых необходимо упомянуть:
В Windows 2000 используются активные каталоги (Active Directory, более подробную информацию о них можно найти в главе 6) для хранения информации о пользователях. Это означает, что информация о глобальных группах теперь хранится в активном каталоге на контроллере домена, а не в его SAM.
Локальные группы теперь называются локальными группами домена.
Была добавлена третья,
пространственная (scope) группа. Помимо глобальных и локальных групп домена в Windows 2000 добавились
универсальные группы. Универсальные группы, по существу, разрывают границы домена. В них могут входить учетные записи, глобальные группы и универсальные группы из любого места каталога. В локальные группы домена могут входить как глобальные группы, так и универсальные группы.
На момент написания этой книги стандартные модули Perl для администрирования
учетных записей еще не учитывали таких изменений. Эти модули можно по-прежнему использовать, поскольку интерфейсы NT4 SAM пока еще действуют, но они не смогут применять новые возможности. Поэтому данная врезка единственное место, .где мы упоминаем об этих различиях в Windows 2000. Подробную информацию вам придется искать в описании интерфейсов служб активных каталогов (Active Directory Service Interfaces, ADSI), о которых речь пойдет в главе 6.
Таким образом, не исключено, что в программах для выполнения одной и той же операции понадобится употребить две функции. Например, если нужно получить список всех групп, в которые может входить пользователь, придется вызвать две функции - одну для локальных, а другую для глобальных групп. Приведенные функции характеризуются своими названиями. Детальное описание можно найти в документации и книге Рота.
Вот короткий совет из книги Рота: чтобы получить список локальных групп, ваша программа должна выполняться с привилегиями администратора, но имена глобальных групп должны быть доступны всем пользователям.
Права пользователей в NT/2000
Последнее различие между информацией о пользователях в Unix и NT/2000, о котором мы поговорим, - это понятие «пользовательских прав». В Unix действия, предпринимаемые пользователем, ограничиваются как правами доступа к файлам, так и различиями между суперпользователем и остальными пользователями. В NT/2000 права реализованы гораздо сложнее. Пользователи (и группы) могут быть наделены особой силой, которая становится частью информации о пользователях. Например, если предоставить обычному пользователю право Change the System Time (Изменение системного времени), то он сможет изменять настройки системных часов.
Некоторые считают, что такая концепция прав пользователей сбивает с толку, поскольку они предпринимали попытки прибегнуть к помощи отвратительного диалогового окна User Rights Policy (Политика прав пользователей) из NT 4.0 в приложениях User Manager (Диспетчер пользователей) или User Manager for Domains (Диспетчер пользователей для доменов). В этом диалоговом окне информация представлена виде, прямо противоположном тому, в котором большинство пользователей ожидают ее там увидеть. Она содержит перечень возможных прав пользователей и предлагает добавить группы или пользователей к списку тех, у кого такие права уже есть. Вот как выглядит это диалоговое окно (рис. 3.1) пользовательских прав в действии.
Было бы лучше, если бы права пользователей разрешалось добавлять и удалять, а не наоборот. В действительности, именно так мы и будем поступать, используя Perl.
Один из возможных подходов - вызвать программу ntrights.exe из Microsoft NT Resource Kit. Если вы об этом никогда не слышали, обязательно прочитайте следующую врезку.
Работать с ntrights.exe очень легко; достаточно вызывать эту программу из Perl, как любую другую (т. е., применяя обратные кавычки или функцию system()). В этом случае мы обратимся к ntrights.exe при помощи такой командной строки:
чтобы предоставить право пользователю или группе (на машине rnachi-пепате,
имя которой указывать не обязательно). Чтобы отнять право, необходимо применить такой синтаксис:
С:\>ntrights.exe -г <ngtit name> +u <user or group narr,e> [-m \\vachintnaive]
Пользователи Unix знакомы с употреблением символов + и - (как в chmod), в данном случае для ключа -г, чтобы предоставить или лишить привилегий. Список допустимых имен (например, SetSys::emti-mePrivilege для разрешения устанавливать системное время) можно найти в документации Microsoft NT Resource Kit no команде ntrights
Второй подход, с использованием только Perl, связан с применением модуля Win32: : Lanman, написанного Йенсом Хелбергом (Jens Helberg), который можно найти либо на ftp://ftp.roth.net/pub/ntperl/Others/ Lanman/, либо на
http://jenda.krynicky.cz. Начнем с того, что рассмотрим процесс получения прав для учетной записи. Этот процесс состоит из нескольких шагов, поэтому рассмотрим его подробно, шаг за шагом.
Сначала необходимо загрузить модуль:
use Win32::Lanman;
Затем следует получить идентификатор (SID) для учетной записи, с которой надо работать. В следующем примере мы получим SID для учетной записи Guest:
unless(Win32: : Lanman: : LsaLookupNames($server, [ 'Guest' ]. \@info)
{ die "Невозможно найти SID':
".Win32::Lanman::Get LastError()."\n";
}
@info - это массив ссылок на анонимные хэши, каждый элемент которого соответствует отдельной учетной записи (в нашем случае это один-единственный элемент для учетной записи Guest). В каждом хэ-ше есть такие ключи: domain, do;nainsid, relativeid, sid и use. На следующем шаге нас будет интересовать только ключ sid. Теперь мы можем узнать о правах этой учетной записи:
unless (Win32: : Lanman :: LsaEnurierateAccountRights($server.
${$info[0]}{sid}, \@rights)){ die "Невозможно узнать права:
"32: :Lanmai .GetL.asrError() "\r"\
Microsoft Windows NT/ Windows 2000 Resource Kits
«У вас должен быть установлен NT 4.0 Server и/или Workstation Resource Kit» - в этом, обычно, единодушны и серьезные администраторы NT, и средства информации. Microsoft Press опубликовал два больших тома, каждый из которых полон жизненно необходимой информации об одной из версий операционной системы NT/2000. Ценность этих книг заключается не столько в сведениях, сколько в компакт-дисках, распространяемых вместе с книгами. На компакт-дисках есть множество важных утилит для администрирования NT/2000. Утилиты, поставляемые с книгой по NT/2000 Server, содержат и утилиты, входящие в компакт-диск для версии NT Workstation/Windows 2000 Professional. Если вам придется выбирать одну из книг, предпочтите издание, посвященное NT/2000 Server.
Многие из этих утилит распространяются группой разработчиков NT/2000, написавших собственные программы, поскольку они нигде не смогли найти нужные им инструменты. Например, в состав этих утилит входят программы для добавления пользователей, изменения информации о безопасности файловой системы, отображения установленных драйверов принтеров, работы с профилями, помощи с отладкой служб доменов и обозревателя сети и т. д.
Инструменты из пакета дополнительных программ поставляются «как есть» (as is), иными словами, они практически не поддерживаются. Такая политика «неподдержки» может показаться грубой, но она преследует важную цель - дать возможность Microsoft предоставить администраторам множество полезных программ и не заставлять их платить непомерно много за поддержку. В программах из этого пакета есть некоторые мелкие ошибки, но, в целом, они работают замечательно. Обновления, исправляющие ошибки в некоторых утилитах, публикуются на веб-сайте Microsoft.
Массив ©rights теперь содержит набор строк, описывающих все права учетной записи Guest.
Узнать, чему соответствует API-имя ( Application Program Interface, интерфейс прикладного программирования) того или иного права пользователя, может оказаться непростой задачей. Самый легкий способ выяснить, каким правам какие имена соответствуют, - ознакомиться с документацией SDK (Software Developement Kit, набор инструментальных средств разработки программного обеспечения), которая находится на http://msdn.microsoft.com. Нужную документацию отыскать легко, потому что Хелберг сохранил имена стандартных
функций SDK для функций в Perl. Чтобы найти имена доступных прав, достаточно поискать в MSDN (Microsoft's Developer Network) «LsaEnumerateAccountRights», и мы быстро их отыщем.
Подобная информация полезна и для изменения прав пользователей. Например, если мы хотим разрешить пользователю Guest выключать (останавливать) систему, мы можем применить следующее:
use Win32::Lanman;
unless (Win32::Lanman::LsaLookupNames($server ['Guest'],
\@info)) {
die " Невозможно найти SID; ".Win32::Lanman::GetLastError()."\n"
}
unless (Win32::Lanman::LsaAddAccountRights($server,
${$info[0]}{sid}, [&SE^SHUTDOWN_NAME])) {
die " Невозможно изменить права: ". Win32::Lanman::GetLastError()."\n"
}
На этот раз мы нашли право SE_SHUTDOWN_NAME в документации по SDK и применили подпрограмму &SE__SHUTDOWN_NAME (определенную в Win32: : Lanman), возвращающую значение этой константы SDK.
Win32: : Lanman: : LsaRemoveAccountRights() - это функция. Она используется для лишения прав и принимает аргументы, схожие с теми, которые применяет функция для добавления прав.
Перед тем как перейти к другим темам, необходимо упомянуть, что в Win32:: Lanman входит также и функция, действующая аналогично неудачному интерфейсу диспетчера пользователей, о котором мы говорили раньше. Вместо того чтобы сопоставлять пользователей с правами, мы можем сопоставлять права с пользователями. Применяя функцию Win32: : Lanman: : LsaEnumerateAccountsWithUserRight(), мы можем получить список идентификаторов (SID), у которых есть определенные поля. Иногда такое знание может сослужить добрую службу.
Рекомендуемая дополнительная литература Файлы паролей в Unix
http://www.freebsd.org/cgi/man.cgi.
Здесь можно получить доступ в оnline-режиме к страницам руководств для *BSD и других вариантов Unix. Это очень удобный способ сравнить форматы файлов и команды системного администрирования (useradd и пр.) для нескольких операционных систем.
«Practical Unix & Internet Security»,
(2nd Edition), Simson Garfinkel, Gene Spafford (O'Reilly, 1999). Отличный источник информации о файлах паролей.
Администрирование пользователей в NT
http://Jenda.Krynicky.cz
- еще один сайт с полезными модулями в Win32 для администрирования пользователей.
http://windows.microsoft.com/windows2000/en/server/help/ -
справка Windows 2000. (Переходите к разделу Active Directory-»Concepts-) Understanding Active Directory-> Understanding Groups). Это хороший обзор новых механизмов групп в Windows 2000.
http://www.actiuestate.com/support /mailing_lists.htm.
Здесь можно найти списки рассылки Perl-Win32-Admin и Perl-Win32-Users. Оба списка и их архивы представляют собой просто бесценный источник информации для программистов Win32.
«Win32 Perl Programming: The Standard Extensions»,
Dave Roth (Mac-millan Technical Publishing, 1999) в настоящее время лучший источник по программированию модулей для Win32 Perl.
«Windows NT User Administration»,
Ashley J. Meggitt, Timothy D. Ritchey (O'Reilly, 1997).
http://www.mspress.com.
Издатели Microsoft NT Resource Kit. Они также предлагают возможность подписки для получения доступа к самым последним утилитам из RK.
http://www.roth.net.
Домашняя страница для Win32: :AdminMisc, Win32: :Perms и других модулей для Win32, используемых для администрирования пользователей.
XML
За последние два года появилось огромное количество материала по XML. Приведенные ниже источники информации - это лучшее, что, на мой взгляд, существует для тех, кто ничего не знает о XML. Когда я писал эту книгу, изданий по XML для Perl еще не было, но мне известно, что несколько подобных проектов уже существует.
http://msdn.microsoft.com/xml
и http://www.ibm.com/developer/xml -оба содержат обилие информации. И Microsoft, и IBM очень серьезно настроены по отношению к XML.
http://www.activestate.com/support/mailing__lists.htm -
содержит список рассылки Perl-XML. Он ( и его архивы) один из лучших источников данной информации.
http://www.w3.org/TR/1998/REC-xml-19980210.
Спецификация XML1.0. Любой, кто делал что-то на XML, наверняка читал спецификацию. Если вам нужно что-либо более подробное, чем справочник, я советую почитать версии с комментариями.
http://www.xml.com.
Хороший источник статей и ссылок, посвященных XML. Кроме того, здесь можно найти отличную версию спецификации с комментариями Тима Брэя (Tim Bray), одного из ее авторов.
«XML: The Annotated Specification»,
Bob DuCharme (Prentice Hall, 1998). Еще одна отличная версия спецификации с комментариями и примерами кода на XML.
«XML Pocket Reference»,
Robert Eckstein (O'Reilly, 1999). Краткое, но на удивление полное введение в XML для нетерпеливых.
Прочее
http://www.mcs.anl.gov/~evard.
Домашняя страница Реми Эварда (Re-my Evard), Использование нескольких баз данных для автоматического генерирования конфигурационных файлов - это лучший прием, который показан в нескольких местах моей книги; спасибо Эварду за идею этого метода. И хотя сейчас подобный прием применяется на многих сайтах, я впервые столкнулся с ним при знакомстве со средой Tenwen, которую он создал (как описано в статье, ссылка на которую есть с домашней страницы Эварда). Чтобы ознакомиться с работой этого метода, загляните в раздел «Implemented the Hosts Database».
http://www.rpi.edu/~finkej/.
Содержит несколько статей Иона Финки (Jon Finke) по использованию реляционных баз данных в системном администрировании.
Создание системы учетных записей для работы с пользователями
Теперь, в достаточной мере познакомившись с информацией о пользователях, мы можем перейти к вопросу администрирования учетных записей. Вместо того чтобы просто привести список подпрограмм и функций Perl, необходимых для добавления и удаления пользователей, я хочу рассмотреть предмет разговора на другом уровне, рассказывая об этих операциях в широком контексте. В оставшейся части этой главы мы попытаемся написать скелет системы учетных записей, которая работает с пользователями как в NT, так и в Unix.
Наша система учетных записей будет состоять из четырех частей: пользовательского интерфейса, хранилища данных, сценариев обработки (в Microsoft это назвали бы «бизнес-логикой») и низкоуровневых библиотечных вызовов. В организации процесса они работают все вместе.
Запросы поступают в систему через пользовательский интерфейс и помещаются в файл «очереди добавления учетных записей» для обработки. Мы будем называть ее просто «очередью добавления». Сценарии обработки читают эту очередь, создают необходимые учетные записи и сохраняют информацию о созданных учетных записях в отдельной базе данных. Этот процесс отвечает за добавление пользователей в систему.
Процесс удаления пользователя подобен только что описанному. Пользовательский интерфейс создает «очереди удаления». Второй сценарий обработки читает эту очередь, удаляет пользователей из системы и обновляет центральную базу данных.
Данные операции разделены по концептуально различным группам, благодаря чему
достигается максимальная гибкость при необходимости что-либо изменить. Например, желание сменить систему баз данных, потребует только других вызовов низкоуровневой библиотеки. Точно так же, для включения дополнительных шагов в процесс добавления пользователей (к примеру, провести сравнение с базой данных отдела кадров) придется изменить только сценарий обработки. Нам-
нем с рассмотрения первого компонента - пользовательского интерфейса, применяемого для создания первоначальной очереди учетных записей. Мы будем использовать простой текстовый интерфейс для запроса параметров учетной записи, поскольку строим только костяк системы:
sub Collectlnformation{
П список полей приводится только для наглядности. На самом
# деле его надо хранить в центральном конфигурационном
в файле
my ^fields = qw{login fullname id type password};
my Krecord;
foreach my $field (©fields){
print "Please enter $field: ";
chomp($record{$field} = <STDIN>);
}
$record{status}="to_be_created"; return \%record;
}
В этой подпрограмме создается список, состоящий из различных полей учетной записи пользователя. Как уже говорилось в комментариях, этот список упоминается в коде программы только для краткости. Хорошим стилем проектирования программного обеспечения было бы чтение списка имен полей из дополнительного конфигурационного файла.
После того как список создан, подпрограмма рассматривает его в цикле и запрашивает значение для каждого поля. Каждое значение затем сохраняется в хэше. После получения ответов на все вопросы ссылка на этот хэш возвращается для последующей обработки. Наш следующий шаг - записать информацию в очередь добавления. Перед тем как посмотреть на этот код, мы должны рассказать о хранилище данных и форматах данных, используемых в нашей системе учетных записей.
База данных
Центральная часть любой системы учетных записей - это база данных. Некоторые администраторы используют только файл /etc/ pass-wd или базу данных SAM для хранения записей о пользователях системы, но такое решение часто оказывается недальновидным. Помимо информации, о которой мы уже говорили, в отдельной базе данных можно хранить метаданные о каждой учетной записи: например, дату создания учетной записи, срок ее действия, номера телефонов пользователей и прочие сведения. Когда появляется такая база данных, ее можно применять не только для работы с учетными записями. Она годится для создания списков рассылки, служб LDAP и индексации веб-страниц пользователей.
Почему настоящие системные администраторы создают системы учетных записей
Системные администраторы делятся на две категории: ремесленники и архитекторы. Ремесленники большую часть своего времени проводят в не посредственном контакте с подробностями внутреннего устройства ОС. Они знают множество тайн об аппаратном и программном обеспечении, которое они администрируют. Если что-то идет не так, как надо, они знают, какую использовать команду, файл, или какой «гаечный ключ» нужно применить. Талантливые ремесленники могут поразить вас способностью определить и исправить неполадки, находясь даже в соседней комнате от «проблемной» машины.
Архитекторы же тратят время, осматривая компьютерные пространства с высоты. Они мыслят более абстрактно, решая, как сформировать более сложные системы из отдельных частей. Архитекторы озабочены вопросами масштабируемости, расширяемости и повторного использования.
Администраторы обоих типов вносят важный вклад в системное администрирование. Я больше всего уважаю системных администраторов, которые могут быть ремесленниками, но при этом предпочитают действовать как архитекторы. Они решают проблему, а потом определяют, какие изменения в системе можно сделать, чтобы избежать повторения ошибки в дальнейшем. Они думают о том, как даже маленькие усилия с их стороны могут послужить для дальнейшего выигрыша.
Отлично действующее компьютерное окружение требует, чтобы архитекторы работали с ремесленниками в тесном взаимодействии. Ремесленники больше всего полезны при работе в рамках, созданных архитекторами. В автомобильном мире ремесленники нужны для сборки и ремонта машин. Но ремесленники расчитывают на то, что проектировщики машин разрабатывают трудно ломаемые и быстро ремонтируемые автомобили. Чтобы хорошо выполнять свою работу, им нужна инфраструктура, напоминающая сборочный цех, инструкция по эксплуатации и канал поставок запасных частей. Если архитектор хорошо выполняет свою работу, работа ремесленника становится проще.
Какое отношение это имеет к предмету нашего обсуждения? Что ж, вероятно, ремесленники будут применять имеющиеся в операционной системе инструменты для работы с пользователями. Они даже могут пойти дальше и написать небольшие сценарии, упрощающие такие задачи, как добавление пользователей, Архитектор, посмотрев на эту проблему, тут же начнет создавать
систему ведения учетных записей. Архитектор задумается над такими вопросами:
Природа повторяющихся действий при работе с пользователями и способы, позволяющие максимально автоматизировать данный процесс.
Тип информации, собираемой системой ведения учетных записей, и условия, при которых правильно созданная система может послужить основой для других действий. Например, как службу каталогов LDAP (Lightweight Directory Access Protocol) и инструменты для автоматического создания веб-страниц можно добавить к такой системе.
Защита данных в системе учетных записей (т. е. безопасность).
Создание системы, которая масштабируется при увеличении числа пользователей.
Создание системы, которую можно использовать и на других машинах.
Как другие системные администраторы решают такие проблемы.
Упоминание о создании отдельной базы данных заставляет некоторых нервничать. Они думают так: «Теперь мне нужно покупать действительно дорогую коммерческую базу данных, отдельный компьютер, на котором она будет работать, и нанимать администратора баз данных». Если у вас в системе тысячи или десятки тысяч учетных записей, с которыми необходимо работать, - да, вам понадобится все это (хотя можно обойтись и некоммерческими базами данных, такими как Postgres и MySQL). В этом случае переходите к главе 7 «Администрирование баз данных SQL», чтобы подробно узнать о работе с подобными базами данных в Perl.
Но когда в этой главе я говорю база данных, то употребляю этот термин в самом широком смысле слова. Плоские файлы вполне подойдут в нашем случае. Пользователи Windows даже могут работать с файлами баз данных Access (например database.mdb). В целях переносимости в этом разделе для различных создаваемых компонентов мы будем использовать простые текстовые базы данных. Но чтобы это было более интересным, базы данных будут в формате XML. Если вы никогда раньше не имели дела с XML, пожалуйста, потратьте немного времени и ознакомьтесь с приложением С «Восьмиминутное руководство по XML».
Почему XML? У XML есть несколько свойств, которые делают его хорошим выбором
для подобных файлов и других конфигурационных файлов системного администрирования:
XML - это текстовый формат, следовательно, для работы с ним мы можем использовать наши обычные Perl-приемы, чтобы легко г ним работать.
XML очень понятен и практически самодокументирован. Разбирая файл, разделенный определенными символами, такой как /etc passwd, не всегда просто определить, какому полю соответствует какая часть строки. В XML этой проблемы нет, поскольку каждое поле можно окружить очевидным тегом.
Располагая правильным анализатором, XML может являться также и самопроверяющим. Если применять анализатор, проверяющий синтаксис, то будет очень просто найти ошибки в формате, т. к. этот файл не будет верно разобран в соответствии с определением типа документа (DTD). Модули, которые мы будем применять в этой главе, основаны на анализаторе, не проверяющем синтаксис, но сейчас проводится важная работа по добавлению проверки синтаксиса. Один из шагов в этом направлении - модуль XML: :Checker, являющийся частью libxml-enno
Энно Дерксена (Enno Derksen). Анализатор, даже не проверяющий синтаксис, все-таки способен найти много ошибок, если он проверяет формат документа.
XML достаточно гибок для описания практически любой текстовой информации. Эта гибкость означает, что вы можете применять одну библиотеку анализатора для всех данных, а не писать новый анализатор для каждого нового формата.
Мы будем использовать текстовые файлы в XML-формате для основного файла, в котором хранятся учетные записи, и для очереди добавления/удаления.
И в этом случае вы увидите, что правило TMTOWTDI по-прежнему действует. Для каждой операции с XML, которая нам понадобится, мы рассмотрим или, по крайней мере, упомянем несколько способов ее выполнения. Обычно, собирая подобную систему, лучше ограничить число реализованных опций, а действуя таким образом, вы сможете понять, какие возможности программирования существуют при работе с XML из Perl.
Создание XML-файла из Perl
Давайте вернемся к событиям, о которых мы говорили в разделе «Права пользователей в NT/2000». Тогда речь шла о том, что необходимо записать информацию об учетной записи, получаемую посредством функции Collectlnformation(), в файл очереди. Но мы так и не видели примеров программы, выполняющей эту задачу. Давайте посмотрим, как записывается этот файл в формате XML.
Проще всего создать XML-файл при помощи простых операторов, но мы поступим лучше. Модули ХМl : Бенджамина Холзмана (Benjamin Holzman) и XML: Дэвида Меггинеона (David Megginson) могут упростить этот процесс и сделать его менее подверженным ошибкам. Они могут обработать такие детали, как соответствие открывающих/закрывающих тегов, а также позаботятся об экранировании специальных символов (<, >, & и т. д.). Вот пример программы, применяемой в нашей системе учетных записей для создания кода XML при помощи модуля XML: :Writer:
sub AppendAccountXML {
получаем полный путь к файлу
my $filename = shift;
ft получаем ссылку на анонимный хэш записи
ту Irecord = shift;
и XML::Writer использует объекты IO::File для управления и выводом use 10::File;
# дописываем в этот файл $fh = new 10: :File("»$filename") or die "Unable tt append to file:$!\n";
# инициализируем модуль XML::Writer и говорим ему
# записывать данные в файловый дескриптор
$fh use XML;;Writer; my $w = new XML::Writer(OUTPUT => $fh);
# записываем открывающий тег для каждой записи <account> $w->startTag("account");
# записываем открывающие/закрывающие внутренние теги и
# данные в <account>
foreach my $field (keys %{$record}){
print $fh "\n\t";
$w->startTag($field);
$w->characters($$record{$field});
$w->endTag;
}
print $fh "\n";
# записываем закрывающий тег для каждой записи <account>
$w->endTag;
$w->end;
$fh->close();
}
Теперь можно использовать всего лишь одну строчку, чтобы получить данные и записать их в файл очереди:
&AppondAcco:jntXML($addqueue. ^Collect In formation);
Вот что получается в результате работы этой подпрограммы:
<account>
<login>bobf</]ogin>
<fullname>Boh Fate'/fiil lr.ame>
<id>24-9057</iri>
<type>staff</type>
<password>passwora</password>
<status>to_be_created</5tatus>
</account>
Да, мы храним пароли открытым текстом. Это очень плохая идея, и даже в случае с нашей демонстрационной системой стоит дважды подумать, прежде чем ее использовать. В настоящей системе учетных записей надо либо шифровать пароль перед тем, как помещать его в очередь, либо вообще не хранить его там.
Функция AppendAccountXML() будет применяться еще раз, когда мы захотим записать данные в очередь удаления и в нашу базу данных учетных записей.
Использование модуля XML: :Writer в подпрограмме AppendAccountXMLO имеет несколько преимуществ:
Код получается достаточно читаемым, и тот, кто хоть немного разбирается в языках разметки, сразу же сориентируется в именах startTagO, charactersQ и endTag().
И хотя в нашем случае этого не понадобится, функция character () обеспечивает некоторую защиту - она экранирует зарезервированные символы, например символ (>).
Мы не должны запоминать последний открывающий тег, чтобы потом добавить соответствующий закрывающий. XML: : Write г заботится об этом за нас и позволяет вызвать функцию eridTag(), не указывая, какой закрывающий тег нам нужен. В нашем случае отслеживание парных тегов не так существенно, поскольку отсутствует их глубокая вложенность, но такая возможность становится очень важной в других ситуациях, где используются более сложные элементы.
Чтение кода XML при помощи XML::Parser
Скоро мы рассмотрим еще один способ построения кода XML в Perl, но сначала вернемся к чтению того кода, который только что научились создавать. Нам необходима программа, которая будет анализировать очереди добавления и удаления учетных записей, а также основную базу данных.
Можно было бы добавить анализатор XML. Но если с нашим ограниченным набором данных без использования регулярных выражений все бы и прошло, то в случае более сложных XML-данных это вряд ли получилось бы просто. Для обычного анализа проще применить модуль XML::Parser, первоначально написанный Ларри Уоллом (Larry Wall) (он был значительно расширен и поддерживается Кларком Купером (Clark Cooper)).
XML: : Parser - это модуль, основанный на событиях. Такие модули работают, как брокеры на бирже. Перед началом торгов вы оставляете им ряд инструкций о том, какие действия необходимо предпринять, если произойдут конкретные события (например, продать тысячу акций, если цена упадет до 31/4, купить другие акции в начале торгового дня и т. д.). В случае с программами, основанными на событиях, возникающие ситуации называются событиями (events), а список инструкций о том, что делать в случае конкретного события, - обработчиками событий (event handlers). Обработчики - это обычно специальные подпрограммы, созданные для работы с конкретным событием. Некоторые называют их функциями обратного вызова (callback routines), т. к. они выполняются тогда, когда основная программа «вызывает нас обратно» после того, как наступят определенные условия. В случае с модулем XML: :Parser, события - это явления, такие как «начало обработки потока данных», «найден открывающий тег» и «найден комментарий». А обработчики будут выполнять что-то подобное: «вывести содержимое только что найденного элемента».
Приступая к анализу данных, необходимо сначала создать объект XML:: Parser. При создании этого объекта следует указать, какой режим анализа или стиль (style) нужно применить. XML: : Parser поддерживает несколько стилей, поведение каждого из которых при анализе данных несколько отличается. Стиль анализа определяет, какие обработчики событий вызываются по умолчанию и каким образом структурированы возвращаемые анализатором данные (если они есть).
Некоторые стили требуют, чтобы мы указывали связь между каждым событием, которое хотим обрабатывать вручную, и его обработчиком. Для событий, не подлежащих обработке, никаких особых действий применяться не будет. Эти связи хранятся в простой хэш-таблице, ключи в ней являются именами событий, которые мы хотим обрабатывать, а значения - ссылками на подпрограммы-обработчики. В стилях, требующих наличия таких связей, мы передаем хэш посредством именованного параметра Handlers (например, Handlers => {Star-\&start_handler}) при создании объекта анализатора.
Мы будем применять стиль stream, который не требует этого шага инициализации. Он просто вызывает группу предопределенных обработчиков событий, если указанные подпрограммы были найдены в пространстве имен программы. Обработчики событий которые мы будем использовать, очень просты: StartTag, EndTag и Text. Все названия, кроме Text, говорят сами за себя. Text в соответствии с документацией XML: :Parser «вызывается прямо перед открывающим или закрывающим тегами с накопленным неразмеченным текстом из переменной $_». Мы будем применять его, когда нам понадобится узнать содержимое конкретного элемента.
Вот какой код будет использоваться в нашем приложении для инициализации:
use XML::Parser;
use Data: : Dumper; tt используется для оформления отладочного вывода, а не
и для анализа XML
$р = new XML::Parser(ErrorContext => 3,
Style => 'Stream',
Pkg => 'Account::Parse');
Этот код возвращает объект анализатора после передачи ему трех параметров. Первый, ErrorContext, передает анализатору требование вернуть три строки контекста из анализируемых данных в случае возникновения ошибки анализа. Второй устанавливает требуемый стиль анализа. Последний параметр, Pkg, сообщает анализатору, что подпрограммы обработчика событий необходимо искать в ином пространстве имен. Устанавливая этот параметр, мы распоряжаемся, чтобы анализатор искал функции &Account;:Parse: :StartTag(), &Account::Parse: :EndTag() и т.д., а не просто &StartTag(), &EndTag() и т. п. В данном случае это не имеет особого значения, но позволяет избежать ситуации, когда анализатор может случайно вызвать другую функцию с тем же именем StartTag(). Вместо того чтобы использовать параметр Pkg, можно было добавить в самое начало приведенной выше программы строку oackag-Account::Parse;.
Теперь посмотрим на подпрограммы, выполняющие функции обработчика событий. Рассмотрим их по очереди:
package Account::Parse:
sub StartTag ,
idef %rccorj : f ($_M1 eq "account"):
&StartTag() вызывается каждый раз, когда встречается открывающий тег. Эта функция вызывается с двумя параметрами: ссылкой на объект и именем встреченного тега. Поскольку для каждой учетной записи будет создаваться новая запись в хэше, можно использовать StartTag0, чтобы обозначить начало новой записи (например, открывающий тег <account>). В этом случае удаляются значения из существующего кэша. Во всех остальных случаях мы возвращаемся, ничего не выполняя:
sub Text {
my $ce = $_[0]->current_element();
$record{$ce}=$_ unless ($ce eq "account");
На этот раз мы используем &Text() для заполнения кэша %record. Как и предыдущая функция, она тоже получает два параметра при вызове: ссылку на объект и «накопленный неразмеченный текст», который анализатор нашел между последним открывающим и закрывающим тегом. Для определения элемента, в котором мы находимся, используется метод current_element(). В соответствии с документацией по XML::Parser: :Expat этот метод «возвращает имя внутреннего элемента, открытого в данный момент». То обстоятельство, что имя текущего элемента не «account», гарантирует нам, что мы находимся внутри одного из подэлементов в <account>, поэтому можно записать имя элемента и его содержимое:
sub EndTag {
print Data::Dumper->Dump([\%record], ["account"]) if ($_[1] eq "account");
И именно сейчас мы должны сделать что-то конкретное вместо
# того, чтобы просто печатать запись
}
Наш последний обработчик, &EndTag(), очень похож на первый &StartTag() с тем лишь исключением, что он вызывается тогда, когда мы находим закрывающий тег. Если мы дойдем до конца соответствующей учетной записи, то сделаем банальную вещь и напечатаем эту запись. Вот как может выглядеть такой вывод:
Saccount = {
'login' => 'bobf
'type' => 'staff.
'password' => 'password',
fullname' => 'Bob Fate'.
id' => '24-9057
}:
Saccount = {
login' => 'we'idyf',
type' => 'fatuity',
password => p35Sv,Gn3
'fullname' => 'Wendy Fate',
'id' => '50-9057'
}:
Если мы захотим использовать это в нашей системе учетных записей, нам, вероятно, понадобится вызвать некую функцию, например ateAccount(\%recorcl), а не выводить запись при помощи Data: : Djrr.per.
Теперь, когда мы познакомились с процедурами инициализации и обработчиками из XML: :Parser, нам нужно добавить код, чтобы действительно начать анализ:
# обрабатывает записи для нескольких учетных записей из
# одного XML-файла очереди
open(FILE, Saddqueue) or die "Unable to open $addq'jeue:$' \n":
# спасибо Джеффу Пиньяну за это мудрое сокращение
read(FILE, $queuecontents, -s FILE);
$p->parse("<queue>".Squeuecontents."</queue>");
Этот фрагмент кода, вероятно, заставил вас приподнять бровь, а то и обе. В первых двух строчках мы открываем файл очереди и считываем его содержимое в скалярную переменную Squeuecontents. Третья строка могла бы показаться понятной, если бы не забавный аргумент, переданный функции parse(). Почему мы считываем содержимое файла очереди и заключаем его в теги XML вместо того, чтобы перейти к его анализу?
А потому, что это хак. И надо сказать - не плохой. И вот почему эти «фокусы» необходимы для анализа нескольких элементов <account> в одном файле очереди.
Каждый XML-документ по определению должен иметь корневой элемент (root element).
Этот элемент служит контейнером для всего документа; все остальные элементы являются его подэлементами. XML-анализатор ожидает, что первый встреченный им тег будет открывающим тегом корневого элемента документа, а последний тег - закрывающим тегом этого элемента. XML-документы, не соответствующие этой структуре, не считаются корректными (well-formed).
Попытка смоделировать очередь в XML заставит нас призадуматься. Если ничего не сделать, то первым тегом, найденным в файле, будет <account>. Все будет работать нормально до тех пор, пока анализатор не встретит закрывающий тег </account> для этой записи. В этот момент анализатор завершит свою работу, даже если в очереди есть другие записи, потому что он посчитает, что дошел до конца документа.
Мы без труда могли бы добавить открывающий тег (<que^e>) в начало очереди, но что делать с закрывающим тегом? Закрывающий тег корневого элемента всегда должен быть расположен в самом конце документа (и не иначе), а сделать это не просто, учитывая, что мы собираемся постоянно добавлять записи в этот файл.
Можно было бы (но это довольно неприятно) достигать конца файла при помощи функции seek(), а затем двигаться назад (опять же при помощи seok()) и остановиться прямо перед последним закрывающим тегом. Затем мы могли бы записать нашу новую запись перед этим тегом, оставляя закрывающий тег в самом конце данных. Риск повредить данные (что, если мы перейдем не в ту позицию?) должен предостеречь вас от использования этого метода. Кроме того, этот метод сложно применять, если вы не можете точно определить конец файла, например, при чтении данных XML по сетевому соединению. В подобных случаях, вероятно, стоит буферизовать поток данных, чтобы можно было вернуться к концу данных после завершения соединения.
Метод, показанный в предыдущем примере и предлагающий добавлять пару корневых тегов к существующим данным, может показаться хаком, но выглядит он гораздо элегантнее, чем другие решения. Впрочем, вернемся к более приятной теме.
Чтение XML при помощи XML::Simple
Мы уже видели один метод, позволяющий анализировать XML-данные при помощи модуля XML::Parser. Чтобы соответствовать правилу TMTOWTDI, давайте снова обратимся к этой проблеме, немного упростив задачу. Многие писали собственные модули, построенные на XML::Parser, для анализа XML-документов и возврата данных в удобной для работы форме объектов/структур данных, к их числу относятся и XML::DOM Энно Дэрксена (Enno Derksen), XML::Grove, и ToObjects (часть
libxml-perl) Кена Маклеода (Ken MacLeod), XML: : DT Xoce Xoa Диaca де Альмейды (Jose Joao Dias de Almeida), и XML: : Simple Гранта Маклина (Grant McLean). Из всех этих модулей, вероятно, проще всего использовать модуль XML::Simple. Он был создан для обработки небольших конфигурационных файлов на XML, что отлично подходит для нашей задачи.
XML:: Simple предоставляет две функции. Вот первая (в данном контексте):
use XML::Simple;
use Data:: Dumper;
# нужен для вывода содержимого структура данных
Squeuefile = "addqueije. xml":
open(FILE, Squeuefiie) or die "Unable to open Squeuenle : $! \n":
read(FILE. Squeuecontents, -s FILE):
Squeue = XMLin("<queue>".Squejecontents."</qjeue>"):
Содержимое Squeue мы выводим подобным образом:
prim Da la : : Dumper->Di;
mp( [Sqi.euc ] ["uueuo" ])
Теперь это ссылка на данные, найденные в файле очереди, сохраненные в виде хэшей, ключами которого являются элементы <id>.
Мы используем именно такие ключи потому, что XML: : Simple позволяет распознавать в данных конкретные теги, выделяя их среди других в процессе преобразования. Если мы отключим эту возможность:
Squeue = XMLin("<queue>".$queuecontents."</queue>",keyattr=>[]);
то получим ссылку на хэш, где единственное значение является ссылкой на анонимный массив.
Такая структура данных не очень полезна. Этот параметр можно определять по собственному желанию:
$queue = XMLin("<queue>".$qucuecontents."</queue>",keyattr => ["login"]):
Замечательно? Теперь мы можем удалить элементы из очереди в памяти, после того как обработаем их всего в одной строке:
# например, Slogin - "bobf"; delete $queue->{account){$logi");
Если мы хотим изменить значение, перед тем как записать его на диск (скажем, мы работаем с нашей основной базой данных), то это тоже просто сделать:
# например, $login="wendyf"; $field="status"
$queue->{account}{$login}{$field}="created";
Создание XML-данных при помощи XML::Simple
Упоминание «записать его на диск» возвращает нас обратно к методу создания XML-данных, который мы обещали показать. Вторая функция из XML: : Simple принимает ссылку на структуру данных и генерирует XML-данные: rootname определяет имя корневого элемента, мы могли бы использовать XMLdecl. чтобы добавить объявление XML print XMLout($queue, rootname =>"queue"),
В результате получаем (отступы сделаны для удобства чтения):
<queue> <account name="bobf" type="staff"
password="password" status="to_be_created"
fullname="Bob Fate" id="24-9C57" />
<account nanie="wendyf" type="faculty"
password="password"
status="to_be_created"
fullname="Wendy Fate" id="50-9057" />
</queue>
Мы получили отличный XML-код, но его формат несколько отличается от формата наших файлов с данными. Данные о каждой учетной записи представлены в виде атрибутов одного элемента <account> </ accojnt>, a не в виде вложенных элементов. В XML: :Simple есть несколько правил, руководствуясь которыми, он преобразовывает структуры данных. Два из них можно сформулировать так (а остальные можно найти в документации): «отдельные значения преобразуются в XML-атрибуты», а «ссылки на анонимные массивы преобразуются во вложенные XML-элементы».
Чтобы получить «верный» XML-документ («верный» означает «в том же стиле и того же формата, что и наши файлы данных»).
Кошмар, не правда ли? Но у нас есть варианты для выбора. Мы можем:
Изменить формат наших файлов данных. Это похоже на крайнюю меру.
Изменить способ, которым XML: :Simple анализирует наш файл. Чтобы получить такую структуру данных (рис. 3.6), мы могли бы использовать функцию XMLin() несколько иначе:
Squeue = XMLin("<queue>",Squeuecontents."</queue>",
forcearray=>1, keyattr => [""]):
Но если мы перекроим способ чтения данных, чтобы упростить запись, то потеряем семантику кэшей, упрощающих поиск и обработку данных.
Выполнить некую обработку данных после чтения, но до записи. Мы могли бы прочитать данные в нужную нам структуру (так же, как делали это раньше), применить эти данные в нужном месте, а затем преобразовать структуру данных в один из вариантов, получаемых модулем XML: : Simple, перед тем как записать ее.
Вариант номер 3 кажется более разумным, так что последуем ему. Вот подпрограмма, которая принимает одну структуру данных (рис. 3.5), и преобразует ее в другую структуру данных (рис. 3.6). Объяснение примера будет приведено позже:
sub TransforuiForWrite{ my $queueref = shift;
my Stoplevel = scalar each %$queueref;
foreach my $user (keys %{$queueref->{$toplevel}}){
my %innerhash =map {$_, [$queueref->
{$toplevel}{$user}{$J] }
keys %<$queueref->{Stoplevel}{$user}};
$innerhash{'login'} = [$user];
push @outputarray, \%innerhash; }
Soutputref = { Stoplevei => \@outnui.array};
return $outputref:
}
Теперь подробно рассмотрим подпрограмму TrarsformForWate().
Если вы сравните две структуры (рис. 3.5, рис. 3.6), то заметите в них кое-что общее: это внешний хэш, ключом которого в обоих случаях является account. В следующей строке видно, как получить имя этого ключа, запрашивая первый ключ из хэша, на который указывает $que-ueref:
my Stoplevel = scalar each :6$i;eref:
Интересно взглянуть на закулисную сторону создания этой структуры данных:
my %innernabh =
mар {$_. [$queuer-ef-><$toplevel){$use'-}{$_ M 1
keys %{$queueref->{$toplevelf{$user}};
В этом отрывке кода мы используем функцию "iap(), чтобы обойти все ключи, найденные во внутреннем хэше для каждой записи (т. е. login, type, password и status). Ключи возвращаются в такой строке:
keys %{$queueref->{$toplevel}{$user}};
Просматривая ключи, можно с помощью тар вернуть два значения для каждого из них: сам ключ и ссылку на анонимный массив, содержащий его значение:
шар {$_, [$queueref->{$topleve]}{$user}{$_n }
Список, возвращаемый тар(), выглядит так:
(login,[bobf], type,[staff], password,[password]...)
Он имеет формат ключ-значение, где значения хранятся как элементы анонимного массива. Этот список можно присвоить хэшу %innerhash, чтобы заполнить внутреннюю хэш-таблицу для получаемой структуры данных (my %innerhash =). Кроме того, к хэшу следует добавить ключ login, соответствующий рассматриваемому пользователю:
$innerhash{'login'} = [$user];
Структура данных, которую мы пытаемся создать, - это список подобных хэшей, поэтому после того как будет создан и определен внутренний хэш, необходимо добавить ссылку на него в конец списка, т. к. он и представляет получаемую структуру данных:
push @outputarray. \%innerhash:
Такую процедуру следует повторить для каждого ключа login из первоначальной структуры данных (один на каждую запись об учетной записи). После того как это будет сделано, у нас появится список ссылок на хэши в той форме, которая нам нужна. Мы создаем анонимный хэш с ключом, совпадающим с внешним ключом из первоначальной структуры данных, и значением, равным нашему списку хэшей. Ссылку на этот анонимный хэш можно возвратить обратно вызывающей программе. Вот и все:
Soutputref = { Stoplevel => \SO'..tputarray}: return Soutputre?:
Теперь, располагая &TransforrnForWrite(), мы можем написать программу для чтения, записи наших данных и работы с ними:
Squeue = XMLin("<queue>".$queuecontents."</queue>",keyattr => ["login"]);
print OUTPLITFILE XMLojt(Ti ansfonnFor Write($queuu), г ootname => "queue");
Записанные и прочитанные данные будут иметь один и тот же формат.
Перед тем как закрыть тему чтения и записи данных, следует избавиться от несоответствий:
1. Внимательные читатели, наверное, заметили, что одновременное использование XML: : Write г и XML: : Simple в одной и той же программе для записи данных в очередь может оказаться непростым делом. Если записывать данные при помощи XML: : Simple, то они будут вложены в корневой элемент по умолчанию. Если же применять XML: : Write г (или просто операторы print) для записи данных, вложения не произойдет, т. е. нам придется опять прибегнуть к хаку "<queue>". Squeuecontents. "</queue>". Возникает неудачный уровень синхронизации чтения-записи между программами, анализирующими и записывающими данные в XML-формате.
Чтобы избежать этой проблемы, надо будет использовать продвинутую возможность модуля XML: .'Simple: если XMLoutO передать параметр rootname с пустым значением или значением undef, то возвращаются данные в XML-формате без корневого элемента. В большинстве случаев так поступать не следует, потому что в результате образуется неправильный (синтаксически) документ, который невозможно проанализировать. Наша программа позволяет этим методом воспользоваться, но такую возможность не стоит применять необдуманно.
2. И хотя в примере этого нет, мы должны быть готовы к обработке ошибок анализа. Если файл содержит синтаксически неверные данные, то анализатор не справится и прекратит работу (согласно спецификации XML), остановив при этом и всю программу в случае, если вы не примете мер предосторожности. Самый распространенный способ справиться с этим из Perl - заключить оператор анализа в eval() и затем проверить содержимое переменной $@ после завершения работы анализатора. Например:
eval {$p->parse("<queue>".Squeuecontents."</queue>")};
if ($@) { сделать что-то для обработки ошибки перед выходом.. };
Другим решением было бы применение известного модуля из разряда XML: : Checker, т. к. он обрабатывает ошибки разбора аккуратнее.
Низкоуровневая библиотека компонентов
Теперь, когда мы умеем отследить данные на всех этапах, включая то, как они получаются, записываются, читаются и хранятся, можно перейти к рассмотрению их использования глубоко в недрах нашей системы учетных записей. Мы собираемся исследовать код, который действительно создает и удаляет пользователей. Ключевой момент этого раздела заключается в создании библиотеки повторно используемых компонентов. Чем лучше вам удастся разбить систему учетных записей на подпрограммы, тем проще будет внести лишь небольшие изменения, когда придет время переходить на другую операционную систему или что-либо менять. Это предупреждение может показаться ненужным, но единственное, что остается постоянным в системном администрировании, - это постоянные изменения.
Подпрограммы для создания и удаления учетных записей в Unix
Начнем с примеров кода для создания учетных записи в Unix. Большая часть этого кода будет элементарной, поскольку мы избрали легкий путь. Наши подпрограммы для создания и удаления учетных записей вызывают команды с необходимыми аргументами, входящие в состав операционной системы, для «добавления пользователей», «удаления пользователей» и «смены пароля».
Зачем нужна эта очевидная попытка отвертеться? Этот метод приемлем, поскольку известно, что программы, входящие в состав операционной системы, хорошо «уживаются» с другими компонентами. В частности, этот метод:
Не забывает о блокировке (т. е. позволяет избежать проблем с поврежденными данными, которые могут возникнуть, если две программы пытаются одновременно записать данные в файл паролей).
Справляется с вариациями в файле паролей (включая шифрование пароля), о чем упоминалось раньше.
Наверняка справится со схемами авторизации и механизмами распространения паролей, существующими в этой операционной системе. Например, в Digital Unix добавляющая пользователей внешняя программа может напрямую работать и с NIS-картами на основном сервере.
Применение внешних программ для создания и удаления учетных записей обладает такими недостатками:
Различия операционных систем
В каждую операционную систему входит свой собственный набор программ, расположенных в разных местах и принимающих несколько различные аргументы. Это редкий пример совместимости, однако практически во всех распространенных вариантах Unix (включая Linux, но исключая BSD) используются максимально совместимые программы для удаления и создания пользователей: useradd и user-del. В вариантах BSD применяются
adduser и rmuser, две программы со сходным назначением, но совершенно разными аргументами. Подобные различия могут значительно усложнить наш код.
Соображения безопасности
Вызываемые программы с переданными им аргументами будут видны всем, кто употребляет команду ps. Если создавать учетные записи только на защищенной машине (например, на основном сервере), риск утечки данных значительно снизится.
Зависимость от программы.
Если внешняя программа почему-либо изменится или будет удалена, то нашей системе учетных записей настанет «полный капут».
Потеря контроля
Нам приходится считать часть процесса создания учетной записи неделимым. Другими словами, когда запущена внешняя программа, мы не можем вмешаться в этот процесс и добавить какие-либо свои собственные операции. Выявление ошибок и процесс восстановления становятся более сложными.
Эти программы редко делают все
Вероятнее всего, что данные программы не выполняют все действия, необходимые для формирования учетной записи на вашей машине. Возможно, вам понадобится добавить некоторых пользователей в некоторые вспомогательные группы, включить их в список рассылки на вашей системе или же добавить пользователей к файлу лицензии коммерческого продукта. Для обработки подобных действий вам придется написать дополнительные программы. Это, конечно, не проблема, наверняка любая система учетных записей, которую вы придумаете, потребует от вас большего, чем просто вызвать пару внешних программ. Более того, это не удивит большинство системных администраторов, потому что их работа меньше всего похожа на беззаботную прогулку по парку.
В случае с нашей демонстрационной системой учетных записей преимущества перевешивают недостатки, поэтому посмотрим на примеры кодов, в которых используется вызов внешних программ. Чтобы ничего не усложнять, мы покажем пример программы, работающей только на локальной машине с Linux и Solaris, и проигнорируем все трудности, вроде NIS и вариаций BSD. Если вам хочется посмотреть на более сложный пример этого метода в действии, поищите семейство модулей Cf gTie Рэнди Мааса (Randy Maas).
Вот основная программа, необходимая для создания учетной записи:
# На самом деле эти переменные надо определить в центральном
# конфигурационном файле
сluseraddex = "/usr/sbin/useradd"; ft путь к useradd $passwdex = "/bin/passwd";
# путь к passwd $homel)nixdirs = "/home"; ft корневой каталог
# домашних каталогов Sskeldir = "/home/skel"; ft прототип домашнего
# каталога $defshell = "/bin/zsh"; ft интерпретатор no
# умолчанию
sub CreateUnixAccount{
my ($account,$record) = @_;
# конструируем командную строку, используя:
ft -с = поле комментария
ft -d = домашний каталог
и -д = группа (считаем равной типу пользователя)
ft -m = создать домашний каталог
ft -k = и скопировать файлы из каталога-прототипа
» -s = интерпретатор по умолчанию
# (можно также использовать -G group, group, group для
# добавления пользователя к нескольким группам)
my @cmd = (Suseraddex,
"-с", $record->{"fullname"},
"-d", "$homeUnixdirs/$account",
"-g", $record->{"type"},
"-m",
"-k", $skeldir,
"-s", Sdefshell,
laccount);
print STOERR "Creating account..."; ^
my $result = Oxff & system @cmd;
# код возврата 0 в случае успеха и не 0 при неудаче,
и поэтому необходимо инвертирование
if (!$result){
print STDERR "failed.\n";
return "Suseraddex failed"; } else {
print STDERR "succeeded.\n"; }
print STDERR "Changing passwd...";
unless ($result = &InitUnixPasswd($account,$record->{"password"»){
print STDERR "succeeded.\n";
return ""; > else {
print STDERR "failed.\n";
return $result; } >
В результате необходимая запись будет добавлена в файл паролей, будет создан домашний каталог для учетной записи и скопированы некоторые файлы окружения
(.profile, .tcshrc, .zshrc, и т.д.) из каталога-прототипа.
Обратите внимание, что мы используем отдельный вызов для установки пароля. Команда useradd на некоторых операционных системах (например, Solaris) оставляет учетную запись заблокированной до тех пор, пока для этой учетной записи не будет вызвана программа pass-wd. Подобный процесс требует известной ловкости рук, поэтому мы оформим данный шаг как отдельную подпрограмму, чтобы оставить в стороне подробности. Об этой подпрограмме мы еще поговорим, а пока рассмотрим «симметричный» код, удаляющий учетные записи:
# На самом деле эти переменные надо устанавливать в центральном
# конфигурационном файле
$userdelex = "/usr/sbin/userdel";
# путь к userdel
sub DeleteUnixAccount{
my ($account,Srecord) = @_;
# конструируем командную строку, используя:
# -г - удалить домашний каталог
my @cmd = (Suserdelex, "-r", $account);
print STDERR "Deleting account.,.";
my $result - Oxffff & system tJcmd;
tt код возврата 0 соответствует успеху, не 0 - неудаче,
№ поэтому необходимо инвертирование
if ('$result){
print STDERR "succeeded.\n";
return ""; } else {
print STDERR "failed.\n":
return "Suserdelex failed";
}
}
Перед тем как перейти к операциям с учетными записями в NT, разберемся с подпрограммой InitUmxPasswdO, о которой упоминалось раньше. Чтобы завершить создание учетной записи (по крайней мере, в Solaris), необходимо изменить ее пароль при помощи стандартной команды passwd. Обращениеpasswd <accountname>
изменит пароль для этой учетной записи.
Звучит просто, но тут затаилась проблема. Команда passwd запрашивает пароль у пользователя. Она принимает меры предосторожности, чтобы убедиться, что общается с настоящим пользователем, взаимодействуя напрямую с его терминалом. В результате следующий код работать не будет:
и такой код РАБОТАТЬ НЕ БУДЕТ open(PW,"|passwd Saccount")
print PW $olcipasswd, "\n"; print PW Snewpasswd,"\n";
На этот раз мы должны быть искуснее, чем обычно; нам нужно как-то заставить команду passed думать, что она имеет дело с человеком, а не программой на Perl. Этого можно достичь, если использовать модуль Expect.pm, написанный Остином Шутцом (Austin Schutz), - ведь он устанавливает псевдотерминал (pty), внутри которого выполняется другая программа. Expect.pm основан на известной Tel-программе Expect Дона Либеса (Don Libes). Этот модуль входит в семейство модулей, взаимодействующих с программами. В главе 6 мы рассмотрим его близкого «родственника», модуль Net: :Telnet Джея Роджерса (Jay Rogers).
Эти модули действуют в соответствии со следующей моделью: они ждут вывода программы, посылают ей на ввод данные, ждут ответа, посылают некоторые данные и т. д. Приведенная ниже программа запускает команду passed в псевдотерминале и ждет до тех пор, пока та запросит пароль. Поддержание «разговора» с passwd не должно требовать усилий:
use Expect;
sub InitUnixPasswd {
my (Saccount,Spasswd) = @_;
ft вернуть объект
my $pobj - Expect->spawn($passwdex, Saccount);
die "Unable to spawn $passwdex:$!\n" unless (defined SpoDj):
it не выводить данные на стандартный вывод (т. е.
# работать молча)
$pobj->log_stdout(0);
# Подождать запроса на ввод пароля и запроса на повторение
# пароля, ответить. $pobj->expect(10,"New password: ");
и Linux иногда выводит подсказки раньше, чем он готов к вводу,
print $pob] "$passwd\r"
$pobi->expect(10, "Re-enter new password: "); print $pob] "$passwd\r";
it работает"
Sresul: = (defined ($pobj-~ expectdO.
"successfully changeo")) ? "" : "password crarac.
failed"): в закрываем обьект, ждем 15 секунд, пока процесс завершится
$pobj-'suft_close();
return $resuJ t.:
}
Модуль Expect.pm очень хорошо подходит для этой подпрограммы, но стоит отметить, что он годится для куда более сложных операций. Подробную информацию можно найти в документации и руководстве по модулю Expect.pm.
Подпрограммы для создания и удаления учетных записей в Windows NT/2000
Процесс создания и удаления учетных записей в Windows NT/2000 несколько проще, чем в Unix, поскольку стандартные вызовы API для этой операции существуют в NT. Как и в Unix, мы могли бы вызвать внешнюю программу, чтобы выполнить подобную работу (например, вездесущую команду net с ключом USERS/ADD), но проще использовать API-вызовы из многочисленных модулей, о некоторых из которых мы уже говорили. Функции для создания учетных записей есть, например, в Win32::NetAdmin, Win32: :UserAdmin, Win32API::Net и Win32::Lanman. Пользователям Windows 2000 лучше ознакомиться с материалом по ADSI в главе 6.
Выбор одного из этих модулей, в основном, дело вкуса. Чтобы разобраться в отличиях между ними, рассмотрим существующие вызовы для создания пользователей. Эти вызовы описаны в документации Network Management SDK на
http://msdn.microsoft. com (если вы ничего не можете найти, поищите «NetUserAdd»). NetllserAdd() и другие вызовы принимают в качестве параметра информационный уровень данных. Например, если информационный уровень равен 1, структура данных на С, передаваемая вызову для создания пользователя, выглядит так:
typedef struct JJSER_INFO_1 {
LPWSTR usri1_name;
LPWSTR usri1_oassworc';
DWORD usril_passwora_age:
DWORD usril_oriv:
LPWSTR usril_home_dir;
LPWSTR usri1_comment;
DWORD usri1_flags:
LPWSTR usri1_script_pat!i:
}
Если используется информационный уровень, равный 2, структура значительно расширится:
typedef struct _UbER_INrG;
LPWSTR |
usn2_name; |
LPWSTP |
lisri? password: |
DWORD |
usri2_password_age: |
DWORD |
usn2_priv: |
LPWSTR |
Lisri2_home_dir; |
LPWSTR |
usri2_conwient : |
DWORD |
usri2_flags; |
LPWSTR |
usri2_scnpt_path; |
DWORD |
usri2_auth_f lags; |
LPWSTR |
usri2_fiJll_name: |
LPWSTR |
usri2_usr_comment; |
LPWSTR |
usri2_parms: |
LPWSTR |
usri2_workstations: |
DWORD |
usri2_last_logon; |
DWORD |
usn2_last_logoff ; |
DWORD |
usri2_acct_expires; |
DWORD |
usri2_max_storage; |
DWORD |
usri2_units_per_week; |
PBYTE |
usri2_logon_hours; |
DWORD |
usri2_bad_pw^count; |
DWORD |
usri2_num_logons; |
LPWSTR |
usri2_logon_server; |
DWORD |
usri2_country_code; |
DWORD |
usri2_code_page; |
He обязательно много знать об этих параметрах или даже вообще о С, чтобы понять, что при изменении уровня увеличивается количество информации, которое можно передать при создании пользователя. Кроме того, каждый последующий уровень является надмножеством предыдущего.
Какое это имеет отношение к Perl? Каждый упомянутый модуль требует принять два решения:
Нужно ли объяснять программистам на Perl, что такое «информационный уровень»?
Какой информационный уровень (т. е. сколько параметров) может использовать программист?
Модули Win32API: :Net и Win32: :UserAdmin позволяют программисту выбрать информационный уровень. Win32; : NetAdmin и Win32: : Lanrnan этого не делают. Из всех этих модулей Win32; :NetAdmin применяет наименьшее число параметров; в частности, вы не можете определить поле на этапе создания пользователя. Если вы решите применять модуль Win32; :NetAcmin, вам, скорее всего, придется дополнить его вызовами из другого модуля, чтобы установить те параметры, которые он устанавливать не позволяет. Если вы остановитесь на комбинации Win32 : : NetAarn.in и Win32 : : AaminMisc, вам стоит обратиться к многократно упомянутой книге Рота, поскольку это отличный справочник по модулю Win32: : NetAdmin, по которому нет достаточного количества документации.
Теперь читателю должно быть понятно, почему выбор модуля - это де ло личных предпочтений. Хорошей стратегией было бы сначала решить, какие параметры важны для вас, а затем найти модуль, который их поддерживает. Для наших демонстрационных подпрограмм мы выбираем модуль Win32: : Lanman. Вот какой код можно применить для создания и удаления пользователей в нашей системе учетных записей:
use Win32: :Lanman; tt для создания учетной записи
use Win32::Perms; # для установки прав на домашний каталог
$homeNTdirs = "\\\\homeserver\\home"; # корневой каталог
# домашних каталогов
sub CreateNTAccount{
my ($account,$record) = @_;
П создаем учетную запись на локальной машине
# (т. е., первый параметр пустой)
$result = Win32::Lanman::NetUserAdd("",
{'name' => Saccount,
'password' => $record->{password},
'home_dir' => "$homeNTdirs\\$account",
'full_name' => $record->{fullname}});
return Win32::Lanman::6etLastError() unless ($result);
добавляем в нужную ЛОКАЛЬНУЮ группу
(предварительно мы Я получаем SID учетной записи)
# Мы считаем, что имя группы совпадает с типом учетной
die "SID lookup error:
".Win32::Lanman::6etLastError()."\n"
unless (Win32: :Lanman: :LsalookupNames("", [$account],
\@info)); $result = Win32::Lanman::NetLocalGroupAddMember("",
$record->{type), ${$info[0]){sid»;
return Win32::Lanman::GetLastError() unless (Sresult);
# создаем домашний каталог
mkdir "$homeNTdirs\\$account",0777 or
return "Unable to make honedir:$!";
№ устанавливаем ACL и владельца каталога
$acl = new Win32::Perms("$homeNTdirs\\$account");
$acl->0wner($account);
# мы предоставляем пользователю полный контроль за
# каталогом и всеми файлами, которые будут в нем созданы
# (потому и два различных вызова)
DIRECTORY | СОНТШЕВ_ШЕИТ_АСЕ);
$acl->Allow($account, FULL, -
FILE|OBJECT_INHERIT_ACE|IMHERIT_ONLY_ACE);
$result = $acl->Set(); $acl->Close();
return($result ? "" : Sresult); }
Программа для удаления пользователей выглядит так:
use Win32: iLanman;
для удаления учетной записи
use File::Path;
для рекурсивного удаления каталогов
sub DeleteNTAccount{
my($account,$record) = @_;
# удаляем пользователя только из ЛОКАЛЬНЫХ групп.
Если мы № хотим удалить их и из глобальных групп, мы можем убрать
слово "Local" из двух вызовов Win32::Lanman::NetUser
(например, NetUserGetGroups)
die "SID lookup error: ".Win32::Lanman::GetLastError()."\n"
unless (Win32::Lanman::LsaLookupNames("",
[Saccount], \@info));
Win32::Lanman::NetUserGetLocalGroups($server, Saccount, ",
\@groups); foreach $group (@groups){
print "Removing user from local group ".
$group->{name}."...";
print(Win32::Lanman::NetLocalGroupDelMember("",
$group->{name}, ${$info[0]}{sid})?
"succeeded\n" : "FAILED\n"); }
tt удалить эту учетную запись с локальной машины
(т. е., перый параметр пустой) Sresult = Win32::Lanman::NetUserDel("", Saccount);
return Win32::Lanman::GetLastError() if ($result);
удалить домашний каталог и его содержимое
Sresult = rmtree("$homeNTdirs\\$account",0,1);
rmtree возвращает число удаленных файлов, так что если мы
удалили более нуля элементов, то скорее всего все прошло 8 успешно return Sresult;
}
Заметьте, для удаления домашнего каталога здесь используется переносимый модуль File: :Path. Если бы мы хотели сделать что-то специфичное для Win32, например, переместить домашний каталог в корзину, то могли бы сделать это при помощи модуля Win32: :File Op Йенды Крыники (Jenda Krynicky), который можно найти на http://jen da.krynicky.cz/. В таком случае мы применили бы Wi n32: ; F: 1еОр и изменили бы строку, включающую rmtrc-e(), на:
# удалим каталог в корзину, потенциально подтверждая
# действие пользователем, если для этой учетной записи
# необходимо подтверждать такие операции
$result = Recycle("$homeNTdirs\\$account");
В данном модуле есть функция Delete(), которая выполняет то же, что и rmtree() менее переносимым (правда, более быстрым) способом.
Сценарии
Теперь, когда мы разобрались с базой данных, самое время написать сценарии для выполнения периодических или каждодневных действий, необходимых при системном администрировании. Эти сценарии построены на низкоуровневой библиотеке компонентов (Account.pm), которую мы создали, объединив в один файл все только что написанные подпрограммы. Такая подпрограмма позволяет убедиться, что все необходимые модули загружены:
sub InitAccountf
use XML: :Writer;
Srecord = { fields => [login, fullname,id,type,password]};
$addqueue = "addqueue"; tt имя файла очереди добавления
Sdelqueue = "delqueue"; ft имя файла очереди удаления
$maindata = "accountdb"; tt имя основной базы данных
ft учетных записей
if ($"0 eq "MSWin32"){
require Win32::Lanman;
require Win32::Perms;
require File::Path;
ft местоположение файлов учетных записей
Saccountdir = "\\\\server\\accountsystem\\";
ft списки рассылки
$maillists = "$accountdir\\maillists\\";
ft корневой каталог домашних каталогов
$homeNTdirs = "\\\\nomeserver\\home";
ft имя подпрограммы, добавляющей учетные записи
Saccountadd = "CreateNTAccount";
ft имя подпрограммы, удаляющей учетные записи
Saccountdel = "DeleteNTAccount": }
else {
require Expect;
и местоположение файлов учетных записей
$accountdir = "/usr/accountsystem/";
в списки рассылки
Smaillists = "Saccountdir/maillists/";
tt местоположение команды useradd
Suseraddex = ",/usr/sbin/useradd";
tt местоположение команды userdel
Suserdelex = "/usr/sbin/userdel";
tt местоположение команды passwd
Spasswdex = "/bin/passwd";
tt корневой каталог домашних каталогов
ShomeUnixdirs = "/home";
tt прототип домашнего каталога
$skeldir = "/home/skel";
ft командный интерпретатор по умолчанию
Sdefshell = "/bin/zsh";
ft имя подпрограммы, добавляющей учетные записи
Saccountadd = "CreateUnixAccount";
tt имя подпрограммы, удаляющей учетные записи
Saccountdel = "DeleteUnixAccount";
}
}
Рассмотрим сценарий, обрабатывающий очередь добавления:
use Account;
use XML;:Simple;
SlnitAccount;
считываем низкоуровневые подпрограммы
&ReadAddQueue;
tt считываем и анализируем очередь добавления
&ProcessAddQueue;
tt пытаемся создать все учетные записи
&DisposeAddQueue;
ft записываем учетную запись либо в основную
tt базу данных, либо обратно в очередь, если
tt возникли какие-то проблемы
tt считываем очередь добавления в структуру данных $queue
sub ReadAddQueue{
open(ADD,Saccountdir.Saddqueue) or
die "Unable to open ".Saccountdir.$addqueue.":$!\n";
read(ADD, Squeuecontents, -s ADD);
close(ADD);
Squeue = XMLin("<queue>".Squeuecontents."</queue>",
keyattr => ["login"]);
ft обходим в цикле структуру данных, пытаясь создать учетную
запись для каждого запроса (т. е. для каждого ключа)
sub ProcessAddQueue{
foreach my Slogin (keys %{$queue->{account}})
{
sub InitAccountf
use XML: :Writer;
Srecord = { fields => [login, fullname,id,type,password]};
$addqueue = "addqueue"; tt имя файла очереди добавления
Sdelqueue = "delqueue"; ft имя файла очереди удаления
$maindata = "accountdb"; tt имя основной базы данных
ft учетных записей
if ($"0 eq "MSWin32"){
require Win32::Lanman;
require Win32::Perms;
require File::Path;
ft местоположение файлов учетных записей
Saccountdir = "\\\\server\\accountsystem\\";
ft списки рассылки
$maillists = "$accountdir\\maillists\\";
ft корневой каталог домашних каталогов
$homeNTdirs = "\\\\nomeserver\\home";
ft имя подпрограммы, добавляющей учетные записи
Saccountadd = "CreateNTAccount";
ft имя подпрограммы, удаляющей учетные записи
Saccountdel = "DeleteNTAccount": }
else {
require Expect;
и местоположение файлов учетных записей
$accountdir = "/usr/accountsystem/";
в списки рассылки
Smaillists = "Saccountdir/maillists/";
tt местоположение команды useradd
Suseraddex = ",/usr/sbin/useradd";
tt местоположение команды userdel
Suserdelex = "/usr/sbin/userdel";
tt местоположение команды passwd
Spasswdex = "/bin/passwd";
tt корневой каталог домашних каталогов
ShomeUnixdirs = "/home";
tt прототип домашнего каталога
$skeldir = "/home/skel";
ft командный интерпретатор по умолчанию
Sdefshell = "/bin/zsh";
ft имя подпрограммы, добавляющей учетные записи
Saccountadd = "CreateUnixAccount";
tt имя подпрограммы, удаляющей учетные записи
Saccountdel = "DeleteUnixAccount";
}
}
Рассмотрим сценарий, обрабатывающий очередь добавления:
use Account;
use XML;:Simple;
SlnitAccount;
считываем низкоуровневые подпрограммы
&ReadAddQueue;
считываем и анализируем очередь добавления
&ProcessAddQueue;
пытаемся создать все учетные записи
&DisposeAddQueue;
записываем учетную запись либо в основную
tt базу данных, либо обратно в очередь, если
tt возникли какие-то проблемы
tt считываем очередь добавления в структуру данных $queue
sub ReadAddQueue{
open(ADD,Saccountdir.Saddqueue) or
die "Unable to open ".Saccountdir.$addqueue.":$!\n";
read(ADD, Squeuecontents, -s ADD);
close(ADD);
Squeue = XMLin("<queue>".Squeuecontents."</queue>",
keyattr => ["login"]);
ft обходим в цикле структуру данных, пытаясь создать учетную
запись для каждого запроса (т. е. для каждого ключа)
sub ProcessAddQueue{
foreach my Slogin (keys %{$queue->{account}}){
Sresult = &$accountadd($login,
$queue->{account}->{$login});
if (!$result){
$queue->{account}->{$login}{status} = "created";
}
else {
$queue->{account}->{$login}{status} =
"error:$result";
}
}
}
ft теперь снова обходим структуру данных. Каждую учетную запись
# со статусом "created," добавляем в основную базу данных. Все
и остальные записываем обратно в файл очереди, перезаписывая
tt все его содержимое.
sub DisposeAddQueue{
foreach my Slogin (keys %{$queue->{account}}){
if ($queue->{account}->{$login}{status} eq "created")!
$queue->{account}->{$login}{login} = Slogin;
$queue->{account}->{$login}{creation_date} = time;
&AppendAccountXML($accountdir.$maindata,
$queue->{account}->{$login});
delete $queue->{account}->{$login};
next;
}
}
# To, что осталось сейчас в Squeue, - это учетные записи,
# которые невозможно создать
# перезаписываем файл очереди
open(ADD,">".$accountdir.$addqueue) or
die "Unable to open ".$accountdir.$addqueue.":$!\n";
П если есть учетные записи, которые не были созданы,
# записываем их
if (scalar keys %{$queue->{account}}){
print ADD XMLout(&TransformForWrite($queue),
rootname => undef);
}
close(ADD);
}
Сценарий, обрабатывающий очередь удаления, очень похож:
use Account;
use XML::Simple;
SlnitAccount;
# считываем низкоуровневые подпрограммы
&ReadDelOueue;
# считываем и анализируем очередь удаления
&ProcessDelQueue;
пытаемся удалить все учетные записи
&DisposeDelQueue;
удаляем учетную запись либо из основной
базы данных, либо записываем ее обратно в
в очередь, если возникли какие-то проблемы
и считываем очередь удаления в структуру данных $queue
sub ReadDelQueue{
open(DEL,Saccountdir.Sdelqueue) or
die "Unable to open ${accountdir}${delqueue}:$!\n";
read(OEL, Squeuecontents, -s DEL);
close(DEL);
Squeue = XMLin("<queue>".$queuecontents."</queue>",
keyattr => ["login"]);
}
# обходим в цикле структуру данных, пытаясь удалить учетную
# запись при каждом запросе (т, е. для каждого ключа)
sub ProcessDelQueue{
foreach my Slogin (keys %{$queue->{account}}){
Sresult = &$accountdel($login,
$queue->{account}->{$login});
if (!$result){
Squeue->{account}->{$login}{status} = "deleted";
}
else {
$queue->{account}->{$login}{status} =
"error:$result";
}
>
}
# считываем основную базу данных и затем вновь обходим в цикле
структуру Squeue. Для каждой учетной записи со статусом
"deleted," изменяем информацию в основной базе данных. Затем
записываем в базу данных. Все, что нельзя удалить, помещаем
# обратно в файл очереди удаления. Файл перезаписывается,
sub DisposeDelQueue{
&ReadMainDatabase;
foreach my Slogin (keys %{$queue->{account}}){
if ($queue->{account}->{Slogin}{status} eq "deleted"){
unless (exists $maindb->{account}->{$login}){
warn " Could not find Slogin in $maindata\n";
next;
}
$maindb->{account}->{$login}{status} = "deleted";
$maindb->{account}->{$login}{deletion_date} = time;
delete $queue->{account}->{$login};
next;
}
&WriteMainDatabase;
# все, что сейчас осталось в Sqjjeue, - это учетные записи,
# которые нельзя удалить
open(DEL,">".$accountdir.$delqueue) or die "Unable to open ".
$accountdir.$delqueue.":$!\n";
if (scalar keys %{$queue->{account}}){
print DEL XMLout(&TransformForWrite($queue), rootname => undef);
}
close(DEL); }
sub ReadMainDatabase{
open(MAIN,$accountdir.$maindata) or
die "Unable to open ".$accountdir.$maindata.":$!\n";
read (MAIN, $dbcontents, -s MAIN);
close(MAIN); $maindb = XMLin("<maindb>".Sdbcontents. "</maindb>",
keyattr => ["login"]); }
sub WriteMainDatabase{
# замечание: было бы «гораздо безопаснее* записывать данные
# сначала во временный файл и только если они были записаны
# успешно, записывать их окончательно open(MAIN,">".
$accountdir.Smaindata) or
die "Unable to open ".$accountdir.$maindata.":$!\n";
print MAIN XMLout(&TransformForWrite($maindb),
rootname => undef); close(MAIN); }
Можно написать еще множество сценариев. Например, мы могли бы применять сценарии, осуществляющие экспорт данных и проверку согласованности. В частности, совпадает ли домашний каталог пользователя с типом учетной записи из основной базы данных? Входит ли пользователь в нужную группу? Нам не хватит места, чтобы рассмотреть весь спектр таких программ, поэтому завершим этот раздел небольшим примером экспортирования данных. Речь уже шла о том, что хотелось бы завести отдельные списки рассылки для пользователей различного типа. В следующем примере из основной базы данных считываются данные и создается набор файлов, содержащих имена пользователей (по одному файлу для каждого типа пользователей):
use Account; И только чтобы найти файлы use XML::Simple:
&InitAccount;
SReadMainDatabase:
&WriteFiles:
open(MAIN,Saccountdir.Smaindata) or
die "Unauie to open ".Saccountdir.$maindata "-$'\r";
read (MAIN, Sdbcontents. -s MAIN);
ciose(MAIN): Smaindb = XMLin("<maindb>
".Sdbcontents." /maincm>",
keyattr -> [""]):
}
обходим в цикле списки, собираем списки учетных записей
определенного типа и сохраняем им в хэше списков. Затем
записываем содержимое каждого ключа в отдельный файл.
sub WriteFiles {
foreach my Saccount (@{$niaindb->{account}}){
next if $account->{status}
eq "deleted"; push(@{$types{$account->{type}}},
$account->{login}); }
foreach $type (keys %types){
open(OUT,">".Smalllists.Stype) or die "Unable to write to
".Saccountdir.$maillists.$type.": $!\n";
print OUT ]0in("\n",sort @{$types{$type}})."\n"; close(OUT);
}
}
Если посмотреть в каталог списков рассылки, то можно увидеть:
> dir
faculty staff
Каждый из этих файлов содержит соответствующий список учетных записей пользователей.
Система учетных записей. Заключение
Рассмотрев все четыре компонента системы учетных записей, подведем итоги и поговорим о том, что было пропущено (в узком, а не в широком смысле):
Проверка ошибок
В нашей демонстрационной программе выполняется проверка лишь небольшого числа ошибок. Любая уважающая себя система учетных записей увеличивается на 40-50% в объеме из-за проверки ошибок на каждом шаге своего выполнения.
Масштабируемость
Наша программа, скорее всего, сможет работать на мелких и средних системах. Но каждый раз, когда встречается фраза «прочитать весь файл в память», это должно звучать для вас предупреждением.
Чтобы повысить масштабируемость, нужно по крайней мере изменить способ получения и хранилище данных. Модуль XML : : Twig Мишеля Родригеса (Michel Rodriguez) может разрешить эту проблему, т. к. он работает с большими XML-документами, не считывая их при этом целиком в память.
Безопасность
Это относится к самому первому элементу списка выводов - проверке ошибок. Помимо таких громадных дыр, в смысле безопасности, как хранение паролей открытым текстом, мы также не выполняем никаких других проверок. Нет даже попыток убедиться, что используемым источникам данных, например, файлам очередей, можно доверять. Стоит добавить еще 20-30% кода, чтобы позаботиться о таких моментах.
Многопользовательская среда
В коде не предусмотрена возможность одновременной работы нескольких пользователей или даже нескольких сценариев. И это, вероятно, самый большой недочет созданной программы. Если одновременно запустить один сценарий, добавляющий учетные записи, и другой, дописывающий учетные записи в очередь, то вероятность повредить или потерять данные будет очень велика. Это настолько важная тема, что ее стоит обсудить перед тем, как завершить этот раздел.
Один из способов разобраться в многопользовательской среде с работой - добавить блокировку файлов. Блокировка позволяет нескольким сценариям действовать одновременно. Если сценарий собирается читать или писать в файл, он может попытаться сначала файл заблокировать. Если это возможно, значит, с файлом можно работать. Если его заблокировать нельзя (потому что другой сценарий использует этот файл), то сценарий знает, что запрещено выполнять операции, которые могут повредить данные. С блокировкой и многопользовательской работой связаны гораздо более серьезные сложности; обратитесь к любой информации по операционным или распределенным системам. Серьезные проблемы могут возникнуть при работе с файлами, расположенными на сетевых файловых системах, где может и не быть хорошего механизма блокировки. Вот несколько советов, которые могут вам пригодиться, если вы коснетесь этой темы при использовании Perl.
Существуют мудрые способы уходить от проблем. Мой любимый способ - использовать программу lockfile, входящую в состав популярной программы фильтрации почты procmail, которую можно найти на
http://www.procmail.org. Процедура установки procmail принимает усиленные меры, чтобы определить безопасные стратегии блокировки для используемой файловой системы, lockfile делает именно то, что можно ожидать, глядя на ее название, скрывая при этом основные сложности.
Если вы не хотите применять внешнюю программу, существует масса модулей, выполняющих блокировку. Например, File :Flock Дэвида Мюир Шарнофа (David Muir Sharnoff), Fi ±e: : LuckD i; из книги «Perl Cookbook» («Perl: Библиотека программиста») Тома Крис тиансена (Tom Christiansen) и Натана Торкингтона (Nathan Tor-kington) (O'Reilly) и его версия для Win95/98 Вильяма Херейры (William Herrera) под названием File: : FIockDiг, File: : Lock Кеннета Альбановски (Kenneth Albanowski), File : Lockf Поля Хенсона (Paul Henson) и Lockfile: :Simple Рафаеля Манфреди (Raphael Manfredi). В основном, они отличаются интерфейсом, хотя File: : FIockDi г и Lockf ile:: Simple пытаются выполнять блокировку, не используя функцию f lock() из Perl. Они могут быть полезными на таких платформах, как MacOS, где эта функция не поддерживается. Осмотритесь и выбирайте тот модуль, который больше всего вам подходит.
Блокировку проще всего выполнить правильно, если не забыть заблокировать файл, перед тем как изменять данные (или считывать данные, которые могли измениться), и снимать блокировку только после того, как убедитесь, что данные были записаны (например, после того как файл будет закрыт). Подробную информацию по этой теме можно найти в упомянутой уже «книге рецептов», в списке часто задаваемых вопросов Perl FAQ, в документации по функции f lock() из модуля DB_File и из документации по Perl.
Мы завершаем наш разговор об администрировании пользователей и о том, как можно перевести эти операции на другой уровень, применив подход архитектора. В этой главе мы уделили особое внимание началу и концу жизненного цикла учетной записи. В следующей главе мы поговорим о том, что делают пользователи между этими двумя моментами.