From bd88991534a1c2775032104ca9db3d6d723623c5 Mon Sep 17 00:00:00 2001 From: Shubham Gogna Date: Wed, 14 May 2025 22:50:25 -0700 Subject: [PATCH 1/4] Introduce PowerPlatformConnectorClient2 --- .../IPowerPlatformConnectorClient2.cs | 39 ++++ .../OpenApiExtensions.cs | 18 +- .../PowerPlatformConnectorClient2.cs | 191 ++++++++++++++++++ ...atformConnectorClient2DiagnosticOptions.cs | 20 ++ 4 files changed, 264 insertions(+), 4 deletions(-) create mode 100644 src/libraries/Microsoft.PowerFx.Connectors/IPowerPlatformConnectorClient2.cs create mode 100644 src/libraries/Microsoft.PowerFx.Connectors/PowerPlatformConnectorClient2.cs create mode 100644 src/libraries/Microsoft.PowerFx.Connectors/PowerPlatformConnectorClient2DiagnosticOptions.cs diff --git a/src/libraries/Microsoft.PowerFx.Connectors/IPowerPlatformConnectorClient2.cs b/src/libraries/Microsoft.PowerFx.Connectors/IPowerPlatformConnectorClient2.cs new file mode 100644 index 0000000000..65722390e9 --- /dev/null +++ b/src/libraries/Microsoft.PowerFx.Connectors/IPowerPlatformConnectorClient2.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerFx.Connectors +{ + /// + /// Delegate for providing a bearer token for authentication. + /// + /// Cancellation token. + /// Token without "Bearer" scheme as the prefix. + public delegate Task PowerPlatformConnectorClient2BearerTokenProvider( + CancellationToken cancellationToken); + + public interface IPowerPlatformConnectorClient2 + { + /// + /// Sends an HTTP request to the Power Platform connector. + /// + /// HTTP method. + /// Operation path and query string. + /// Headers. + /// Content. + /// Diagnostic options. + /// Cancellation token. + /// . + Task SendAsync( + HttpMethod method, + string operationPathAndQuery, + IEnumerable>> headers, + HttpContent content, + PowerPlatformConnectorClient2DiagnosticOptions diagnosticOptions, + CancellationToken cancellationToken); + } +} diff --git a/src/libraries/Microsoft.PowerFx.Connectors/OpenApiExtensions.cs b/src/libraries/Microsoft.PowerFx.Connectors/OpenApiExtensions.cs index 4d24231d31..7922d46efc 100644 --- a/src/libraries/Microsoft.PowerFx.Connectors/OpenApiExtensions.cs +++ b/src/libraries/Microsoft.PowerFx.Connectors/OpenApiExtensions.cs @@ -51,6 +51,17 @@ public static class OpenApiExtensions public static string GetAuthority(this OpenApiDocument openApiDocument, SupportsConnectorErrors errors) => GetUriElement(openApiDocument, (uri) => uri.Authority, errors); private static string GetUriElement(this OpenApiDocument openApiDocument, Func getElement, SupportsConnectorErrors errors) + { + var uri = GetFirstServerUri(openApiDocument, errors); + if (uri is null) + { + return null; + } + + return getElement(uri); + } + + internal static Uri GetFirstServerUri(this OpenApiDocument openApiDocument, SupportsConnectorErrors errors) { if (openApiDocument?.Servers == null) { @@ -72,8 +83,7 @@ private static string GetUriElement(this OpenApiDocument openApiDocument, Func> list, bool isNumber) = openApiParameter.GetEnumValues(); if (list != null && list.Any()) @@ -1205,7 +1215,7 @@ internal static Dictionary GetParameterMap(thi { // https://github.com/microsoft/OpenAPI.NET/issues/533 // https://github.com/microsoft/Power-Fx/pull/1987 - https://github.com/microsoft/Power-Fx/issues/1982 - // api-version, x-ms-api-version, X-GitHub-Api-Version... + // api-version, x-ms-api-version, X-GitHub-Api-Version... if (prm.Key.EndsWith("api-version", StringComparison.OrdinalIgnoreCase)) { fv = FormulaValue.New(dtv.GetConvertedValue(TimeZoneInfo.Utc).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); diff --git a/src/libraries/Microsoft.PowerFx.Connectors/PowerPlatformConnectorClient2.cs b/src/libraries/Microsoft.PowerFx.Connectors/PowerPlatformConnectorClient2.cs new file mode 100644 index 0000000000..36df514b85 --- /dev/null +++ b/src/libraries/Microsoft.PowerFx.Connectors/PowerPlatformConnectorClient2.cs @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.OpenApi.Models; + +namespace Microsoft.PowerFx.Connectors +{ + /// + /// Client for invoking operations of Power Platform connectors. + /// + public class PowerPlatformConnectorClient2 : IPowerPlatformConnectorClient2 + { + private readonly string _baseUrlStr; + private readonly HttpMessageInvoker _httpMessageInvoker; + private readonly PowerPlatformConnectorClient2BearerTokenProvider _tokenProvider; + private readonly string _environmentId; + + /// + /// Initializes a new instance of the class. + /// + /// Document used for extracting the base URL. + /// HTTP message invoker. + /// Bearer token provider. + /// Environment ID. + public PowerPlatformConnectorClient2( + OpenApiDocument document, + HttpMessageInvoker httpMessageInvoker, + PowerPlatformConnectorClient2BearerTokenProvider tokenProvider, + string environmentId) + : this(GetBaseUrlFromOpenApiDocument(document), httpMessageInvoker, tokenProvider, environmentId) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Base URL for requests. + /// HTTP message invoker. + /// Bearer token provider. + /// Environment ID. + public PowerPlatformConnectorClient2( + Uri baseUrl, + HttpMessageInvoker httpMessageInvoker, + PowerPlatformConnectorClient2BearerTokenProvider tokenProvider, + string environmentId) + { + this._baseUrlStr = GetBaseUrlStr(baseUrl ?? throw new ArgumentNullException(nameof(baseUrl))); + this._httpMessageInvoker = httpMessageInvoker ?? throw new ArgumentNullException(nameof(httpMessageInvoker)); + this._tokenProvider = tokenProvider ?? throw new ArgumentNullException(nameof(tokenProvider)); + this._environmentId = environmentId ?? throw new ArgumentNullException(nameof(environmentId)); + + static string GetBaseUrlStr(Uri uri) + { + var str = uri.GetLeftPart(UriPartial.Path); + + // Note (shgogna): Ensure the base URL does NOT end with "/". + // This will allow us to concatenate the operation path to the base URL + // without worrying about the "/". + str = str.TrimEnd('/'); + return str; + } + } + + // + public Task SendAsync( + HttpMethod method, + string operationPathAndQuery, + IEnumerable>> headers, + HttpContent content, + CancellationToken cancellationToken) + { + return this.SendAsync( + method: method, + operationPathAndQuery: operationPathAndQuery, + headers: headers, + content: content, + diagnosticOptions: null, + cancellationToken: cancellationToken); + } + + // + public async Task SendAsync( + HttpMethod method, + string operationPathAndQuery, + IEnumerable>> headers, + HttpContent content, + PowerPlatformConnectorClient2DiagnosticOptions diagnosticOptions, + CancellationToken cancellationToken) + { + if (operationPathAndQuery is null) + { + throw new ArgumentNullException(nameof(operationPathAndQuery)); + } + + var authToken = await this._tokenProvider(cancellationToken).ConfigureAwait(false); + + var uri = this.CombineBaseUrlWithOperationPathAndQuery(operationPathAndQuery); + using (var req = new HttpRequestMessage(method, uri)) + { + req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken); + + this.AddDiagnosticHeaders(diagnosticOptions, req); + + foreach (var header in headers) + { + req.Headers.Add(header.Key, header.Value); + } + + req.Content = content; + return await this._httpMessageInvoker.SendAsync(req, cancellationToken).ConfigureAwait(false); + } + } + + private Uri CombineBaseUrlWithOperationPathAndQuery(string operationPathAndQuery) + { + if (operationPathAndQuery.StartsWith("/", StringComparison.Ordinal)) + { + return new Uri(this._baseUrlStr + operationPathAndQuery); + } + else + { + return new Uri(this._baseUrlStr + "/" + operationPathAndQuery); + } + } + + private void AddDiagnosticHeaders( + PowerPlatformConnectorClient2DiagnosticOptions diagnosticOptions, + HttpRequestMessage req) + { + var userAgent = string.IsNullOrWhiteSpace(diagnosticOptions?.UserAgent) + ? $"PowerFx/{PowerPlatformConnectorClient.Version}" + : $"{diagnosticOptions.UserAgent} PowerFx/{PowerPlatformConnectorClient.Version}"; + + var clientRequestId = string.IsNullOrWhiteSpace(diagnosticOptions?.ClientRequestId) + ? Guid.NewGuid().ToString() + : diagnosticOptions.ClientRequestId; + + // CorrelationID can be the same as ClientRequestID + var correlationId = string.IsNullOrWhiteSpace(diagnosticOptions?.CorrelationId) + ? clientRequestId + : diagnosticOptions.CorrelationId; + + req.Headers.Add("User-Agent", userAgent); + req.Headers.Add("x-ms-user-agent", userAgent); + req.Headers.Add("x-ms-client-environment-id", $"/providers/Microsoft.PowerApps/environments/{this._environmentId}"); + req.Headers.Add("x-ms-client-request-id", clientRequestId); + req.Headers.Add("x-ms-correlation-id", correlationId); + + if (!string.IsNullOrWhiteSpace(diagnosticOptions?.ClientSessionId)) + { + req.Headers.Add("x-ms-client-session-id", diagnosticOptions.ClientSessionId); + } + + if (!string.IsNullOrWhiteSpace(diagnosticOptions?.ClientTenantId)) + { + req.Headers.Add("x-ms-client-tenant-id", diagnosticOptions.ClientTenantId); + } + + if (!string.IsNullOrWhiteSpace(diagnosticOptions?.ClientObjectId)) + { + req.Headers.Add("x-ms-client-object-id", diagnosticOptions.ClientObjectId); + } + } + + private static Uri GetBaseUrlFromOpenApiDocument(OpenApiDocument document) + { + ConnectorErrors errors = new ConnectorErrors(); + Uri uri = document.GetFirstServerUri(errors); + + if (uri is null) + { + errors.AddError("Swagger document doesn't contain an endpoint"); + } + + if (errors.HasErrors) + { + throw new PowerFxConnectorException(string.Join(", ", errors.Errors)); + } + + return uri; + } + } +} diff --git a/src/libraries/Microsoft.PowerFx.Connectors/PowerPlatformConnectorClient2DiagnosticOptions.cs b/src/libraries/Microsoft.PowerFx.Connectors/PowerPlatformConnectorClient2DiagnosticOptions.cs new file mode 100644 index 0000000000..a4b29564e5 --- /dev/null +++ b/src/libraries/Microsoft.PowerFx.Connectors/PowerPlatformConnectorClient2DiagnosticOptions.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Microsoft.PowerFx.Connectors +{ + public class PowerPlatformConnectorClient2DiagnosticOptions + { + public string ClientSessionId { get; set; } + + public string ClientRequestId { get; set; } + + public string ClientTenantId { get; set; } + + public string ClientObjectId { get; set; } + + public string CorrelationId { get; set; } + + public string UserAgent { get; set; } + } +} From 4fc8508f34353b04249132cdf842552a39891473 Mon Sep 17 00:00:00 2001 From: Shubham Gogna Date: Wed, 14 May 2025 23:11:30 -0700 Subject: [PATCH 2/4] Remove method that was originally part of the interface, but was removed --- .../PowerPlatformConnectorClient2.cs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/libraries/Microsoft.PowerFx.Connectors/PowerPlatformConnectorClient2.cs b/src/libraries/Microsoft.PowerFx.Connectors/PowerPlatformConnectorClient2.cs index 36df514b85..75886b4486 100644 --- a/src/libraries/Microsoft.PowerFx.Connectors/PowerPlatformConnectorClient2.cs +++ b/src/libraries/Microsoft.PowerFx.Connectors/PowerPlatformConnectorClient2.cs @@ -69,23 +69,6 @@ static string GetBaseUrlStr(Uri uri) } } - // - public Task SendAsync( - HttpMethod method, - string operationPathAndQuery, - IEnumerable>> headers, - HttpContent content, - CancellationToken cancellationToken) - { - return this.SendAsync( - method: method, - operationPathAndQuery: operationPathAndQuery, - headers: headers, - content: content, - diagnosticOptions: null, - cancellationToken: cancellationToken); - } - // public async Task SendAsync( HttpMethod method, From cf0dd38b6a763ea061ba49d5987743f9b68b7910 Mon Sep 17 00:00:00 2001 From: Shubham Gogna Date: Thu, 15 May 2025 00:10:44 -0700 Subject: [PATCH 3/4] Add SendAsync method overload for HttpRequestMessage instead of message parts --- .../IPowerPlatformConnectorClient2.cs | 12 +++++ .../PowerPlatformConnectorClient2.cs | 49 ++++++++++++++++++- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/libraries/Microsoft.PowerFx.Connectors/IPowerPlatformConnectorClient2.cs b/src/libraries/Microsoft.PowerFx.Connectors/IPowerPlatformConnectorClient2.cs index 65722390e9..37bc396f78 100644 --- a/src/libraries/Microsoft.PowerFx.Connectors/IPowerPlatformConnectorClient2.cs +++ b/src/libraries/Microsoft.PowerFx.Connectors/IPowerPlatformConnectorClient2.cs @@ -18,6 +18,18 @@ public delegate Task PowerPlatformConnectorClient2BearerTokenProvider( public interface IPowerPlatformConnectorClient2 { + /// + /// Sends an HTTP request to the Power Platform connector. + /// + /// HTTP request message. URI path will be used as a relative path for the connector request. + /// Diagnostic options. + /// Cancellation token. + /// . + Task SendAsync( + HttpRequestMessage requestMessage, + PowerPlatformConnectorClient2DiagnosticOptions diagnosticOptions, + CancellationToken cancellationToken); + /// /// Sends an HTTP request to the Power Platform connector. /// diff --git a/src/libraries/Microsoft.PowerFx.Connectors/PowerPlatformConnectorClient2.cs b/src/libraries/Microsoft.PowerFx.Connectors/PowerPlatformConnectorClient2.cs index 75886b4486..6d225c5edc 100644 --- a/src/libraries/Microsoft.PowerFx.Connectors/PowerPlatformConnectorClient2.cs +++ b/src/libraries/Microsoft.PowerFx.Connectors/PowerPlatformConnectorClient2.cs @@ -70,7 +70,29 @@ static string GetBaseUrlStr(Uri uri) } // - public async Task SendAsync( + public Task SendAsync( + HttpRequestMessage requestMessage, + PowerPlatformConnectorClient2DiagnosticOptions diagnosticOptions, + CancellationToken cancellationToken) + { + if (requestMessage is null) + { + throw new ArgumentNullException(nameof(requestMessage)); + } + + var uri = this.CombineBaseUrlWithOperationPathAndQuery(requestMessage.RequestUri.PathAndQuery); + + return this.InternalSendAsync( + requestMessage.Method, + uri, + requestMessage.Headers, + requestMessage.Content, + diagnosticOptions, + cancellationToken); + } + + // + public Task SendAsync( HttpMethod method, string operationPathAndQuery, IEnumerable>> headers, @@ -83,9 +105,32 @@ public async Task SendAsync( throw new ArgumentNullException(nameof(operationPathAndQuery)); } + var uri = this.CombineBaseUrlWithOperationPathAndQuery(operationPathAndQuery); + + if (!uri.AbsoluteUri.StartsWith(this._baseUrlStr, StringComparison.Ordinal)) + { + throw new ArgumentException("Path traversal detected during combination of base URL path and operation path.", nameof(operationPathAndQuery)); + } + + return this.InternalSendAsync( + method, + uri, + headers, + content, + diagnosticOptions, + cancellationToken); + } + + private async Task InternalSendAsync( + HttpMethod method, + Uri uri, + IEnumerable>> headers, + HttpContent content, + PowerPlatformConnectorClient2DiagnosticOptions diagnosticOptions, + CancellationToken cancellationToken) + { var authToken = await this._tokenProvider(cancellationToken).ConfigureAwait(false); - var uri = this.CombineBaseUrlWithOperationPathAndQuery(operationPathAndQuery); using (var req = new HttpRequestMessage(method, uri)) { req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken); From 20fce4a899d814245de28df876a72e0fee782f19 Mon Sep 17 00:00:00 2001 From: Shubham Gogna Date: Thu, 15 May 2025 10:29:05 -0700 Subject: [PATCH 4/4] Add Extension --- .../IPowerPlatformConnectorClient2.cs | 12 ----- ...PowerPlatformConnectorClient2Extensions.cs | 46 +++++++++++++++++++ .../PowerPlatformConnectorClient2.cs | 22 --------- 3 files changed, 46 insertions(+), 34 deletions(-) create mode 100644 src/libraries/Microsoft.PowerFx.Connectors/IPowerPlatformConnectorClient2Extensions.cs diff --git a/src/libraries/Microsoft.PowerFx.Connectors/IPowerPlatformConnectorClient2.cs b/src/libraries/Microsoft.PowerFx.Connectors/IPowerPlatformConnectorClient2.cs index 37bc396f78..65722390e9 100644 --- a/src/libraries/Microsoft.PowerFx.Connectors/IPowerPlatformConnectorClient2.cs +++ b/src/libraries/Microsoft.PowerFx.Connectors/IPowerPlatformConnectorClient2.cs @@ -18,18 +18,6 @@ public delegate Task PowerPlatformConnectorClient2BearerTokenProvider( public interface IPowerPlatformConnectorClient2 { - /// - /// Sends an HTTP request to the Power Platform connector. - /// - /// HTTP request message. URI path will be used as a relative path for the connector request. - /// Diagnostic options. - /// Cancellation token. - /// . - Task SendAsync( - HttpRequestMessage requestMessage, - PowerPlatformConnectorClient2DiagnosticOptions diagnosticOptions, - CancellationToken cancellationToken); - /// /// Sends an HTTP request to the Power Platform connector. /// diff --git a/src/libraries/Microsoft.PowerFx.Connectors/IPowerPlatformConnectorClient2Extensions.cs b/src/libraries/Microsoft.PowerFx.Connectors/IPowerPlatformConnectorClient2Extensions.cs new file mode 100644 index 0000000000..bdd9561351 --- /dev/null +++ b/src/libraries/Microsoft.PowerFx.Connectors/IPowerPlatformConnectorClient2Extensions.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerFx.Connectors +{ + public static class IPowerPlatformConnectorClient2Extensions + { + /// + /// Sends an HTTP request to the Power Platform connector. + /// + /// Client. + /// HTTP request message. URI path will be used as a relative path for the connector request. + /// Diagnostic options. + /// Cancellation token. + /// . + public static Task SendAsync( + this IPowerPlatformConnectorClient2 client, + HttpRequestMessage requestMessage, + PowerPlatformConnectorClient2DiagnosticOptions diagnosticOptions, + CancellationToken cancellationToken) + { + if (client is null) + { + throw new ArgumentNullException(nameof(client)); + } + + if (requestMessage is null) + { + throw new ArgumentNullException(nameof(requestMessage)); + } + + return client.SendAsync( + requestMessage.Method, + requestMessage.RequestUri.PathAndQuery, + requestMessage.Headers, + requestMessage.Content, + diagnosticOptions, + cancellationToken); + } + } +} diff --git a/src/libraries/Microsoft.PowerFx.Connectors/PowerPlatformConnectorClient2.cs b/src/libraries/Microsoft.PowerFx.Connectors/PowerPlatformConnectorClient2.cs index 6d225c5edc..3836f3dad9 100644 --- a/src/libraries/Microsoft.PowerFx.Connectors/PowerPlatformConnectorClient2.cs +++ b/src/libraries/Microsoft.PowerFx.Connectors/PowerPlatformConnectorClient2.cs @@ -69,28 +69,6 @@ static string GetBaseUrlStr(Uri uri) } } - // - public Task SendAsync( - HttpRequestMessage requestMessage, - PowerPlatformConnectorClient2DiagnosticOptions diagnosticOptions, - CancellationToken cancellationToken) - { - if (requestMessage is null) - { - throw new ArgumentNullException(nameof(requestMessage)); - } - - var uri = this.CombineBaseUrlWithOperationPathAndQuery(requestMessage.RequestUri.PathAndQuery); - - return this.InternalSendAsync( - requestMessage.Method, - uri, - requestMessage.Headers, - requestMessage.Content, - diagnosticOptions, - cancellationToken); - } - // public Task SendAsync( HttpMethod method,