Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Delegate for providing a bearer token for authentication.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Token without "Bearer" scheme as the prefix.</returns>
public delegate Task<string> PowerPlatformConnectorClient2BearerTokenProvider(
CancellationToken cancellationToken);

public interface IPowerPlatformConnectorClient2
{
/// <summary>
/// Sends an HTTP request to the Power Platform connector.
/// </summary>
/// <param name="method">HTTP method.</param>
/// <param name="operationPathAndQuery">Operation path and query string.</param>
/// <param name="headers">Headers.</param>
/// <param name="content">Content.</param>
/// <param name="diagnosticOptions">Diagnostic options.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns><see cref="HttpResponseMessage"/>.</returns>
Task<HttpResponseMessage> SendAsync(
HttpMethod method,
string operationPathAndQuery,
IEnumerable<KeyValuePair<string, IEnumerable<string>>> headers,
HttpContent content,
PowerPlatformConnectorClient2DiagnosticOptions diagnosticOptions,
CancellationToken cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Sends an HTTP request to the Power Platform connector.
/// </summary>
/// <param name="client">Client.</param>
/// <param name="requestMessage">HTTP request message. URI path will be used as a relative path for the connector request.</param>
/// <param name="diagnosticOptions">Diagnostic options.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns><see cref="HttpResponseMessage"/>.</returns>
public static Task<HttpResponseMessage> 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);
}
}
}
18 changes: 14 additions & 4 deletions src/libraries/Microsoft.PowerFx.Connectors/OpenApiExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Uri, string> 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)
{
Expand All @@ -72,8 +83,7 @@ private static string GetUriElement(this OpenApiDocument openApiDocument, Func<U
// This is a full URL that will pull in 'basePath' property from connectors.
// Extract BasePath back out from this.
var fullPath = openApiDocument.Servers[0].Url;
var uri = new Uri(fullPath);
return getElement(uri);
return new Uri(fullPath);

default:
errors.AddError($"Multiple servers in OpenApiDocument is not supported");
Expand Down Expand Up @@ -704,7 +714,7 @@ private static ConnectorType TryGetOptionSet(ISwaggerParameter openApiParameter,

if (settings.Settings.Compatibility.IsCDP() || schema.Format == "enum" || settings.Settings.SupportXMsEnumValues)
{
// Try getting enum from 'x-ms-enum-values'
// Try getting enum from 'x-ms-enum-values'
(IEnumerable<KeyValuePair<DName, DName>> list, bool isNumber) = openApiParameter.GetEnumValues();

if (list != null && list.Any())
Expand Down Expand Up @@ -1205,7 +1215,7 @@ internal static Dictionary<string, IConnectorExtensionValue> 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));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
// 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
{
/// <summary>
/// Client for invoking operations of Power Platform connectors.
/// </summary>
public class PowerPlatformConnectorClient2 : IPowerPlatformConnectorClient2
{
private readonly string _baseUrlStr;
private readonly HttpMessageInvoker _httpMessageInvoker;
private readonly PowerPlatformConnectorClient2BearerTokenProvider _tokenProvider;
private readonly string _environmentId;

/// <summary>
/// Initializes a new instance of the <see cref="PowerPlatformConnectorClient2"/> class.
/// </summary>
/// <param name="document">Document used for extracting the base URL.</param>
/// <param name="httpMessageInvoker">HTTP message invoker.</param>
/// <param name="tokenProvider">Bearer token provider.</param>
/// <param name="environmentId">Environment ID.</param>
public PowerPlatformConnectorClient2(
OpenApiDocument document,
HttpMessageInvoker httpMessageInvoker,
PowerPlatformConnectorClient2BearerTokenProvider tokenProvider,
string environmentId)
: this(GetBaseUrlFromOpenApiDocument(document), httpMessageInvoker, tokenProvider, environmentId)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="PowerPlatformConnectorClient2"/> class.
/// </summary>
/// <param name="baseUrl">Base URL for requests.</param>
/// <param name="httpMessageInvoker">HTTP message invoker.</param>
/// <param name="tokenProvider">Bearer token provider.</param>
/// <param name="environmentId">Environment ID.</param>
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;
}
}

// <inheritdoc />
public Task<HttpResponseMessage> SendAsync(
HttpMethod method,
string operationPathAndQuery,
IEnumerable<KeyValuePair<string, IEnumerable<string>>> headers,
HttpContent content,
PowerPlatformConnectorClient2DiagnosticOptions diagnosticOptions,
CancellationToken cancellationToken)
{
if (operationPathAndQuery is null)
{
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<HttpResponseMessage> InternalSendAsync(
HttpMethod method,
Uri uri,
IEnumerable<KeyValuePair<string, IEnumerable<string>>> headers,
HttpContent content,
PowerPlatformConnectorClient2DiagnosticOptions diagnosticOptions,
CancellationToken cancellationToken)
{
var authToken = await this._tokenProvider(cancellationToken).ConfigureAwait(false);

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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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; }
}
}
Loading