У пользователя отчёт пустой, у вас работает: находим недоступные поля за один проход
Иван Недомолков · Опубликовано:
Ситуация, ради которой этот текст: под вашими правами отчёт считается, под правами пользователя он формируется, закрывается и показывает ноль строк. Ошибки нет ни одной. Или наоборот, падает с сообщением про ненайденное поле, но только у некоторых людей.
Это один дефект в двух положениях. Компоновка молча выбрасывает поля, на которые у пользователя нет права просмотра, а какой из двух симптомов вы увидите, решает позиция выброшенного поля в схеме.
Ниже сначала инструмент, потом объяснение. Порядок такой намеренно: список недоступных полей нужен до того, как вы начнёте выдавать права по одному, иначе диагностика превращается в цикл “запустил, поймал сообщение, выдал право, запустил снова”.
Сначала отсечь ограничение записей
Первая версия, которую называют все, почти всегда неверна, а стоит проверка одну минуту. Зайдите в сеансе целевого пользователя и посчитайте строки в источниках отчёта напрямую, без компоновки:
// Проверка "виноват ли RLS": считаем то, что при этой версии обязано быть нулём.
// Запускать ОБЯЗАТЕЛЬНО в сеансе целевого пользователя, иначе смысла нет.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ КОЛИЧЕСТВО(*) КАК Строк, ""Сотрудники"" КАК Istochnik
|ИЗ Справочник.Сотрудники
|
|ОБЪЕДИНИТЬ ВСЕ
|
|ВЫБРАТЬ КОЛИЧЕСТВО(*), ""Начисления""
|ИЗ РегистрНакопления.Начисления";
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
Сообщить(Выборка.Istochnik + ": " + Выборка.Строк);
КонецЦикла;
Ненулевые числа означают, что данные пользователю видны и ограничение доступа на уровне записей ни при чём. В разобранном случае вышли тысячи сотрудников и десятки тысяч начислений.
Обратите внимание на форму проверки, она полезнее результата: мы посчитали то, что при популярной гипотезе обязано быть нулём. Одно число закрывает версию, на которой иначе застревают надолго. Чем ограничение записей занимается на самом деле и во что обходится, разобрано отдельно.
Инструмент: полный список недоступных полей
Идея простая. Берём поля, которые схема реально использует, берём поля, которые компоновка отдаёт доступными в сеансе целевого пользователя, и вычитаем одно из другого.
Смотреть надо шире, чем на наборы данных, иначе половина находок потеряется:
- поля наборов данных, включая участвующие только в связях;
- поля вычисляемых полей и выражений ресурсов;
- поля в отборах и параметрах настроек по умолчанию;
- поля структуры вариантов отчёта: строки, колонки, группировки.
Последний пункт и даёт молчаливый симптом, поэтому в коде ниже такие поля помечаются отдельно.
Код кладётся во внешнюю обработку. Запускать обязательно в сеансе целевого пользователя: под полными правами список выйдет пустым, потому что доступно будет всё.
// Аудит: какие поля схемы компоновки недоступны текущему пользователю.
// Возвращает соответствие "путь к данным -> где встретилось".
&НаСервере
Функция НедоступныеПоля(Схема)
Ispolzuemye = Новый Соответствие; // путь -> список мест
SobratNaboryDannyh(Схема.НаборыДанных, Ispolzuemye);
SobratNastroyki(Схема.НастройкиПоУмолчанию, Ispolzuemye);
Для Каждого Variant Из Схема.ВариантыНастроек Цикл
SobratNastroyki(Variant.Настройки, Ispolzuemye);
КонецЦикла;
// Что компоновка отдаёт доступным ИМЕННО В ЭТОМ СЕАНСЕ
Adres = ПоместитьВоВременноеХранилище(Схема, Новый УникальныйИдентификатор);
Компоновщик = Новый КомпоновщикНастроекКомпоновкиДанных;
Компоновщик.Инициализировать(
Новый ИсточникДоступныхНастроекКомпоновкиДанных(Adres));
Компоновщик.ЗагрузитьНастройки(Схема.НастройкиПоУмолчанию);
Dostupnye = Новый Соответствие;
SobratDostupnye(Компоновщик.Настройки.Выбор.ДоступныеПоляВыбора.Элементы, Dostupnye);
Rezultat = Новый Соответствие;
Для Каждого КЗ Из Ispolzuemye Цикл
Если Dostupnye[КЗ.Ключ] = Неопределено Тогда
Rezultat.Вставить(КЗ.Ключ, КЗ.Значение);
КонецЕсли;
КонецЦикла;
Возврат Rezultat;
КонецФункции
&НаСервере
Процедура SobratDostupnye(Элементы, Prinimatel)
Для Каждого Элемент Из Элементы Цикл
Prinimatel.Вставить(Строка(Элемент.Поле), Истина);
Если Элемент.Папка Тогда
// папки полей вложены, поэтому обход рекурсивный
SobratDostupnye(Элемент.Элементы, Prinimatel);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
&НаСервере
Процедура Otmetit(Prinimatel, Put, Mesto)
Если ПустаяСтрока(Put) Тогда
Возврат;
КонецЕсли;
Spisok = Prinimatel[Put];
Если Spisok = Неопределено Тогда
Spisok = Новый Массив;
Prinimatel.Вставить(Put, Spisok);
КонецЕсли;
Spisok.Добавить(Mesto);
КонецПроцедуры
&НаСервере
Процедура SobratNaboryDannyh(Наборы, Prinimatel)
Для Каждого Набор Из Наборы Цикл
Для Каждого Поле Из Набор.Поля Цикл
Otmetit(Prinimatel, Поле.ПутьКДанным, "набор данных " + Набор.Имя);
КонецЦикла;
Если ТипЗнч(Набор) = Тип("НаборДанныхОбъединениеСхемыКомпоновкиДанных") Тогда
SobratNaboryDannyh(Набор.Элементы, Prinimatel); // вложенные наборы
КонецЕсли;
КонецЦикла;
КонецПроцедуры
&НаСервере
Процедура SobratNastroyki(Настройки, Prinimatel)
Для Каждого Элемент Из Настройки.Отбор.Элементы Цикл
Если ТипЗнч(Элемент) = Тип("ЭлементОтбораКомпоновкиДанных") Тогда
Otmetit(Prinimatel, Строка(Элемент.ЛевоеЗначение), "отбор");
КонецЕсли;
КонецЦикла;
Для Каждого Поле Из Настройки.Выбор.Элементы Цикл
Если ТипЗнч(Поле) = Тип("ВыбранноеПолеКомпоновкиДанных") Тогда
Otmetit(Prinimatel, Строка(Поле.Поле), "выбранные поля");
КонецЕсли;
КонецЦикла;
OboytiStrukturu(Настройки.Структура, Prinimatel);
КонецПроцедуры
&НаСервере
Процедура OboytiStrukturu(Структура, Prinimatel)
Для Каждого Элемент Из Структура Цикл
Если ТипЗнч(Элемент) = Тип("ГруппировкаКомпоновкиДанных") Тогда
Для Каждого Поле Из Элемент.ПоляГруппировки.Элементы Цикл
Если ТипЗнч(Поле) = Тип("ПолеГруппировкиКомпоновкиДанных") Тогда
// вот эти поля и схлопывают отчёт молча
Otmetit(Prinimatel, Строка(Поле.Поле), "СТРУКТУРА (молчаливый риск)");
КонецЕсли;
КонецЦикла;
SobratNastroyki(Элемент, Prinimatel);
OboytiStrukturu(Элемент.Структура, Prinimatel);
ИначеЕсли ТипЗнч(Элемент) = Тип("ТаблицаКомпоновкиДанных") Тогда
OboytiStrukturu(Элемент.Строки, Prinimatel);
OboytiStrukturu(Элемент.Колонки, Prinimatel);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Схема берётся из макета отчёта:
Схема = Отчеты.ВашОтчёт.ПолучитьМакет("ОсновнаяСхемаКомпоновкиДанных");
Итог = НедоступныеПоля(Схема);
Для Каждого КЗ Из Итог Цикл
Сообщить(КЗ.Ключ + " <- " + СтрСоединить(КЗ.Значение, ", "));
КонецЦикла;
Это рабочий скелет, а не законченный инструмент. Вычисляемые поля и выражения ресурсов он не разбирает: там нужен разбор выражения, а не обход коллекции. Для большинства отчётов хватает, но если логика живёт в вычисляемых полях, добавьте их обход руками.
Как читать вывод
Строки с пометкой СТРУКТУРА чините первыми. Это поля из строк, колонок
и группировок: именно они схлопывают отчёт без единого сообщения. Остальные хотя бы
назовут себя ошибкой, когда вы запустите отчёт.
Ещё одно, что экономит время на повторных случаях: недоступные поля почти никогда не разбросаны случайно. Они кучкуются вокруг одного объекта или одного смыслового блока, чаще всего суммы и персональные данные. Нашли одно - сразу смотрите соседей по тому же объекту, не дожидаясь следующего запуска.
Почему одни поля молчат, а другие падают
Теперь механизм, коротко и по делу.
Компоновка при сборке макета выбрасывает недоступное поле и продолжает работу с тем, что осталось. Ни сообщения, ни строки в журнале. Дальше решает позиция поля:
| Где стояло поле | Что произойдёт | Что увидит пользователь |
|---|---|---|
| Строки, колонки, группировки | структура схлопывается, наборов данных остаётся ноль | отчёт сформирован успешно и пуст |
| Вывод детальных записей, ссылочное поле | обращение к представлению ссылки не компилируется | ошибка про ненайденное поле |
Это проверяется руками за десять минут, и если механизм не укладывается в голове, соберите его на тестовой базе. Заведите роль с чтением на справочник и снимите просмотр с одного реквизита. Поставьте этот реквизит в строки таблицы: отчёт отработает пустым. Переставьте его же в вывод детальных записей: получите ошибку. Не изменилось ничего, кроме позиции одного поля.
Почему компоновка про это молчит, в документации не написано. Моё объяснение, без документального подтверждения: сообщение вида “поле такое-то от вас скрыто” само по себе утечка, оно рассказывает пользователю состав схемы, до которого его не допустили. Механизм выбран в пользу безопасности и против диагностируемости. Знать причину полезно хотя бы затем, чтобы не искать несуществующую настройку “включить подробные сообщения”.
Три вещи, из-за которых список полей не сходится
Просмотр нужен на все типы составного поля сразу. У поля два возможных типа, право есть на один - падает так же, как если бы не было ни одного. В сообщении вы увидите имя поля, а имя недостающего типа там не появится. Состав типов в живых конфигурациях глазами не охватывается, поэтому разворачивайте его кодом:
// Все типы составного реквизита и есть ли на каждый право просмотра
// у ТЕКУЩЕГО пользователя сеанса.
Реквизит = Метаданные.Документы.Начисление.Реквизиты.Основание;
Для Каждого Тип Из Реквизит.Тип.Типы() Цикл
Объект = Метаданные.НайтиПоТипу(Тип);
Если Объект = Неопределено Тогда
Продолжить; // примитивный тип, права не при чём
КонецЕсли;
Сообщить(Объект.ПолноеИмя() + " - просмотр: "
+ ?(ПравоДоступа("Просмотр", Объект), "есть", "НЕТ"));
КонецЦикла;
Единственная строка с “НЕТ” и есть причина ошибки, которая назвала вам только имя поля.
“Чтение” и “Просмотр” - разные права, ломаются на разных этапах. Первое решает, может ли объект участвовать в запросе, второе - может ли поле быть выведено.
| Признак | Нет права “Чтение” | Нет права “Просмотр” |
|---|---|---|
| Где ломается | разбор текста запроса | сборка макета компоновки |
| Что видит пользователь | ошибка про неизвестный объект или реквизит | пустой отчёт либо ошибка про поле |
| Сообщение | есть, и оно честное | сообщения нет вообще |
Звучат обе жалобы одинаково, “нет прав”, а лечатся по-разному.
Признак роли решает, надо ли перечислять поля вообще. У роли есть галочка “Устанавливать права для новых реквизитов и табличных частей”. Когда она включена, право просмотра на объект расходится на все его поля, включая те, что появятся в будущих обновлениях. Когда выключена, нужно каждое поле поимённо, и каждый новый реквизит после обновления конфигурации становится новой миной.
Это самая дешёвая проверка из всех, и делать её мышью по ролям не надо. В выгрузке конфигурации в XML признак лежит в начале файла прав каждой роли:
# Признаки ролей по всей выгрузке конфигурации.
# setForAttributesByDefault - те самые "права для новых реквизитов",
# setForNewObjects - права на новые объекты метаданных.
Get-ChildItem -Path '.\Roles' -Filter 'Rights.xml' -Recurse | ForEach-Object {
$x = [xml](Get-Content $_.FullName -Encoding utf8)
[pscustomobject]@{
Rol = $_.Directory.Parent.Name
NovyeRekvizity = $x.Rights.setForAttributesByDefault
NovyeObiekty = $x.Rights.setForNewObjects
NezavisimyePrava = $x.Rights.independentRightsOfChildObjects
}
} | Sort-Object NovyeRekvizity | Format-Table -AutoSize
Роли со значением false и есть кандидаты в источники проблемы: у них поля
перечисляются поимённо, и они протухают после каждого обновления конфигурации.
По умолчанию признак стоит в true, так что false там оказался чьим-то сознательным
решением, и выяснить чьим стоит до того, как переключать обратно. Обратная сторона
признака: он выдаёт право вперёд, на реквизиты, которых ещё нет. Для ролей, где
ограничение носит организационный характер, это неприемлемо; для технических ролей
снимает целый класс регулярной работы.
Выгрузку в XML делает штатная команда конфигуратора /DumpConfigToFiles.
Что мешает диагностике
Роль, выданная пользователю напрямую, не держится. Механизм управления доступом синхронизирует роли по профилям групп и снимает выданную напрямую. Со стороны это выглядит издевательски: первый запуск после выдачи работает, второй снова падает. Класть роль надо в профиль группы доступа. Если вместо пустого отчёта пользователь видит окно “Нарушение прав доступа!”, порядок другой: сначала журнал регистрации, где платформа записала, какого права не хватило, потом подбор группы. Он разобран в статье про нарушение прав доступа в 1С.
Штатно выяснить, какая именно роль даёт право, нельзя. Функция ПравоДоступа()
суммирует все роли пользователя и роль параметром не принимает:
// Это работает и отвечает "есть или нет",
// но не отвечает "от какой роли".
ПравоДоступа("Просмотр", Метаданные.Справочники.Сотрудники);
Ответ на “откуда у него это право” ищется поиском по выгрузке прав конфигурации. Для пользователя с несколькими профилями это отдельная работа, и закладывать её надо заранее. Мы вынесли её в обработку Матрица прав доступа 1С для нейросети, про подход - отдельный разбор.
Отчёт формируется в фоновом задании. Точка останова не сработает, пока явно не включена отладка фоновых заданий. Об это спотыкаются первым делом.
Собрать отчёт руками для анализа не выйдет. Выражения обращаются к функциям, которые регистрируются только при штатном запуске через форму. Анализировать надо через список доступных полей компоновки, как в коде выше.
Развилка, которую нельзя проскочить
Без этого раздела текст был бы вредным.
Роли вида “чтение без просмотра” существуют намеренно. Они дают чтение для расчётов и запрещают вывод сумм: расчётчик считает, но чужую зарплату в отчёте не видит. В такой конфигурации “отчёт не работает” - это не поломка, а работающее ограничение.
Поэтому порядок такой: сначала выясняем, недоступность поля это упущение при настройке роли или сознательное правило. Если правило, дальше разговор идёт про вариант отчёта без этой колонки, и права обсуждать бесполезно.
Проверять надо до выдачи прав, потому что обратно не отыграешь. На бумаге обратный порядок выглядит безобидно: поставил галочку, закрыл заявку, все довольны. По факту в этот момент кто-то получил доступ к чужим зарплатам, и обнаружится это на ближайшей проверке, где объясняться придётся уже не техникой.
Честно про разобранный случай: чем всё закончилось по существу, не зафиксировано. Дали права на суммы или сделали урезанный вариант отчёта, я не знаю, решение оставалось за бизнесом. Материал доводит до развилки и на ней обрывается.
Границы применимости
Материал методический, числовой фактуры мало. Единственная содержательная проверка - что данные под ограниченным пользователем видны, и подтверждается она ненулевыми тысячами строк.
Нет цифры, сколько полей оказались недоступны. А это единственная метрика масштаба: без неё непонятно, экономит разовый аудит десять минут или день. Экономия линейна по числу полей, но само число не названо.
Время диагностики до и после не измерено. Замер делался после того, как механизм стал понятен, сравнивать не с чем.
Часть наблюдений сделана на бета-версии платформы. Номер релиза не зафиксирован, привязывать выводы к версии не берусь, воспроизводимость один в один не проверялась.
Порядок действий целиком
- Посчитать строки источников в сеансе пользователя - отсечь ограничение записей.
- Прогнать код-аудитор в том же сеансе, получить полный список недоступных полей.
- Начать с помеченных
СТРУКТУРА: они схлопывают отчёт молча. - Для полей со составным типом развернуть состав и найти тип без просмотра.
- Проверить признак “права для новых реквизитов” у всех ролей одной командой.
- До выдачи прав выяснить, упущение это или сознательное ограничение.
Если разбираться некогда, аудит прав мы делаем как работу: выгружаем матрицу по ролям и объектам, находим расхождения и показываем, где ограничение сознательное, а где упущение. Что входит - на странице оптимизации и сопровождения 1С.
Инструменты по теме
Матрица прав доступа 1С для нейросети - выгружает права ролей по объектам и реквизитам: тот самый список, который здесь собирается руками.
Выгрузка структуры метаданных 1С для нейросети - показывает состав реквизитов и типы, включая составные. По ней видно, на сколько типов придётся выдавать просмотр.