Xpoint
   [напомнить пароль]

Методы распределения доступа

Метки: анализ и проектирование, распределение доступа
2005-07-19 11:10:26 [обр] Thirteensmay(0/157)[досье]
Здравствуйте !, Хотелось бы услышать советы/опыт по теме:
Имеем пользователей и скрипты которыми они пользуются, необходимо распределять доступ к скриптам.
(все крутится до кучи, есть идентификация, сессии, таблицы пользователей и скриптов и пр.)
Немогу окончательно определиться с методом распределения, склоняюсь к повторению модели NTFS,
(т.е. определить группы пользователей, выстроить иерархию скриптов и устанавливать разрешения
на узлы этой иерархии, либо включать наследование), что впрочем 'слегка' жирновато ;)
Остальные модели либо не обеспечивают гибкость, либо тяжелы в администрировании (правда проще).
Например: Задать уровни доступа пользователей и скриптов и сравнивать, если уровень пользователя
не дотягивает до уровня скрипта - рубить., для тонкой настройки в этом случае предусмотреть
определение 'списков разрешенных пользователей' для необходимых скриптов.
Пока считаю это лучшей альтернативой, вот только администрирование немного хромает:
'списки разрешенных пользователей' распухают да и работать с ними нудно.
P.S. Задача того вроде стоит: несколько сотен скриптов и пользователей в несколько раз больше.
Типично: Определить доступ для 15 пользователей на 25 скриптов.
Ну поругайте чтоли... ;)
спустя 2 часа 8 минут [обр] Андрей Новиков(0/1242)[досье]

Права надо давать не к с криптам, а к действиям:

  • Право — действие
  • Роль — набор разрешенных действий

Иерархию лучше выстраивать не у скриптов (право по сути атомарно), а у ролей.

спустя 22 минуты [обр] Андрей Брайнин(0/127)[досье]

Мне непонятно почему объектом авторизации в вашей модели является скрипт? Скрипт, на мой взгляд, - всего лишь интерфейс для отображения данных или операциями на ними. Посему разграничивать доступ лучше не на уровне скриптов, а на уровне данных. Реализация разграничения зависит о ваших требований. По личному опыту скажу, что в большинстве случаев достаточно ролевого разграничения, при котором то или иное право над объектом авторизации (например просмотр, изменение, удаление) выдается (разрешается/запрещается) одной/нескольким ролям. Роль - понятие организационное. В зависимости от условий задачи его можно понимать как набор пользователей, объединенных по какому-либо признаку (например "Сотрудник Web-департамента" - может изменять новости, добавлять/изменять товары в каталоге и т.п.) так и как набор прав (например "Редактор новостей" - может изменять/удалять новости, "Менеджер товаров" - может управлять товарами). Один пользователь может входить в одну или несколько ролей.


использовать модель а-ля NTFS на мой взгляд жирновато, т.к. велик объем runtime-вычислений связанных с учетом иерархии групп/пользователей и наследованием прав от родительских объектов, что при использовании скриптовых языков может повлиять на производительность приложения.

спустя 1 час 9 минут [обр] Thirteensmay(0/157)[досье]
Как бы нам друг друга понять ? ;)
Данные сами по себе - ничто, вот информация - другое дело, но информация это результат
обработки данных (именно ОБРАБОТКИ), а вот обработка - це скрипты !
Предлагаемая Вами 'Роль' - ничто иное как набор разрешенных действий (обработок) - т.е. скриптов !
и сводится в конечном случае к группам пользователей (а-ля NTFS).
Или Вы предлагаете за счет введения Роли както упростить эту модель ? Как ?
спустя 1 час 24 минуты [обр] Андрей Брайнин(0/127)[досье]
мы говорим на разных языках. посему концептуального разговора, увы, не получится.
в ваших терминах модель а-ля NTFS вполне пригодна. без вложенния групп в группу и без наследования прав от скриптов, расположенных выше по иерархии (для экономии)
P.S. просили поругать - ругаю за "скрипты" как объекты авторизации :)
это все равно что разграничивать доступ к различным exe-программам, работающим с файловой системой, вместо разграничения доступа к объектам файловой системы на уровне ОС.
спустя 9 часов [обр] Thirteensmay(0/157)[досье]
Ну я конечно понимаю что разграничивать доступ к 'обьектам' более правильно, вот только к каким и как ?
эти обьекты в перспективе появиться могут в любом виде и работать с ними будут скрипты, я не собираюсь ограничивать
конечного программера в этом плане. Так что со скриптами - проще.
А вообще вот чего я надумал на основе Вашей идеи 'Роли': (спасибо ;)
Есть понятие 'Роль' - список разрешенных скриптов,
у кажого пользователя есть 'Роль' (либо несколько),
один скрипт может входить в несколько ролей.
Получается эффективно с точки зрения производительности: фактически для определения возможности доступа пользователя
к запрашиваемому действию (скрипту), необходимо обьединить массивы его ролей в один,
и посмотреть есть ли там этот скрипт - и все ! Администрировать тоже вроде несложно, всего 2 формы - 'Пользователи-Роли' и 'Роли-Скрипты' хотя тут еще стоит подумать...
А ?
спустя 9 часов [обр] Андрей Брайнин(0/127)[досье]
Ну я конечно понимаю что разграничивать доступ к 'обьектам' более правильно, вот только к каким и как?

вот тут сложно говорить конкретно, т.к. непонятно какие подходы вы используете при разработке приложения.
рассказ о различнных подходах занял бы много времени.
я имеел в виду информационные объекты. например объект "Новость", объект "Продукт" или объект "Новости".
как? - это собственно и есть решение вашей задачи :)

эти обьекты в перспективе появиться могут в любом виде и работать с ними будут скрипты. 
...
Так что со скриптами - проще.

а если с одним и тем же объектом будет работать несколько скриптов? а если программер напишет скрипт, который будет использовать ваши объекты и не будет заботиться о проверке прав?
на мой взгляд со скриптами проще только в том случае, когда скрипты написаны методом водопада.
т.е. в том случае, когда кроме скриптов и зацепиться не за что.
в остальных случаях разграничение доступа лучше делать на более низком уровне (например, в функциях доступа к данным при процедурном программировании или, при объектно-ориентированном, в методах доступа к данным (защищаемому объекту) и/или в методах самого защищаемого объекта)

... 'Роли-Скрипты' хотя тут еще стоит подумать...
А ?

ну пусть будет так, если вы находите это удобным.

спустя 30 минут [обр] Thirteensmay(0/157)[досье]
Ну все правильно... ;)
Еще раз повторюсь - я не знаю какие обьекты будут в перспективе обрабатываться, как они будут
устроены и прочее (малоли чего со временем понадобиться и как это будет реализовано).
Конечно, несколько скриптов смогут работать с одним обьектом - так оно и будет.
Вот только не заботиться о проверке прав - не получиться (это будет на уровне системы).
Программер уровня бизнес-логики в любом случае работает на основе следующего шаблона:
 #/usr/bin/perl
 header(script_id);
   print "Hello World !";
 footer();
где: в header(); - заложена вся система (идентификация, визуализация, сессии и пр.),
в т.ч. проверка прав доступа, если нет, то редирект, так что его "Hello World !"
будет выполняться только если разрешат.
спустя 1 день 4 часа [обр] Thirteensmay(0/157)[досье]
Да, вот еще что:
Есть у меня там интерфейс к БД, ничего особенного за исключением встроенного механизма
опять таки ;), распределения доступа. Так вот, функция коннекта может вызываться как стандартно,
т.е. с указанием всех параметров, так и без оных вообще. В этом случае интерфейс определяет
идентификатор текущего пользователя и устанавливает такой коннект какой нужен.
Соответственно имеется список доступных коннектов (логин, пароль, тип БД, хост, и пр.)
Естественно требуется привязать его к общему методу распределения (см. выше)
Вопрос к чему его привязать ? Первое что приходит на ум - к 'Роли', но в этом случае
возможна неоднозначность т.к. у одного пользователя может быть несколько ролей.
К остальным обьектам - тоже нелогично/не без последствий,
вводить дополнительные сущности нежелательно (и так слава богу)... А ?
спустя 2 часа [обр] GRAy(0/259)[досье]
Значит скрипты должны знать какими коннектами им можно пользоваться, а какими нельзя. Не получится разграничить права доступа по двум иерархиям (скрипты и БД) - никак вам не избавиться от неоднозначностей.
спустя 18 часов [обр] Thirteensmay(0/157)[досье]
Кроме как ввести связку "скрипт-коннект" на ум ничего не приходит...
спустя 11 дней [обр] Ярослав Ворожко[досье]
 #/usr/bin/perl
 header(script_id);
   print "Hello World !";
 footer();
разве для "Hello World !" програмер не может закоментировать header(script_id);
спустя 2 часа 3 минуты [обр] Алексей Севрюков(2/1280)[досье]
Ярослав Ворожко[досье] Вы о чем? Прочитайте тему с самого начала и повнимательнее.
спустя 1 год 5 месяцев [обр] ask[досье]

Хотел бы задать вопрос в тему.
Вот рекомендуется использовать разграничение прав доступа именно к данным, а не к скриптам.
Например у нас есть таблица со списком юзеров (в ней явки-пароли, контакты и т.п. инфа).
Юзер совершает следующие шаги:
1.Заходит на сайт.
2.Логинится.
3.Заходит на страницу просмотра персональной информации о себе.
4.Он может являться админом.

Вопрос: как разграничивать в таком случае права доступа к данным, если во всех случаях нужно вычитывать что-то из таблицы юзеров, где содержится конфиденциальная информация?

спустя 1 час 25 минут [обр] GRAy(0/259)[досье]
Админ - это всего-лишь роль с повышенными привилегиями, но никак не с абсолютными. Абсолютными привилегиями должна обладать только сама система (например System, или root). Соотв. некоторые особо критичные данные должны быть доступны только системе и вход под этим пользователем должен быть невозможен. Доступ к данным должен осуществлятся через API, который работает на уровне системы и всегда проверяет можно или нельзя осуществить чтение/запись/изменение/удаление данному пользователю.
спустя 1 час 26 минут [обр] ask[досье]
GRAy[досье]
Вы не поняли вопрос, который заключается в том, КАК разграничивать доступ разных ролей к одной и той же таблице с данными (пример - таблица юзверей), если одна роль может читать только ники юзеров, другая - только полную инфу о себе, третья - читать всё, пятая - редактировать некоторые поля у части записей и т.д.?
спустя 15 часов [обр] GRAy(0/259)[досье]
ask[досье] Вы не поняли ответ ;) Разграничить доступ таким образом можно только завернув все обращения к данным через API, который и будет отдавать только ту информацию на которую у данной роли есть права. Сам API работает на уровне системы, а все остальные пользователи и админы в том числе - через API.
спустя 21 минуту [обр] Thirteensmay(0/157)[досье]

ask[досье]
Я ввел понятие "уровня доступа" - у каждого скрипта в роли есть число определяющее этот уровень. Если скрипт доступен из разных ролей то соответственно у него могут быть и разные уровни доступа в этих ролях. Например: Скрипт "Регистрационная информация" доступен в ролях "Пользователи" и "Администраторы" - для пользователя уровень доступа - 1, для администратора - 2, и т.д. (по умолчанию - 1). Ядро системы передает уровень доступа текущего пользователя к текущему скрипту через переменную окружения, программист соответственно может определить зависимое поведение скрипта.

Кое что из документации:
Механизм распределения доступа основывается на понятии ролей пользователей, с расширением в виде уровней доступа. Схема механизма распределения доступа приведена на следующем рисунке:

Основополагающей в данной схеме является понятие роли. Роль - это набор разрешенных действий пользователя (инструментов/скриптов), т.е. фактически роль представляет собой массив идентификаторов доступных скриптов (инструментов). Каждый пользователь может иметь произвольный набор ролей, в свою очередь роль может иметь произвольный набор скриптов, т.е. каждый скрипт может входить в любое количество ролей. Механизм распределения доступа основывается на служебных таблицах USERRULES и RULESCRIPTS, а также дополнительно использует USERS и SCRIPTS. Исходными данными для определения возможности доступа служат идентификатор текущего пользователя и идентификатор текущего скрипта, которые вычисляются соответственно блоками идентификации и подключения функции header();.

Таблица USERRULES содержит определения ролей пользователей. Каждая запись представляет собой соответствие роли пользователю. Таким образом, совокупность всех записей с идентификатором одного пользователя представляет собой список (массив) его ролей.

Каждая запись таблицы RULESCRIPTS представляет собой описание доступности скрипта в роли, с указанием уровня доступа к этому скрипту. Понятие уровня доступа является дополнительным слоем в механизме распределения. С помощью ролей определяется возможность доступа, а с помощью уровней - его качество. Уровень доступа представляет собой числовой идентификатор, определяющий возможности пользования скриптом. Например: 1 - Чтение, 2 - Чтение и изменение данных скриптом, 3 - Административный (изменение кода скрипта). В отличие от ролей, работа с которыми осуществляется абсолютно прозрачно для конечного разработчика, обработка уровней доступа осуществляется непосредственно самим скриптом. Ядро системы лишь передает текущий уровень доступа в скрипт средствами переменной окружения $acslevel, далее, в случае необходимости, разработчик может определить зависимое поведение скрипта. Необходимо заметить, что возвращается максимальный уровень доступа, т.к. каждый пользователь может иметь несколько ролей, в каждой из которых, в свою очередь, может быть определен доступ с различными уровнями к одному и тому же скрипту. В идеале подразумевается, что каждый скрипт выполняет одно элементарное действие, в этом случае механизм уровней доступа во многом избыточен, однако в случае сложных с точки зрения доступа скриптов он может быть полезен. В текущей версии системы не один скрипт не использует данный механизм, несмотря на что в ядре он реализован полностью.

Общий алгоритм работы механизма распределения доступа (в составе функции header()) следующий: Блок идентификации определяет идентификатор текущего пользователя, блок подключения - идентификатор скрипта. Далее, из служебной таблицы USERS, извлекается состояние пользователя. Затем определяется список ролей пользователя, на основе которого, а также идентификатора скрипта, вычисляется максимальный уровень доступа. После выполнения данных предварительных действий принимается решение о запрете доступа - в случае если состояние пользователя - 0 (отключен), либо максимальный уровень доступа не определен (0). Если решение о запрете доступа не принято, то управление передается основному телу скрипта. В противном случае, функция header() формирует сообщение о запрете доступа, после чего из ее тела осуществляется вызов функции footer(), и окончание работы скрипта.

Подсистема доступа также включает в свой состав отдельный механизм фильтрации доступа по IP адресам: Служебная БД Heads содержит таблицу IPFILTER - список правил фильтрации. Правила проверяются по порядку их номеров, до первого подходящего по адресу, после чего найденное правило применяется. Разрешение/запрет доступа определяется флагом ACS. Адрес может быть указан не полностью, сравнение осуществляется путем нахождения указанной ADDRESS подстроки в IP адресе обратившегося клиента, что используется для определения подсетей.

Администрирование доступа удобно осуществлять с помощью специальных входящих в состав системы визуальных инструментов: "Администрирование пользователей" и "Администрирование ролей".

  1. Интегрирующие функции

header(); - Функция обеспечивающая интегрирование прикладного скрипта в единую систему. Данная функция формирует заголовок ответа сервера, визуализацию системной панели навигации, проводит идентификацию клиента, осуществляет контроль доступа, открывает "форму по умолчанию", и инициализирует множество системных переменных. В данной версии функция не имеет параметров. Ее вызов должен осуществляться прикладным скриптом до начала вывода им каких либо данных, с точки зрения безопасности предпочтительно это должна быть первая функция скрипта. В ходе своей работы, функция осуществляет следующую последовательность: 1) Обрабатывает входящие cookie, и инициализирует системный хеш %COOKIE вида $COOKIE{cookie_name} = cookie_value; 2) Определяет параметры вызывающего (интегрируемого) скрипта, на основе переменной CGI окружения SCRIPT_NAME, по данным служебной таблицы скриптов, устанавливаются переменные $spathname, $scriptid, $scriptname, $sparent, определяющие соответственно: путь текущего скрипта относительно корня системы, его идентификатор, наименование и родительский раздел. 3) Завершает процесс авторизации (в случае необходимости), устанавливает при этом флаг $startsession = 1; 4) Проводит идентификацию клиента на основе %COOKIE и переменных CGI окружения REMOTE_ADDR и HTTP_X_FORWARDED_FOR, устанавливает переменные $idinfo и $altidinfo определяющие текущую идентификационную информацию. В случае успеха идентификации, по служебной таблице сессий, устанавливает системные переменные $sessionid и $userid определяющие идентификатор сессии и текущего пользователя соответственно. В случае провала идентификации назначает новую, соответствующую гостю идентификационную информацию, и стартует новую сессию (уст. флаг $startsession). 5) Определяет дополнительные атрибуты текущего пользователя на основе его идентификатора по служебной таблице пользователей, устанавливает переменные $userlogin, $username, $usergroup, $ustate, означающие соответственно: имя входа текущего пользователя в систему, его полное имя, его группу, и состояние. 6) Определяет по служебной таблице ролей список ролей текущего пользователя, устанавливает переменную $curules хранящую перечисление идентификаторов этих ролей через запятую. 7) Вычисляет максимально возможный уровень доступа текущего пользователя к текущему скрипту, на основе списка его ролей (максимально возможный т.к. в разных ролях пользователя его уровень доступа к одному и тому же скрипту может быть разным), устанавливает соответствующую системную переменную $acslevel вычисленным значением. 8) Принимает решение о запрете доступа, по принципу: если состояние пользователя, или его уровень доступа к текущему скрипту равны нулю - доступ отклоняется (устанавливается флаг $acsden = 1). 9) Проводит логирование доступа в системном журнале. 10) Осуществляет построение ветви текущего пути (родства) скрипта в едином дереве скриптов. Формирует массив @cbranch, заполняет его парами значений: $scriptname, $spathname (имя и путь скрипта отн. корня системы), где первая пара - родительский скрипт наивысшего уровня, последующие пары - его потомки, последняя пара - текущий скрипт. 11) Формирует массивы навигации - массив скриптов (@childscript) и массив разделов (@childfolder), дочерних относительно текущего скрипта. Заполняет эти массивы тройками значений определяющими соответствующие скрипты (идентификатор скрипта, наименование и путь). Формирование массивов осуществляется с учетом доступа к скриптам - недоступные для текущего пользователя отфильтровываются. 12) По служебной таблице избранного формирует массив избранного (@userfavor) текущего пользователя, заполняет его тройками значений аналогично массивам навигации. 13) Выводит начало HTTP/HTML ответа, включительно по тег <body>, производит это при помощи функции htheader() библиотеки HLib. 14) Выводит (визуализирует) системную панель, выводит ветвь родства, навигацию и избранное, формирует ссылки на системные инструменты авторизации, справочной системы и пр., открывает форму по умолчанию (defform), открывает тег ячейки для вывода контента прикладного скрипта (100% ширины и высоты). 15) Проверяет запрет доступа по флагу $acsden, в случае наличия такового, выводит сообщение о запрете доступа и передает управление функции footer(), в которой осуществляется формирование окончания и вывод соответствующей визуализации, после чего осуществляется системный вызов exit(), т.е. управление прикладному скрипту не передается. Если флаг запрета доступа ранее не установлен, функция завершает свою работу и передает управление прикладному скрипту. Естественно, все вышеперечисленные переменные доступны после выполнения функции в прикладном скрипте через разименование. Разрешено их чтение, модификация может привести к краху системы и порчи системной БД.

Формирование массивов навигации (@childscript, @childfolder), а соответственно и вся связанная с ними визуализация списков ссылок и разделов, по умолчанию осуществляется в родственно-иерархическом, алфавитном порядке наименований скриптов. Начиная с версии Heads 0.36a имеется возможность определения произвольного порядка вывода, что эффективно с точки зрения повышения удобства использования путем группировки необходимых пунктов. Более того, возможно полное переопределение состава раздела, что может с успехом использоваться например для формирования сводных разделов и пр. Эта функциональность основывается на введенной в служебную БД таблице forder, имеющей 2 поля следующего формата: fld - int32, scripts - varchar255. Эти поля описывают определяемый раздел и состав его скриптов. Поле fld содержит идентификатор раздела, scripts - перечисление идентификаторов его скриптов через запятую, в необходимом порядке вывода. В случае возникновения необходимости определения особого раздела такого рода, достаточно разместить соответствующее описание в forder, при этом используемый по умолчанию родственно-иерархическо-алфавитный порядок формирования будет перекрыт.

спустя 2 часа 7 минут [обр] ask[досье]

Thirteensmay[досье]
Получается, что ситуацию, когда зашедший на страницу со списком записей юзер может

1...просматривать только запись о себе;
2...просматривать и редактировать только запись о себе;
3...просматривать все записи на странице и редактировать только запись о себе;
4...просматривать и редактировать все записи, кроме содержащей информацию о нем;
5...просматривать только созданные им записи;
6...просматривать и редактировать только созданные им записи;
7...просматривать все записи и редактировать только созданные им;
9...просматривать и редактировать все записи,

... Вы решаете с помощью "уровня доступа"?

Но тогда нужно в каждом скрипте ставить case и, в зависимоти от "уровня доступа", показывать/не показывать что-либо (записи, контролы и т.д.)?

спустя 32 минуты [обр] ask[досье]

Хотелось бы также обсудить способы разруливания следующих ситуаций:

  1. Разграничене доступа к разным типам записей:
    1. Содержащим информацию о текущем пользователе (например, контактная информация)
    2. Созданных текущим пользователем (например, статья или контактная информация о другом пользователе)

В каждом из этих случаях, чтобы разрешить пользователю доступ к записи нужно анализировать разные поля таблицы (userId или createdBy) и уже в каждом скрипте отдельно принимать решение.

  1. Назначение привилегий пользователем себе или своей группе. Может возникнуть ситуация, когда пользователь случайно запретит самому себе доступ к приложению.

Решение - затычка в коде, запрещающая это действие для своего id?

  1. Удаление пользователя самим собой.

Решение - затычка в коде, запрещающая это действие для своего id?

спустя 3 часа 41 минуту [обр] Thirteensmay(0/157)[досье]

По первому посту:

Я не создаю себе таких ситуаций. Не надо делать мегаскриптов, один скрипт - одно действие (в идеале), на практике - от одного до 3, т.е. в крайнем случае 3 уровня доступа, что крайне редко.
В вашем случае имеем 3 отдельных инструмента (скрипта): 1 - Записи о себе, 2 - Дочерние записи, 3 - Все записи. Для каждого 2 уровня доступа - просмотр и редактирование. Нет, ну можно конечно залепить все и в один, никто не запрещает, будет хоть 100 уровней, но голова же поломается, причем не только у разработчика но и у пользователя. Естественно что "тонкие" моменты (типа "кроме чего то") реализуются отдельно (вне mainstream системы распределения, но с ее использованием). Понятия "созданные им", "кроме себя", "свои" и пр. в общую систему не ложатся, т.к. это частные данные и она о них ничего не знает. Система распределения оперирует понятиями действий (доступ, чтение, запись, изменение, пр.), а не данными. Данными оперирует инструмент (скрипт), только он о них знает. Но т.к. система распределения рулит скриптами, то в случае "один скрипт - одно действие" получается идеально, другое дело что не всегда удобно и часто громоздко, отсюда и компромиссы - чтото разделением, чтото уровнями доступа.

По второму посту:

1 - Да. Но не обязательно в каждом отдельном скрипте, если удается выделить общие моменты то надо объединять. Например: if ($acslevel > 1) get_current_user_extendedinfo(); else get_current_user_info();
2 - Если пользователю разрешено запрещать себе доступ (он имеет доступ к этой операции/скрипту), значит он может это сделать, затычку тут ставить "не красиво", хотя если пользователь особо глуп то вполне возможно. Но в любом случае раз он может закрыть доступ то значит он должен иметь право потом и открыть его - реализации тут могут быть разные, взять хотя бы идею с "хозяином-владельцем" который в любом случае как угодно может рулить доступом к "своим" ресурсам, т.е. даже если они закрыты их можно открыть с помощью стороннего инструмента.
3 - Пусть будет такая возможность, но также необходимо предусмотреть восстановление.

спустя 3 часа 2 минуты [обр] ask[досье]
Thirteensmay[досье]
  1. Согласен
  2. Пример: админ запретит себе доступ к странице назначения прав доступа. Смешно, но вполне реальная ситуация, т.к. он имеет на это право. Как разруливать? Заплатками?
  3. Аналогично. По поводу восстановления - это уже вопрос журналирования/версионирования/откатов. А в реале - он УДАЛИТ себя, после чего естесственно не сможет войти в систему и что-либо восстановить. А оставлять на такоие случаи потайные дверцы чревато, ибо... сами понимаете.
спустя 14 часов [обр] Thirteensmay(0/157)[досье]
Ну вариантов собственно говоря только два: Затычки и потайные дверцы - решайте на свой вкус. Можно еще например завести superuser'а и в "аварийных" ситуациях пользоваться им.
Powered by POEM™ Engine Copyright © 2002-2005