- Скажите я имею право?
- Да, имеете.
- А я могу?..
- Нет, не можете.
Посвящается всем тем, кто хоть раз в жизни собирал вручную изменённые RLS при обновлении конфигурации...
Тестировалось на конфигурации Бухгалтерия Предприятия 3.0.127.49.
Одна из основных задач при настройке прав доступа - это обеспечить, чтобы какие-то пользователи видели какие-то данные, а другие их не видели. Например, чтобы пользователи головной организации видели все документы, а пользователи филиала - только документы филиала. Решаются эти задачи с помощью назначения соответствующих ролей и настройки RLS. Проблема же заключается в том, что рано или поздно вся эта грамотно настроенная и доказавшая свою состоятельность на практике система прав доступа, мягко говоря, идёт к коту под хвост. Причины могут быть самые различные. Но, пожалуй, самая частая связана с обновлением конфигураций. Где-то в правах что-то добавилось типовое, где-то что перетёрлось нетиповое - и всё, начинается "ужас-ужас! тот кто не надо увидел то, что не надо!". Разумеется, грамотное проектирование доработок конфигурации значительно уменьшает подобные риски (а иногда и вовсе их исключает), но в любом случае, перед установкой новой конфигурации на рабочий сервер, обычно хочется убедиться, что права "не съехали".
Сразу оговорюсь, предлагаемый тут вариант - это не универсальное средство, а всего лишь ещё один дополнительный эшелон для защиты от ошибки.
Итак, приступим. Будем решать следующую упрощённую (но при этом вполне реальную) задачу: в организации применяется политика, согласно которой пользователи головной организации должны иметь полный доступ ко всем документам, а пользователи филиала - только доступ к документам филиала. Нам нужно проверить выполняется ли эта политика на практике.
Предлагаемый вариант решения такой: раз нужно проверить выполняется ли на практике, значит и проверять будем тоже на практике. Т.е. возьмём какого-нибудь пользователя головной организации (с достаточными правами, но без полных прав; например, это может быть ГБ или зам.ГБ), и попробуем под ним прочитать/записать/создать документы и головной организации, и филиала. И это должно у него получиться. А потом возьмём пользователя филиала, и попробуем под ним прочитать/записать/создать документы филиала (это у него должно получиться!), а также прочитать/записать/создать документы головной организации (а это не должно у него получиться!).
Как театр начинается с вешалки, так и файл тестов xddTestRunner начинается с процедуры ЗаполнитьНаборТестов(). В ней мы определяем, что наш тестовый пользователь с именем "ПользовательГоловной" принадлежит к головной организации, а тестовый пользователь "ПользовательФилиала" - к филиалу. Далее перебираем все документы в метаданных, пропускаем те из них, которые начинаются с "Удалить", и создаём тесты. В параметрах теста передаем имя проверяемой таблицы (например, "Документ.СчетНаОплатуПокупателю"), вид проверяемых прав ("чтение"/"запись"/"создание") и фильтр для отбора какого-то конкретного документа для проверки есть к нему доступ у пользователя или нет (в нашем случае это будет фильтр по Организации).
У данного файла-теста есть всего два метода:
- Процедура ТестДолжен_ПроверитьЕстьДоступ() - для проверки что у пользователя есть доступ к объектам с указанным видом прав
- и Процедура ТестДолжен_ПроверитьНетДоступа() - для проверки, что у пользователя нет доступа.
Методы тривиальны. На входе получают параметры теста (см. выше), а внутри с помощью вызова функции ПолучитьСсылкуНаОбъект() получают ссылку на какой-нибудь объект информационной базы и через процедуры ПроверитьЕстьДоступКДанным()/ПроверитьНетДоступаКДанным() проверяют есть ли у пользователя доступ к этому объекту.
Функция ПолучитьСсылкуНаОбъект() тоже весьма незамысловата. Она возвращает первую попавшуюся ссылку на объект БД из таблицы, переданной в параметре ИмяТаблицы (например, "Документ.СчетНаОплатуПокупателю") и с отборами, переданными в структуре Фильтр (в текущей версии функции работают только сравнения на равенство).
Единственный момент, над которым пришлось задуматься - это использовать или не использовать в функции ПолучитьСсылкуНаОбъект() в запросе ключевое слово "РАЗРЕШЕННЫЕ". В текущем варианте решил не использовать. Дело в том, что если использовать "РАЗРЕШЕННЫЕ", то в процедуре ТестДолжен_ПроверитьЕстьДоступ() при вызове
СсылкаНаОбъект = ПолучитьСсылкуНаОбъект(ИмяТаблицы, Фильтр);
вернётся пустое значение. Как-будто в базе нет объектов такого типа. Хотя на самом деле они могут там быть, просто у пользователя нет к ним доступа. Решил, что всё-так не буду писать "РАЗРЕШЕННЫЕ". В связи с этим в методе ТестДолжен_ПроверитьНетДоступа() вызов функции ПолучитьСсылкуНаОбъект() пришлось обернуть в Попытка ... Исключение (т.к. запрос без слова "РАЗРЕШЕННЫЕ" генерирует исключение "Нет прав доступа" в случае, если у пользователя нет прав на чтение данных из БД):
Попытка
СсылкаНаОбъект = ПолучитьСсылкуНаОбъект(ИмяТаблицы, Фильтр);
Исключение
// всё ок, не смогли получить
Возврат;
КонецПопытки;
Вообще, я исхожу из того, что пусть лучше будут ложноположительные срабатывания, чем ложноотрицательные. Пусть лучше тест покажет, что есть ошибка там, где её на самом деле нет, чем покажет, что ошибки нет там, где она на самом деле есть.
Ну и, наконец, сами процедуры проверки наличия или отсутствия прав доступа к конкретному объекту БД:
- Проверка наличия доступа на чтение осуществляется через вызов ПолучитьОбъект() для ссылки на объект БД.
- Проверка наличия доступа на запись осуществляется через ПолучитьОбъект() + Записать().
- Проверка наличия доступа на создание нового осуществляется через копирование существующего объекта, ситуативного дозаполнения (например, установки даты документа) и записи.
Процедура ПроверитьЕстьДоступКДанным(СсылкаНаОбъект, ВидПрав)
Если ВидПрав = "чтение" Тогда
ОбъектБД = СсылкаНаОбъект.ПолучитьОбъект();
ИначеЕсли ВидПрав = "запись" Тогда
ОбъектБД = СсылкаНаОбъект.ПолучитьОбъект();
ОбъектБД.Записать();
ИначеЕсли ВидПрав = "создание" Тогда
НовыйОбъект = СсылкаНаОбъект.Скопировать();
Если Метаданные.Документы.Содержит(НовыйОбъект.Метаданные()) Тогда
НовыйОбъект.Дата = ТекущаяДатаСеанса(); //у некоторых документов почему-то не заполняется автоматически дата, поэтому заполним её явно
КонецЕсли;
НовыйОбъект.Записать();
КонецЕсли;
КонецПроцедуры
Проверка на отсутствие доступа сделана через "инверсию" проверки на наличие доступа. Т.е. вызываем процедуру ПроверитьЕстьДоступКДанным() и смотрим. Если было вызвано исключение, считаем, что это случилось из-за того, что у пользователя нет прав доступа. Если же исключения не было, значит ПроверитьЕстьДоступКДанным() отработала без ошибок и у пользователя есть доступ к данным.
Процедура ПроверитьНетДоступаКДанным(СсылкаНаОбъект, ВидПрав);
БылоИсключение = Ложь;
Попытка
ПроверитьЕстьДоступКДанным(СсылкаНаОбъект, ВидПрав);
Исключение
БылоИсключение = Истина;
КонецПопытки;
Если НЕ БылоИсключение Тогда
ВызватьИсключение "У пользователя есть права на """ + ВидПрав + """, хотя их быть не должно!";
КонецЕсли;
КонецПроцедуры
Про файл теста вроде бы и всё.
Теперь про методику тестирования.
Этап 1. Подготавливаем пользователя головной организации (в нашем примере, это пользователь с именем "ПользовательГоловной", но рекомендуется проверять под каким-то реальным пользователем!), заходим под ним в базу, запускаем тест в xddTestRunner, анализируем результаты.
Этап 2. Подготавливаем пользователя филиала, заходим под ним в базу (в нашем примере, это пользователь с именем "ПользовательФилиала", но рекомендуется проверять под каким-то реальным пользователем!), заходим под ним в базу, запускаем тест в xddTestRunner, анализируем результаты.
Как видите, тут тоже без неожиданностей и пасхалочек. Единственный момент, который надо прояснить - это как "подготовить пользователя". Дело в том, что для корректной работы xddTestRunner и этого теста необходимо учесть определённые моменты, такие как:
- наличие у пользователей ролей Администрирование (не путать с "полными правами"!) и ИнтерактивноеОткрытиеВнешнихОтчетовИОбработок - иначе xddTestRunner просто не запустится,
- отключение у пользователя предупреждений об опасных действиях,
- отключение у пользователя доменной аутентификации и установка ему известного вам пароля,
- перенос/отключение даты запрета редактирования для этого пользователя,
- обновление нумерации объектов (да-да! чтобы не получилось так, что запустили тест, прождали полчаса-час пока он выполняется, а в конце увидели протокол, в котором написано, что значительная часть тестов упала, потому что "не удалось записать, т.к. номер объекта не уникальный"),
- и т.п.
Справиться со всем этим (точнее, почти со всем) поможет обработка, на форме которой есть реквизиты Логин (тип = строка), Пароль (тип = строка), ИмяТестовогоСервера (тип = строка; нужна чтобы случайно не запустить на рабочем сервере!), УстановитьПароль (тип = булево; признак нужно или не нужно устанавливать пароль для пользователя), ОбновитьНумерацию (тип = булево; признак нужно или нет обновлять нумерацию объектов), и вызывающая следующую функцию:
Запускать это нужно, разумеется, под администратором.
Вот теперь совсем всё. Весь приведённый исходный код доступен под лицензией Mozilla Public Licence 2.0 (для совместимости с проектом Vanessa-ADD).
Ну и, конечно, пользуясь случаем, хочу ещё раз выразить благодарность всем разработчикам Vanessa-ADD. Отличный инструмент, выручает регулярно. Спасибо!