SQL CLR — ошибка разрешения на TSQL UDF, даже если у пользователя есть доступ

Я использую контекстное соединение в скалярной функции SQL CLR. Функция создает динамический запрос, а затем выполняет его (получая данные/логику из разных мест для построения запроса).

Я могу «построить» динамическую строку вручную и запустить ее в стандартном окне Management Studio, и она работает нормально. Когда тот же запрос выполняется через CLR в контекстном соединении, я получаю сообщение об ошибке.

Я отладил эту вещь и обнаружил, что ошибка связана с проблемой разрешения на UDF TSQL, который вызывается в запросе. Созданное сообщение об ошибке:

Отказано в разрешении EXECUTE на be.udfDivide

Я знаю, что у меня есть доступ к этой определяемой пользователем функции, потому что она работает, когда я запускаю запрос вручную, но при запуске через SQLCLR происходит сбой, хотя код SQLCLR должен выполняться в моем контексте.

Запрос эффективно:

select be.udfDivide(sum(Numerator), sum(Denominator))
from Table where <some stuff>

В <some stuff> живет вся сложная логика, но я уверяю вас, что это просто предложение where, и эта функция CLR успешно запускается миллионы раз в день, когда в запросе нет ссылки на UDF.

С помощью некоторого творческого взлома запросов я смог подтвердить, что контекстное соединение выполняется под правильным пользователем (как SYSTEM_USER, так и CURRENT_USER), и я знаю, что у этого пользователя есть разрешения на UDF. Таким образом, похоже, что причиной этого является нарушение цепочки владения. Только пока не знаю, как это исправить.

Я знаю, что могу удалить UDF в этом конкретном случае, так как это простое деление (хотя UDF обрабатывает деление на ноль для нас), но в случае более сложных UDF мы хотели бы иметь возможность ссылаться на них в запросе.


Функция be.udfDivide приведена ниже для справки:

ALTER function [be].[udfDivide](@Numerator float, @Demoninator float,
                                @ValueIfDivideByZero float)
returns float
as
begin
    declare @result float
    if @Demoninator = 0 
        begin set @result = @ValueIfDivideByZero end
    else
        begin set @result = @Numerator / @Demoninator end
    return @result
end

person thomas    schedule 14.06.2017    source источник


Ответы (1)


То, что здесь происходит, не задокументировано нигде, что я мог найти, но, похоже, требуется указать SystemDataAccess = SystemDataAccessKind.Read в атрибуте SqlFunction (по умолчанию он установлен на None) при выполнении запросов, содержащих T-SQL Scalar UDF или Multi-statement TVF, НО только если функция не привязана к схеме. Это означает, что вам может сойти с рук SystemDataAccess = SystemDataAccessKind.None, если ссылочная функция:

  • встроенный TVF
  • скалярная UDF или TVF с несколькими операторами, созданная с использованием параметра WITH SCHEMABINDING

Имеет смысл, что у встроенных TVF нет проблем, поскольку их определение втягивается в запрос, в котором на них ссылаются, и они не вызываются как отдельные объекты.

Я предполагаю, что причина требования «системного» доступа к данным для не привязанных к схеме UDF и TVF с несколькими операторами заключается в том, что представления системного каталога, вероятно, необходимо проверять во время выполнения, чтобы убедиться, что все еще существуют какие-либо зависимости и разрешения. действительны, поскольку нельзя гарантировать, что они будут такими же, как они были изначально, из-за отсутствия привязки схемы (что предотвращает определенные изменения объектов, которые являются зависимостями других объектов).

Имея это в виду, самым быстрым/самым прямым исправлением (которое позволяет SystemDataAccess оставаться неопределенным или, по крайней мере, иметь значение None) будет добавление этой опции в UDF следующим образом:

ALTER FUNCTION [be].[udfDivide](@Numerator FLOAT, @Denominator FLOAT,
                                @ValueIfDivideByZero FLOAT)
RETURNS FLOAT
WITH SCHEMABINDING -- this option helps in a few ways
AS
BEGIN
    DECLARE @Result FLOAT;
    IF (@Denominator = 0)
    BEGIN
      SET @Result = @ValueIfDivideByZero;
    END;
    ELSE
    BEGIN
      SET @Result = (@Numerator / @Denominator);
    END;

    RETURN @Result;
END;

НО, если этот запрос выполняется так часто, вам, безусловно, следует преобразовать его во встроенный TVF. Существует огромный удар по производительности при использовании UDF, и ITVF обычно являются ответом на этот вопрос. Вам просто нужно выразить ту же логику в виде одного запроса (используйте CASE для «если-то-иначе»), а затем использовать его через CROSS APPLY, а не в списке SELECT, оба из которых достаточно просты.

Здесь та же логика, что и у Inline TVF:

CREATE FUNCTION [be].[itvfDivide](@Numerator FLOAT, @Denominator FLOAT,
                                @ValueIfDivideByZero FLOAT)
RETURNS TABLE
WITH SCHEMABINDING -- not necessary, but still a good idea to use
AS RETURN
   SELECT CASE @Denominator
             WHEN 0 THEN @ValueIfDivideByZero
             ELSE (@Numerator / @Denominator)
          END AS [Result];
GO

А вот и простой тест:

SELECT itvf.[Result]
FROM   (VALUES (1, 2, NULL), (3, 0, -5.789), (1.1, 5.67567, -999)
       ) tbl(num, den, dbz)
CROSS APPLY be.itvfDivide(tbl.[num], tbl.[den], tbl.[dbz]) itvf;

Этот ITVF будет намного быстрее, чем текущий Scalar UDF :-).

person Solomon Rutzky    schedule 14.06.2017
comment
Обтекание exec дает правильный результат (тот же результат, что и нединамический запрос в SSMS). У нас есть десятки встроенных функций табличных значений, но этот конкретный случай не способствует этому. Отредактировал вопрос, чтобы предоставить сообщение об ошибке. - person thomas; 14.06.2017
comment
быстрое пояснение - этот конкретный вариант использования будет выполняться только около дюжины раз в день - другие запросы без вызовов UDF выполняются миллионы - person thomas; 14.06.2017
comment
Я предполагаю, что все эти объекты находятся в одной БД? Кроме того, используется ли какое-либо олицетворение? Определение CREATE FUNCTION включает в себя пункт EXECUTE AS? Обычно это будет EXECUTE AS CALLER. Вы вошли в систему как логин Windows или логин SQL Server? Что возвращает функция ORIGINAL_LOGIN() при работе в контекстном соединении? - person Solomon Rutzky; 16.06.2017
comment
Извините за задержку - втянулся в другие дела. Все в той же БД. EXECUTE AS не указано в CREATE FUNCTION. Зашел под логином Windows. Я sa на сервере, и это не сработает для меня. ORIGINAL_LOGIN() возвращает мой идентификатор пользователя, когда я запускаю запрос. - person thomas; 05.07.2017
comment
Установка EXECUTE AS в CREATE FUNCTION на любой из CALLER, SELF, OWNER, <USER_NAME> по-прежнему вызывает ошибку. - person thomas; 05.07.2017
comment
Спасибо за всю эту дополнительную информацию. Вы на 100% уверены, что это контекстная связь? Извините, просто нужно спросить ;-). Кроме того, находятся ли пользовательские функции T-SQL и SQLCLR в одной и той же схеме? Кроме того, делает ли сгенерированный запрос что-либо кроме простого SELECT? Можете ли вы использовать SQL Server Profiler для захвата того, что отправляет пользовательская функция SQLCLR? - person Solomon Rutzky; 05.07.2017
comment
Уверен в контекстной связи. Это код, который я использую в функции SQLCLR. SqlConnection connection = null; try { connection = new SqlConnection("context connection=true"); connection.Open(); Функции CLR и TSQL находятся в одной схеме в базе данных. Код CLR строит строку запроса динамически (на основе многих факторов), но конечным результатом является оператор select, который агрегирует строки из таблицы — единственным требованием является то, что оператор select просто должен возвращать значение с плавающей запятой. - person thomas; 05.07.2017
comment
Поиграюсь с профилировщиком и сообщу о результатах. - person thomas; 05.07.2017
comment
Ok. Итак, вы строите запрос и вызываете SqlCommand.ExecuteReader() или, возможно, ExecuteScalar()? Кроме того, я надеюсь, что у вас есть блок finally, который включает connection.Dispose(); ;-) - person Solomon Rutzky; 05.07.2017
comment
В профилировщике я даже не вижу выполнения динамического запроса в SQLCLR - только вызов выбора TSQL select be.udfGetCellValue1(1, 1, ''), который является моей оболочкой SQLCLR в TSQL. Происходит ряд вещей, но в конечном итоге выполняется динамический запрос. Это работает до тех пор, пока этот запрос не содержит UDF, а только простые суммы, подсчеты, средние значения и т. д. Как только мы вводим UDF в оператор select, мы получаем ошибку. Попытка отлаживать код CLR и сейчас - удаленная отладка оставляет желать лучшего! - person thomas; 05.07.2017
comment
return (double)command.ExecuteScalar(); это то, что вызывается. - person thomas; 05.07.2017
comment
Если вы не видите динамический SQL в вызываемом UDF SQLCLR, то он каким-то образом отфильтровывается, но не может быть так, чтобы его там не было. Я думаю, что вам нужна категория SQL:. Можете ли вы временно предоставить EXECUTE в пользовательской функции T-SQL для [Public], чтобы исключить вопрос о том, кто это вызывает? Кроме того, мне интересно, есть ли где-то DENY, но не уверен, почему он появляется только при вызове из SQLCLR. Рад, что вы закрываете соединение; Я слышал хорошие отзывы об авторе этой статьи ;-) (хотя сейчас я предпочитаю звонить Dispose(), но и Close можно). - person Solomon Rutzky; 05.07.2017
comment
Кроме того, попробуйте еще одну вещь: создайте новую пользовательскую функцию SQLCLR, которая ничего не делает, кроме как выполняет полный запрос, содержащий пользовательскую функцию T-SQL. Таким образом, мы сужаем то, что происходит. Это должен быть простой оператор SQL, который не создается, а только предоставляется. - person Solomon Rutzky; 05.07.2017
comment
Ok. Так что щелкнул практически каждое событие Profiler, которое смог найти. Запустил запрос и получил обратно This statement has attempted to access data whose access is restricted by the assembly.. У меня в декораторе следующее: [Microsoft.SqlServer.Server.SqlFunction(DataAccess = DataAccessKind.Read)] но, может быть, мне нужно еще и SystemDataAccess? Если это так, я избавлю вас от хлопот и сам себя ударю. - person thomas; 05.07.2017
comment
Нет, SystemDataAccess — это то, что нужно методу для выбора из системных объектов. Я не думал, что T-SQL UDF делает что-то подобное, или, по крайней мере, я ожидал другого сообщения об ошибке, если это так. Можете ли вы попробовать поместить этот запрос, который ссылается на пользовательскую функцию T-SQL, в собственную пользовательскую функцию SQLCLR, чтобы изолировать происходящее? Тем не менее, беспокоит то, что вы не видите этого в Profiler. Вы уверены, что код сборки в SQL Server соответствует версии исходного кода, который вы просматриваете? - person Solomon Rutzky; 05.07.2017
comment
Теперь я вижу это в Profiler. Я только что добавил параметр SystemDataAccess в декоратор, и... теперь он работает. Не уверен, что сейчас что-то еще сломано - нужно провести много тестов. В отладчике Visual Studio мы получили ошибку, что у нас нет разрешений, однако Profiler дал нам лучшую ошибку о том, что у сборки нет необходимых разрешений, а не у пользователя. - person thomas; 06.07.2017
comment
Итак, с SystemDataAccess=Read пользовательскую функцию T-SQL теперь можно использовать в запросе, не вызывая ошибки? Что делает эта T-SQL UDF? Я могу запустить тест или два, а затем обновить свой ответ, чтобы он был более точным и полным объяснением. - person Solomon Rutzky; 06.07.2017
comment
Правильный. udfDivide просто делит 2 числа и обрабатывает деление на ноль, не выдавая ошибки. Только что вышел из офиса, но завтра вставлю код в функцию. Мы видели, что это происходит и с другими пользовательскими функциями. - person thomas; 06.07.2017
comment
Это займет немного больше времени, но я приближаюсь. Я смог воспроизвести ошибку, а затем применил исправление SystemDataAccess = Read (которое не должно требоваться), и это сработало. НО проблема не решена, так как я тестировал встроенный TVF и TVF с несколькими операторами. Верните функцию обратно в SystemDataAccess = None, и они оба заработали. Но потом я попробовал оригинальный UDF, и теперь он работает. Это может быть проблема с метаданными, но я запустил DBCC FREESYSTEMCACHE('ALL');, но UDF все еще работает. Странный. Я все еще ищу и дам вам знать, что я найду. - person Solomon Rutzky; 06.07.2017
comment
@thomas Ответ обновлен. Пожалуйста, верните SystemDataAccess обратно в None, затем используйте WITH SCHEMABINDING для других UDF, но для этого конкретного UDF преобразуйте его во встроенный TVF, как я указал в конце своего ответа. Вам будет намного лучше :-). (p.s. Я также исправил знаменатель с ошибкой ;-). Наслаждайтесь ???? (счастливый кот) ???? - person Solomon Rutzky; 07.07.2017
comment
Удивительно. Мы очень ценим ваш подробный ответ и расследование. Я смог добиться тех же результатов, что и вы. Спасибо за вашу помощь! - person thomas; 07.07.2017
comment
Вы очень кстати. Интересный вопрос для изучения, и он войдет в мой грядущий проект веб-сайта, посвященного SQLCLR - лучшие практики, распространенные ошибки, ресурсы и т. д. :-) - person Solomon Rutzky; 07.07.2017