Как обнаружить отфильтрованные данные RLS при использовании ВЫБРАТЬ РАЗРЕШЕННЫЕ в 1С?

Программист 1С v8.3 (Управляемые формы) 1С:Зарплата и Управление Персоналом
← На главную

При работе с системой 1С:Предприятие 8.3, особенно в конфигурациях с активным механизмом Разграничения Прав на Уровне Записей (RLS), мы часто сталкиваемся с особенностями обработки данных при выполнении запросов. Давайте разберем ситуацию, когда требуется получить данные, но у пользователя нет полных прав доступа, и как мы можем обнаружить, что часть информации была скрыта механизмом RLS.

Основная проблема возникает при попытке выбрать данные, на которые у текущего пользователя отсутствуют права доступа. Если мы используем обычный оператор ВЫБРАТЬ, система 1С прервет выполнение запроса с ошибкой "Недостаточно прав для выполнения операции над базой данных", как только встретит первую запись, к которой нет доступа. Это удобно для информирования пользователя о проблеме, но прерывает работу. С другой стороны, оператор ВЫБРАТЬ РАЗРЕШЕННЫЕ позволяет запросу успешно завершиться, но при этом просто отсекает все записи, к которым у пользователя нет прав. В результате пользователь получает неполный набор данных, не подозревая об этом. Наша задача – научиться определять факт такого "молчаливого" отсечения данных и при необходимости информировать пользователя.

Понимание работы RLS и операторов ВЫБРАТЬ / ВЫБРАТЬ РАЗРЕШЕННЫЕ

Прежде чем перейти к решениям, давайте кратко остановимся на том, как работает RLS и в чем ключевая разница между ВЫБРАТЬ и ВЫБРАТЬ РАЗРЕШЕННЫЕ. RLS (Row Level Security) — это механизм, который позволяет ограничивать доступ к данным на уровне отдельных записей или полей, а не только на уровне целых таблиц или объектов метаданных. Условия RLS задаются в конфигураторе для определенных ролей и применяются платформой автоматически к каждому запросу, выполняемому под пользователем с такой ролью.

  1. Оператор ВЫБРАТЬ: При его использовании платформа последовательно читает записи из таблицы. Если в процессе чтения обнаруживается запись, которая не удовлетворяет условиям RLS для текущего пользователя, система генерирует исключение "Недостаточно прав...", и запрос аварийно завершается. Это гарантирует, что пользователь либо получит полный набор разрешенных данных, либо будет явно уведомлен о невозможности получения полных данных.

  2. Оператор ВЫБРАТЬ РАЗРЕШЕННЫЕ: Этот оператор изменяет поведение RLS. Вместо генерации ошибки, платформа просто пропускает (фильтрует) записи, к которым у пользователя нет доступа, и включает в результат только те, что разрешены. Запрос выполняется успешно, но его результат может быть неполным без каких-либо явных уведомлений. Ключевое слово РАЗРЕШЕННЫЕ применяется к запросу верхнего уровня и распространяется на все вложенные соединения и таблицы, участвующие в выборке.

Например, в ЗУП 3, как в нашем случае, RLS часто настраивается по группам физических лиц. Это означает, что даже если документ РаботаВВыходныеИПраздничныеДни содержит нескольких сотрудников, и хотя бы на одного из них у пользователя нет прав, полный запрос (без РАЗРЕШЕННЫЕ) может быть заблокирован целиком, так как RLS может срабатывать по условию "наличие хотя бы одного запрещенного сотрудника в табличной части" или "отсутствие прав на сам документ, потому что он связан с запрещенным физлицом".

Решение 1: Сравнение выборок с использованием привилегированного режима

Наиболее надежный и распространенный способ обнаружить отфильтрованные RLS данные — это сравнить результат выполнения запроса с правами текущего пользователя с результатом выполнения того же запроса с полными правами. Для этого мы будем использовать механизм привилегированного режима.

Разберем по шагам, как реализовать этот подход:

  1. Выполняем запрос с ВЫБРАТЬ РАЗРЕШЕННЫЕ: Сначала мы выполняем запрос под текущим пользователем, используя оператор ВЫБРАТЬ РАЗРЕШЕННЫЕ. Этот запрос вернет все данные, доступные пользователю согласно его правам RLS. Мы сохраним эти данные в отдельную таблицу значений или массив.

  2. Устанавливаем привилегированный режим: Далее, мы временно повышаем права выполнения кода с помощью метода УстановитьПривилегированныйРежим(Истина). В этом режиме платформа игнорирует все настройки RLS и другие проверки прав доступа для текущего контекста выполнения кода. Это очень мощный и потенциально опасный инструмент, который требует аккуратного использования!

  3. Выполняем тот же запрос без РАЗРЕШЕННЫЕ: В привилегированном режиме мы выполняем тот же самый запрос, но уже без оператора РАЗРЕШЕННЫЕ. Поскольку RLS игнорируется, этот запрос вернет полный набор данных, который существует в базе, независимо от прав текущего пользователя. Мы также сохраним этот результат.

  4. Отключаем привилегированный режим: Критически важно! Сразу после получения данных в привилегированном режиме мы должны отключить его, вызвав УстановитьПривилегированныйРежим(Ложь). Это необходимо для обеспечения безопасности системы и предотвращения несанкционированного доступа к данным в других частях кода. Рекомендуется использовать блок Попытка...Исключение...КонецПопытки для гарантированного отключения режима даже в случае ошибок.

  5. Сравниваем результаты: Теперь у нас есть два набора данных: один — разрешенный для пользователя, другой — полный. Мы сравниваем их количество. Если количество записей в "разрешенной" выборке меньше, чем в "полной", это означает, что RLS отфильтровал часть данных.

  6. Информируем пользователя: На основе сравнения мы можем выдать пользователю соответствующее сообщение, например: "Внимание! Часть данных была скрыта из-за ограничений прав доступа." При желании, можно даже определить, какие именно записи были скрыты, сравнив уникальные идентификаторы (ссылки) документов или строк.

Посмотрим на пример кода:


// Исходный текст запроса к табличной части документа
ТекстЗапроса = "ВЫБРАТЬ
    |    РаботаВВыходныеИПраздничныеДниСотрудники.Ссылка КАК Ссылка,
    |    РаботаВВыходныеИПраздничныеДниСотрудники.Сотрудник КАК Сотрудник,
    |    РаботаВВыходныеИПраздничныеДниСотрудники.Дата КАК Дата,
    |    РаботаВВыходныеИПраздничныеДниСотрудники.ОтработаноЧасов КАК ОтработаноЧасов,
    |    РаботаВВыходныеИПраздничныеДниСотрудники.СпособКомпенсацииПереработки КАК СпособКомпенсацииПереработки,
    |    РаботаВВыходныеИПраздничныеДниСотрудники.НеРаботал КАК НеРаботал
    |ИЗ
    |    Документ.РаботаВВыходныеИПраздничныеДни.Сотрудники КАК РаботаВВыходныеИПраздничныеДниСотрудники
    |ГДЕ
    |    РаботаВВыходныеИПраздничныеДниСотрудники.Дата МЕЖДУ &ДатаН И &ДатаК
    |    И РаботаВВыходныеИПраздничныеДниСотрудники.Ссылка.ПометкаУдаления = &ПометкаУдаления";

// Создаем объект запроса
Запрос = Новый Запрос(ТекстЗапроса);

// Устанавливаем параметры запроса (предполагаем, что ДатаН, ДатаК, ПометкаУдаления уже определены)
Запрос.УстановитьПараметр("ДатаН", НачалоДня(ТекущаяДата())); // Пример
Запрос.УстановитьПараметр("ДатаК", КонецДня(ТекущаяДата()));   // Пример
Запрос.УстановитьПараметр("ПометкаУдаления", Ложь);

// --- Шаг 1: Выполняем запрос с ВЫБРАТЬ РАЗРЕШЕННЫЕ для текущего пользователя ---
// Добавляем ключевое слово "РАЗРЕШЕННЫЕ" в начало текста запроса
// Мы ищем первое ключевое слово "ВЫБРАТЬ" и вставляем после него "РАЗРЕШЕННЫЕ"
ПозицияВыбрать = Найти(ТекстЗапроса, "ВЫБРАТЬ");
Если ПозицияВыбрать = 0 Тогда
    Сообщить("Некорректный текст запроса: не найдено ключевое слово ВЫБРАТЬ.");
    Возврат;
КонецЕсли;

ТекстЗапросаРазрешенные = Сред(ТекстЗапроса, 1, ПозицияВыбрать + СтрДлина("ВЫБРАТЬ") - 1)
                       + " РАЗРЕШЕННЫЕ "
                       + Сред(ТекстЗапроса, ПозицияВыбрать + СтрДлина("ВЫБРАТЬ"));

ЗапросРазрешенные = Новый Запрос(ТекстЗапросаРазрешенные);
ЗапросРазрешенные.УстановитьПараметры(Запрос.Параметры); // Передаем те же параметры

ВыборкаРазрешенных = Новый ТаблицаЗначений;
Попытка
    ВыборкаРазрешенных = ЗапросРазрешенные.Выполнить().Выгрузить();
Исключение
    // Если даже с РАЗРЕШЕННЫЕ произошла непредвиденная ошибка,
    // например, в каком-то вложенном запросе или из-за некорректного RLS.
    Сообщить("Ошибка при получении разрешенных данных для пользователя: " + ОписаниеОшибки());
    Возврат;
КонецПопытки;

// --- Шаг 2-4: Выполняем тот же запрос с полными правами и отключаем привилегированный режим ---
ВыборкаПолная = Новый ТаблицаЗначений;
Попытка
    // Устанавливаем привилегированный режим
    УстановитьПривилегированныйРежим(Истина);

    ЗапросПолный = Новый Запрос(ТекстЗапроса); // Используем оригинальный текст запроса без "РАЗРЕШЕННЫЕ"
    ЗапросПолный.УстановитьПараметры(Запрос.Параметры);

    ВыборкаПолная = ЗапросПолный.Выполнить().Выгрузить();

Исключение
    Сообщить("Ошибка при получении полных данных в привилегированном режиме: " + ОписаниеОшибки());
КонецПопытки;
Наконец
    // Обязательно отключаем привилегированный режим, даже если произошла ошибка
    УстановитьПривилегированныйРежим(Ложь);
КонецПопытки;

// --- Шаг 5-6: Сравниваем результаты и информируем пользователя ---
Если ВыборкаРазрешенных.Количество() < ВыборкаПолная.Количество() Тогда
    Сообщить("ВНИМАНИЕ! Часть данных была скрыта из-за ограничений прав доступа текущего пользователя.", СтатусСообщения.Важное);

    // Дополнительно, если требуется вывести список скрытых документов или сотрудников:
    // Создадим хеш-таблицу для быстрого поиска разрешенных элементов
    РазрешенныеСсылки = Новый Соответствие;
    Для Каждого СтрокаТЗ Из ВыборкаРазрешенных Цикл
        РазрешенныеСсылки.Вставить(СтрокаТЗ.Ссылка.УникальныйИдентификатор(), Истина);
    КонецЦикла;

    СкрытыеДокументы = Новый Массив;
    Для Каждого СтрокаТЗ Из ВыборкаПолная Цикл
        Если РазрешенныеСсылки.Получить(СтрокаТЗ.Ссылка.УникальныйИдентификатор()) = Неопределено Тогда
            // Если ссылки документа нет в разрешенных, значит, он был скрыт
            Если СкрытыеДокументы.Найти(СтрокаТЗ.Ссылка) = Неопределено Тогда
                СкрытыеДокументы.Добавить(СтрокаТЗ.Ссылка);
            КонецЕсли;
        КонецЕсли;
    КонецЦикла;

    Если СкрытыеДокументы.Количество() > 0 Тогда
        ТекстСкрытыхДокументов = "Скрыты следующие документы:";
        Для Каждого ДокументСсылка Из СкрытыеДокументы Цикл
            ТекстСкрытыхДокументов = ТекстСкрытыхДокументов + Символы.ПС + "  - " + ДокументСсылка;
        КонецЦикла;
        Сообщить(ТекстСкрытыхДокументов);
    КонецЕсли;

Иначе
    Сообщить("Все доступные данные получены.", СтатусСообщения.Информация);
КонецЕсли;

Данный подход является наиболее надежным, так как позволяет получить точную информацию о том, сколько данных было скрыто и, при дополнительной логике, даже какие именно элементы. Однако, мы всегда должны помнить о безопасности при использовании УстановитьПривилегированныйРежим(Истина) и максимально ограничивать его действие по времени и по коду.

Решение 2: Определение отфильтрованных данных с помощью агрегатных функций (ограниченное применение)

Существует идея использования агрегатных функций для определения факта фильтрации данных без использования привилегированного режима. Однако, этот метод имеет серьезные ограничения и не всегда надежен. Основная мысль заключается в попытке получить "полное" количество строк и "разрешенное" количество строк в одном запросе с ВЫБРАТЬ РАЗРЕШЕННЫЕ.

Предположим, у нас есть таблица, и мы хотим знать общее количество записей в ней, даже если часть из них скрыта RLS. Это возможно только в очень специфических случаях, например, если у нас есть служебное поле, которое всегда доступно и может дать нам общее количество, или если условия RLS известны и могут быть инвертированы или "встроены" в запрос. В общем случае, оператор ВЫБРАТЬ РАЗРЕШЕННЫЕ применяет RLS ко всему запросу, и получить "нефильтрованное" количество строк через агрегатную функцию в рамках того же запроса становится невозможно без внешнего источника информации о "полном" количестве.

Пример, предложенный в обсуждении (сообщение 7): Количество(РаботаВВыхи.Сотрудник) = Максимум(РаботаВВыхи.НомерСтроки) как НетуЧужихФизиков. Этот подход предполагает, что НомерСтроки каким-то образом отражает "общее" количество строк до применения RLS, что, к сожалению, не соответствует действительности. Максимум(РаботаВВыхи.НомерСтроки) всегда вернет максимальный номер строки среди тех, к которым есть доступ, а Количество() — количество доступных строк. Если RLS отфильтрует строки, то и Количество(), и Максимум(НомерСтроки) будут отражать только доступные данные, и их равенство не будет гарантировать отсутствие фильтрации.

Поэтому, для надежного определения факта отсечения данных RLS, метод сравнения выборок с использованием привилегированного режима остается предпочтительным. Если же привилегированный режим по какой-либо причине не может быть использован, а требуется определить факт фильтрации, то придется искать иные, более сложные и менее универсальные подходы, например, попытаться получить "полное" количество записей из таблиц, не защищенных RLS, или, в крайнем случае, переосмыслить настройки RLS для данной задачи.

Дополнительные аспекты и переосмысление подхода

  1. Смысл RLS: Изначальный смысл RLS заключается в том, чтобы пользователь *не знал* о существовании данных, к которым у него нет доступа. Если бизнес-требования диктуют, что пользователь должен быть осведомлен о неполноте данных, возможно, стоит пересмотреть саму настройку RLS или логику работы с данными для данной конкретной задачи.

  2. Причины полной блокировки документа: Как было отмечено в сообщении 4, RLS может блокировать чтение всего документа, даже если у пользователя есть права на некоторых сотрудников в его табличной части. Это происходит, когда условие RLS накладывается на сам документ по наличию "запрещенных" физических лиц или по другим критериям, связанным с документом в целом.

  3. Альтернативные стратегии:

    • Явный отбор в запросе: Если мы точно знаем критерии доступа пользователя (например, его подразделение, список доступных физлиц и т.д.), мы можем явно включить эти критерии в секцию ГДЕ запроса. Это позволит получить только разрешенные данные без использования ВЫБРАТЬ РАЗРЕШЕННЫЕ и избежать ошибок, но потребует ручного формирования условий.

    • Метод ВыполнитьПроверкуПравДоступа: Этот метод (например, Метаданные.Документы.РаботаВВыходныеИПраздничныеДни.Право("Чтение") или Объект.ПроверитьПраво("Чтение")) используется для проверки прав доступа к объекту метаданных или конкретному объекту в целом, но он не проверяет RLS на уровне записей. То есть, он не поможет определить, была ли отфильтрована какая-то конкретная строка в табличной части.

  4. Производительность: Использование RLS, особенно со сложными условиями, может негативно сказаться на производительности запросов, так как платформа вынуждена применять дополнительные фильтры. Использование привилегированного режима также несет определенные накладные расходы, но обычно они незначительны по сравнению с преимуществами.

В заключение, задача обнаружения отфильтрованных RLS данных при использовании ВЫБРАТЬ РАЗРЕШЕННЫЕ является решаемой, и наиболее надежный метод включает сравнение выборок с использованием привилегированного режима. Мы, как разработчики, должны тщательно взвешивать необходимость такой проверки и ее влияние на безопасность и производительность системы.

← На главную