Azure / azure-kusto-dotnet

Azure Data Explorer (Kusto) SDK for .NET
MIT License
6 stars 3 forks source link

Can't run Kusto.Data SDK queries in a deployed Azure Functions App, but same setup works with Azure Function Kusto bindings #29

Closed isakengstrom closed 3 months ago

isakengstrom commented 3 months ago

I'm trying to use the Kusto.Data SDK to retrieve data from Azure Data Explorer, through an Azure Function app. I'm able to use the SDK to get data when I run it locally. However, I get the following Kusto client authentication exception when I'm executing the following method ICslQueryProvider.ExecuteQueryAsync() in Azure (see full stack trace further down):

Kusto client failed to perform authentication: 'An HttpListenerException occurred while listening on http://localhost:52264/ for the system browser to complete the login. Possible cause and mitigation: the app is unable to listen on the specified URL; run 'netsh http add iplisten 127.0.0.1' from the Admin command prompt.. [InnerException: HttpListenerException]: Access is denied.'

I'm able to run the same query using the Azure Functions Kusto bindings, both locally and in Azure. I've configured both functions to use System Assigned Managed Identity, and they both use the same connection string.

Connection string format: Data Source=<DATA_SOURCE_URI>;Fed=true;Accept=true;Authority Id=<TENANT_ID>

Any ideas of what I'm doing wrong?

Sample code, using the Kusto Data SDK

I'm using the following NuGet: Microsoft.Azure.Kusto.Data 12.1.0

// - Works locally, 
// - Does not work when deployed to Azure
[Function(nameof(Function0))]
public async Task<HttpResponseData> Function0(
    [HttpTrigger(AuthorizationLevel.Function, "get", Route = "v0/mydata")] HttpRequestData req, 
    FunctionContext context
)
{
    var kustoConnectionStringBuilder = new KustoConnectionStringBuilder()
    {
        PreventAccessToLocalSecretsViaKeywords = false,
        ConnectionString = _connectionOptionsWrapper.Value.KustoConnectionString,
        Accept = true,
        FederatedSecurity = true
    };

    kustoConnectionStringBuilder.WithAadSystemManagedIdentity();

    var clientRequestProperties = new ClientRequestProperties
    {
        ClientRequestId = $"myQuery{Guid.NewGuid()}"
    };
    clientRequestProperties.SetOption(ClientRequestProperties.OptionServerTimeout, "1m");

    using var queryProvider = KustoClientFactory.CreateCslQueryProvider(kustoConnectionStringBuilder);

    using var dataReader = await queryProvider.ExecuteQueryAsync( // <-- this is the line that throws the exception
        "databaseName", 
        "tableName | take 2",
        clientRequestProperties,
        context.CancellationToken
    );

    var data = dataReader
        .ToJObjects()
        .Select(jo => jo.ToObject<MyDataDto>() ?? new MyDataDto())
        .ToList();

    return await req.CreateResponseAsync(HttpStatusCode.OK, data);
}

Sample code, using Azure Functions Kusto bindings

I'm using the following NuGet: Microsoft.Azure.Functions.Worker.Extensions.Kusto 1.0.9-preview

// - Works locally 
// - Works when deployed to Azure
[Function(nameof(Function1))]
public async Task<HttpResponseData> Function1(
    [HttpTrigger(AuthorizationLevel.Function, "get", Route = "v1/mydata")] HttpRequestData req,
    FunctionContext context,
    [KustoInput(Database: "databaseName", KqlCommand = $"tableName | take 2", Connection = "KustoConnectionString", ManagedServiceIdentity = "system")] List<MyDataDto> data
)
{
    return await req.CreateResponseAsync(HttpStatusCode.OK, data);
}

Other specs:

Stack Trace:

Exception while executing function: Functions.Function0 Result: Failure Exception: System.AggregateException: One or more errors occurred. (Kusto client failed to perform authentication: 'An HttpListenerException occurred while listening on http://localhost:52264/ for the system browser to complete the login. Possible cause and mitigation: the app is unable to listen on the specified URL; run 'netsh http add iplisten 127.0.0.1' from the Admin command prompt.. [InnerException: HttpListenerException]: Access is denied.') ---> [0]Kusto.Data.Exceptions.KustoClientAuthenticationException: Kusto client failed to perform authentication: 'An HttpListenerException occurred while listening on http://localhost:52264/ for the system browser to complete the login. Possible cause and mitigation: the app is unable to listen on the specified URL; run 'netsh http add iplisten 127.0.0.1' from the Admin command prompt.. [InnerException: HttpListenerException]: Access is denied.' Timestamp=2024-04-02T14:28:29.2325955Z ClientRequestId=myQuery91a61506-2860-4675-9a30-5c407fd05763 ActivityId= ActivityType=KD.RestClient.ExecuteQuery MachineName= ProcessName=dotnet ProcessId= ThreadId= ActivityStack=(Activity stack: CRID=myQuery91a61506-2860-4675-9a30-5c407fd05763 ARID= > KD.RestClient.ExecuteQuery/) MonitoredActivityContext=(ActivityType=KD.RestClient.ExecuteQuery, Timestamp=2024-04-02T14:28:29.1836333Z, ParentActivityId=, TimeSinceStarted=48.973 [ms])ErrorMessage= DataSource= DatabaseName= ClientRequestId= ---> MSAL.NetCore.4.59.0.0.MsalClientException: ErrorCode: http_listener_error Microsoft.Identity.Client.MsalClientException: An HttpListenerException occurred while listening on http://localhost:52264/ for the system browser to complete the login. Possible cause and mitigation: the app is unable to listen on the specified URL; run 'netsh http add iplisten 127.0.0.1' from the Admin command prompt. ---> System.Net.HttpListenerException (5): Access is denied. at System.Net.HttpListener.SetupV2Config() at System.Net.HttpListener.Start() at Microsoft.Identity.Client.Platforms.Shared.DefaultOSBrowser.HttpListenerInterceptor.ListenToSingleRequestAndRespondAsync(Int32 port, String path, Func2 responseProducer, CancellationToken cancellationToken) --- End of inner exception stack trace --- at Microsoft.Identity.Client.Platforms.Shared.DefaultOSBrowser.HttpListenerInterceptor.ListenToSingleRequestAndRespondAsync(Int32 port, String path, Func2 responseProducer, CancellationToken cancellationToken) at Microsoft.Identity.Client.Platforms.Shared.Desktop.OsBrowser.DefaultOsBrowserWebUi.InterceptAuthorizationUriAsync(Uri authorizationUri, Uri redirectUri, Boolean isBrokerConfigured, CancellationToken cancellationToken) at Microsoft.Identity.Client.Platforms.Shared.Desktop.OsBrowser.DefaultOsBrowserWebUi.AcquireAuthorizationAsync(Uri authorizationUri, Uri redirectUri, RequestContext requestContext, CancellationToken cancellationToken) at Microsoft.Identity.Client.Internal.AuthCodeRequestComponent.FetchAuthCodeAndPkceInternalAsync(IWebUI webUi, CancellationToken cancellationToken) at Microsoft.Identity.Client.Internal.AuthCodeRequestComponent.FetchAuthCodeAndPkceVerifierAsync(CancellationToken cancellationToken) at Microsoft.Identity.Client.Internal.Requests.InteractiveRequest.GetTokenResponseAsync(CancellationToken cancellationToken) at Microsoft.Identity.Client.Internal.Requests.InteractiveRequest.ExecuteAsync(CancellationToken cancellationToken) at Microsoft.Identity.Client.Internal.Requests.RequestBase.RunAsync(CancellationToken cancellationToken) at Microsoft.Identity.Client.ApiConfig.Executors.PublicClientExecutor.ExecuteAsync(AcquireTokenCommonParameters commonParameters, AcquireTokenInteractiveParameters interactiveParameters, CancellationToken cancellationToken) at Kusto.Cloud.Platform.Http.AadUserHttpClientAuthenticator.GetTokenAsync() Inner Exception: System.Net.HttpListenerException (5): Access is denied. at System.Net.HttpListener.SetupV2Config() at System.Net.HttpListener.Start() at Microsoft.Identity.Client.Platforms.Shared.DefaultOSBrowser.HttpListenerInterceptor.ListenToSingleRequestAndRespondAsync(Int32 port, String path, Func2 responseProducer, CancellationToken cancellationToken) --- End of inner exception stack trace --- ([0]Kusto.Data.Exceptions.KustoClientAuthenticationException.StackTrace:) at Kusto.Cloud.Platform.Http.AadUserHttpClientAuthenticator.GetTokenAsync() at Kusto.Cloud.Platform.Http.AadUserHttpClientAuthenticator.AuthenticateAsync(HttpRequestMessage request) at Kusto.Data.Net.Client.RestClient2.MakeHttpRequestAsyncImpl(RestApi restApi, String address, String csl, String ns, String databaseName, Boolean streaming, ClientRequestProperties properties, ServiceModelTimeoutKind timeoutKind, String clientRequestId, Stream body, StreamProperties streamProperties, CancellationToken cancellationToken, KustoProtocolRequest request, String hostHeaderOverride) at Kusto.Cloud.Platform.Utils.MonitoredActivity.InvokeAsync[TActivityType,TResult](TActivityType activityType, Func1 func, String clientRequestId) at Kusto.Cloud.Platform.Utils.MonitoredActivity.InvokeAsync[TActivityType,TResult](TActivityType activityType, Func1 func, String clientRequestId) at Kusto.Data.Net.Client.RestClient2.MakeHttpRequestAsync(RestApi restApi, String baseAddress, String relativeAddress, String clientRequestIdPrefix, String ns, String databaseName, String csl, String addr, Boolean streaming, ClientRequestProperties properties, ServiceModelTimeoutKind timeoutKind, StreamProperties streamProperties, CancellationToken cancellationToken) at Kusto.Data.Net.Client.RestClient2.ExecuteQueryAsync(String databaseName, String query, ClientRequestProperties properties, CancellationToken cancellationToken) at My.App.FunctionApp.Functions.MyDataFunctions.Function0(HttpRequestData req, FunctionContext context) in D:\a\1\s\applications\My.App\src\My.App.FunctionApp\Functions\MyDataFunctions.cs:line 107 --- End of inner exception stack trace --- at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions) at System.Threading.Tasks.Task1.GetResultCore(Boolean waitCompletionNotification) at Microsoft.Azure.Functions.Worker.Invocation.DefaultFunctionInvoker2.<>c.<InvokeAsync>b__6_0(Task1 t) in D:\a_work\1\s\src\DotNetWorker.Core\Invocation\DefaultFunctionInvoker.cs:line 32 at System.Threading.Tasks.ContinuationResultTaskFromResultTask2.InnerInvoke() at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state) --- End of stack trace from previous location --- at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state) at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread) --- End of stack trace from previous location --- at Microsoft.Azure.Functions.Worker.Invocation.DefaultFunctionExecutor.ExecuteAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\Invocation\DefaultFunctionExecutor.cs:line 49 at Microsoft.Azure.Functions.Worker.OutputBindings.OutputBindingsMiddleware.Invoke(FunctionContext context, FunctionExecutionDelegate next) in D:\a\_work\1\s\src\DotNetWorker.Core\OutputBindings\OutputBindingsMiddleware.cs:line 13 at Microsoft.Azure.Functions.Worker.FunctionsApplication.InvokeFunctionAsync(FunctionContext context) in D:\a\_work\1\s\src\DotNetWorker.Core\FunctionsApplication.cs:line 77 at Microsoft.Azure.Functions.Worker.Handlers.InvocationHandler.InvokeAsync(InvocationRequest request) in D:\a\_work\1\s\src\DotNetWorker.Grpc\Handlers\InvocationHandler.cs:line 88 Stack: at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions) at System.Threading.Tasks.Task1.GetResultCore(Boolean waitCompletionNotification) at Microsoft.Azure.Functions.Worker.Invocation.DefaultFunctionInvoker2.<>c.<InvokeAsync>b__6_0(Task1 t) in D:\a_work\1\s\src\DotNetWorker.Core\Invocation\DefaultFunctionInvoker.cs:line 32 at System.Threading.Tasks.ContinuationResultTaskFromResultTask`2.InnerInvoke() at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state) --- End of stack trace from previous location --- at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state) at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread) --- End of stack trace from previous location --- at Microsoft.Azure.Functions.Worker.Invocation.DefaultFunctionExecutor.ExecuteAsync(FunctionContext context) in D:\a_work\1\s\src\DotNetWorker.Core\Invocation\DefaultFunctionExecutor.cs:line 49 at Microsoft.Azure.Functions.Worker.OutputBindings.OutputBindingsMiddleware.Invoke(FunctionContext context, FunctionExecutionDelegate next) in D:\a_work\1\s\src\DotNetWorker.Core\OutputBindings\OutputBindingsMiddleware.cs:line 13 at Microsoft.Azure.Functions.Worker.FunctionsApplication.InvokeFunctionAsync(FunctionContext context) in D:\a_work\1\s\src\DotNetWorker.Core\FunctionsApplication.cs:line 77 at Microsoft.Azure.Functions.Worker.Handlers.InvocationHandler.InvokeAsync(InvocationRequest request) in D:\a_work\1\s\src\DotNetWorker.Grpc\Handlers\InvocationHandler.cs:line 88

line 107 from the stack trace is the following line from the sample code above

using var dataReader = await queryProvider.ExecuteQueryAsync(
        "databaseName", 
        "tableName | take 2",
        clientRequestProperties,
        context.CancellationToken
    );
isakengstrom commented 3 months ago

I managed to fix the problem. It was related to the system managed identity not being configured correctly for the connection string builder. I had misinterpreted kustoConnectionStringBuilder.WithAadSystemManagedIdentity() to be a setter for a managed identity flag on the builder. But, as the name of the method actually suggests, it's not a setter..

I ended up solving my issue by changing this:

kustoConnectionStringBuilder.WithAadSystemManagedIdentity();

to this:

kustoConnectionStringBuilder = kustoConnectionStringBuilder.WithAadSystemManagedIdentity();
SimidieTulpe commented 1 month ago

You saved my week!!!