diff --git a/.publicApi/Microsoft.ApplicationInsights.dll/net452/PublicAPI.Unshipped.txt b/.publicApi/Microsoft.ApplicationInsights.dll/net452/PublicAPI.Unshipped.txt index fe86d4bbb0..44ef0ab106 100644 --- a/.publicApi/Microsoft.ApplicationInsights.dll/net452/PublicAPI.Unshipped.txt +++ b/.publicApi/Microsoft.ApplicationInsights.dll/net452/PublicAPI.Unshipped.txt @@ -1,3 +1,5 @@ +Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration.SetAzureTokenCredential(object tokenCredential) -> void + Microsoft.ApplicationInsights.Channel.IAsyncFlushable Microsoft.ApplicationInsights.Channel.IAsyncFlushable.FlushAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task Microsoft.ApplicationInsights.Channel.InMemoryChannel.FlushAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task diff --git a/.publicApi/Microsoft.ApplicationInsights.dll/net46/PublicAPI.Unshipped.txt b/.publicApi/Microsoft.ApplicationInsights.dll/net46/PublicAPI.Unshipped.txt index fe86d4bbb0..44ef0ab106 100644 --- a/.publicApi/Microsoft.ApplicationInsights.dll/net46/PublicAPI.Unshipped.txt +++ b/.publicApi/Microsoft.ApplicationInsights.dll/net46/PublicAPI.Unshipped.txt @@ -1,3 +1,5 @@ +Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration.SetAzureTokenCredential(object tokenCredential) -> void + Microsoft.ApplicationInsights.Channel.IAsyncFlushable Microsoft.ApplicationInsights.Channel.IAsyncFlushable.FlushAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task Microsoft.ApplicationInsights.Channel.InMemoryChannel.FlushAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task diff --git a/.publicApi/Microsoft.ApplicationInsights.dll/netstandard2.0/PublicAPI.Unshipped.txt b/.publicApi/Microsoft.ApplicationInsights.dll/netstandard2.0/PublicAPI.Unshipped.txt index fe86d4bbb0..44ef0ab106 100644 --- a/.publicApi/Microsoft.ApplicationInsights.dll/netstandard2.0/PublicAPI.Unshipped.txt +++ b/.publicApi/Microsoft.ApplicationInsights.dll/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,3 +1,5 @@ +Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration.SetAzureTokenCredential(object tokenCredential) -> void + Microsoft.ApplicationInsights.Channel.IAsyncFlushable Microsoft.ApplicationInsights.Channel.IAsyncFlushable.FlushAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task Microsoft.ApplicationInsights.Channel.InMemoryChannel.FlushAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task diff --git a/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/Extensibility/Implementation/Authentication/MockCredential.cs b/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/Extensibility/Implementation/Authentication/MockCredential.cs new file mode 100644 index 0000000000..fff204b43a --- /dev/null +++ b/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/Extensibility/Implementation/Authentication/MockCredential.cs @@ -0,0 +1,27 @@ +#if NET461 || NETCOREAPP2_1 || NETCOREAPP3_1 || NET5_0 +namespace Microsoft.ApplicationInsights.TestFramework.Extensibility.Implementation.Authentication +{ + using System; + using System.Threading; + using System.Threading.Tasks; + + using Azure.Core; + + + /// + /// Copied from (https://github.com/Azure/azure-sdk-for-net/blob/master/sdk/core/Azure.Core.TestFramework/src/MockCredential.cs). + /// + public class MockCredential : TokenCredential + { + public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + return new ValueTask(GetToken(requestContext, cancellationToken)); + } + + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + return new AccessToken("TEST TOKEN " + string.Join(" ", requestContext.Scopes), DateTimeOffset.MaxValue); + } + } +} +#endif diff --git a/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/Extensibility/Implementation/Authentication/ReflectionCredentialEnvelopeTests.cs b/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/Extensibility/Implementation/Authentication/ReflectionCredentialEnvelopeTests.cs new file mode 100644 index 0000000000..75535b7289 --- /dev/null +++ b/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/Extensibility/Implementation/Authentication/ReflectionCredentialEnvelopeTests.cs @@ -0,0 +1,203 @@ +#if !NET452 && !NET46 +namespace Microsoft.ApplicationInsights.TestFramework.Extensibility.Implementation.Authentication +{ + using System; + using System.Threading; + using System.Threading.Tasks; + + using Azure.Core; + + using Microsoft.ApplicationInsights.Extensibility.Implementation.Authentication; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + using Moq; + + /// + /// The cannot take a dependency on . + /// We must use reflection to interact with this class. + /// These tests are to confirm that we can correctly identity classes that implement TokenCredential and address it's methods. + /// + /// + /// These tests do not run in NET452 OR NET46. + /// In these cases, the test runner is NET452 or NET46 and Azure.Core.TokenCredential is NOT SUPPORTED in these frameworks. + /// This does not affect the end user because we REQUIRE the end user to create their own instance of TokenCredential. + /// This ensures that the end user is consuming the AI SDK in one of the newer frameworks. + /// + [TestClass] + [TestCategory("AAD")] + public class ReflectionCredentialEnvelopeTests + { + [TestMethod] + public void VerifyCanIdentifyValidClass() + { + var testClass2 = new TestClassInheritsTokenCredential(); + _ = new ReflectionCredentialEnvelope(testClass2); + // NO ASSERT. This test is valid if no exception is thrown. :) + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void VerifyCanIdentityInvalidClass() + { + var notTokenCredential2 = new NotTokenCredential2(); + _ = new ReflectionCredentialEnvelope(notTokenCredential2); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void VerifyCannotSetInvalidType() + { + _ = new ReflectionCredentialEnvelope(Guid.Empty); + } + + [TestMethod] + public void VerifyCanMakeTokenRequestContext() + { + var testScope = new string[] { "test/scope" }; + + var requestContext = new TokenRequestContext(testScope); + + var tokenRequestContextViaReflection = ReflectionCredentialEnvelope.AzureCore.MakeTokenRequestContext(testScope); + Assert.IsInstanceOfType(tokenRequestContextViaReflection, typeof(TokenRequestContext)); + Assert.AreEqual(requestContext, tokenRequestContextViaReflection); + } + + [TestMethod] + public void VerifyGetToken_UsingCompileTimeTypes() + { + var mockCredential = new MockCredential(); + var requestContext = new TokenRequestContext(new string[] { "test/scope" }); + + var testResult = ReflectionCredentialEnvelope.AzureCore.InvokeGetToken(mockCredential, requestContext, CancellationToken.None); + + Assert.AreEqual("TEST TOKEN test/scope", testResult); + } + + [TestMethod] + public async Task VerifyGetTokenAsync_UsingCompileTimeTypes() + { + var mockCredential = new MockCredential(); + var requestContext = new TokenRequestContext(new string[] { "test/scope" }); + + var testResult = await ReflectionCredentialEnvelope.AzureCore.InvokeGetTokenAsync(mockCredential, requestContext, CancellationToken.None); + + Assert.AreEqual("TEST TOKEN test/scope", testResult); + } + + /// + /// This more closely represents how this would be used in a production environment. + /// + [TestMethod] + public void VerifyGetToken_UsingDynamicTypes() + { + var mockCredential = (object)new MockCredential(); + var requestContext = ReflectionCredentialEnvelope.AzureCore.MakeTokenRequestContext(new[] { "test/scope" }); + + var testResult = ReflectionCredentialEnvelope.AzureCore.InvokeGetToken(mockCredential, requestContext, CancellationToken.None); + + Assert.AreEqual("TEST TOKEN test/scope", testResult); + } + + /// + /// This more closely represents how this would be used in a production environment. + /// + [TestMethod] + public async Task VerifyGetTokenAsync_UsingDynamicTypes() + { + var mockCredential = (object)new MockCredential(); + var requestContext = ReflectionCredentialEnvelope.AzureCore.MakeTokenRequestContext(new[] { "test/scope" }); + + var testResult = await ReflectionCredentialEnvelope.AzureCore.InvokeGetTokenAsync(mockCredential, requestContext, CancellationToken.None); + + Assert.AreEqual("TEST TOKEN test/scope", testResult); + } + + /// + /// This test verifies that both and return identical tokens. + /// + [TestMethod] + public void VerifyGetToken_ReturnsValidToken() + { + var requestContext = new TokenRequestContext(scopes: CredentialConstants.GetScopes()); + var mockCredential = new MockCredential(); + var tokenUsingTypes = mockCredential.GetToken(requestContext, CancellationToken.None); + + var reflectionCredentialEnvelope = new ReflectionCredentialEnvelope(mockCredential); + var tokenUsingReflection = reflectionCredentialEnvelope.GetToken(); + + Assert.AreEqual(tokenUsingTypes.Token, tokenUsingReflection); + } + + /// + /// This test verifies that both and return identical tokens. + /// + [TestMethod] + public async Task VerifyGetTokenAsync_ReturnsValidToken() + { + var requestContext = new TokenRequestContext(scopes: CredentialConstants.GetScopes()); + var mockCredential = new MockCredential(); + var tokenUsingTypes = await mockCredential.GetTokenAsync(requestContext, CancellationToken.None); + + var reflectionCredentialEnvelope = new ReflectionCredentialEnvelope(mockCredential); + var tokenUsingReflection = await reflectionCredentialEnvelope.GetTokenAsync(); + + Assert.AreEqual(tokenUsingTypes.Token, tokenUsingReflection); + } + + [TestMethod] + public void VerifyGetToken_IfCredentialThrowsException_EnvelopeReturnsNull() + { + Mock mockTokenCredential = new Mock(); + mockTokenCredential.Setup(x => x.GetToken(It.IsAny(), It.IsAny())).Throws(new NotImplementedException()); + var mockCredential = mockTokenCredential.Object; + + var reflectionCredentialEnvelope = new ReflectionCredentialEnvelope(mockCredential); + var token = reflectionCredentialEnvelope.GetToken(); + Assert.IsNull(token); + } + + [TestMethod] + public async Task VerifyGetTokenAsync_IfCredentialThrowsException_EnvelopeReturnsNull() + { + Mock mockTokenCredential = new Mock(); + mockTokenCredential.Setup(x => x.GetTokenAsync(It.IsAny(), It.IsAny())).Throws(new NotImplementedException()); + var mockCredential = mockTokenCredential.Object; + + var reflectionCredentialEnvelope = new ReflectionCredentialEnvelope(mockCredential); + var token = await reflectionCredentialEnvelope.GetTokenAsync(); + Assert.IsNull(token); + } + +#region TestClasses + + /// + /// This class inherits which inherits . + /// This class is used to verify that the can correctly identify tests that inherit . + /// + private class TestClassInheritsTokenCredential : MockCredential { } + + private abstract class NotTokenCredential + { + public abstract AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken); + + public abstract ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken); + } + + private class NotTokenCredential1 : NotTokenCredential + { + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + } + + private class NotTokenCredential2 : NotTokenCredential1 { } +#endregion + } +} +#endif diff --git a/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/Extensibility/Implementation/Authentication/TelemetryConfigurationCredentialEnvelopeTests.cs b/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/Extensibility/Implementation/Authentication/TelemetryConfigurationCredentialEnvelopeTests.cs new file mode 100644 index 0000000000..b3394e32bc --- /dev/null +++ b/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/Extensibility/Implementation/Authentication/TelemetryConfigurationCredentialEnvelopeTests.cs @@ -0,0 +1,52 @@ +#if !NET452 && !NET46 +namespace Microsoft.ApplicationInsights.TestFramework.Extensibility.Implementation.Authentication +{ + using System; + using System.Threading.Tasks; + + using Microsoft.ApplicationInsights.Extensibility; + using Microsoft.ApplicationInsights.Extensibility.Implementation.Authentication; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + /// + /// These tests verify that can receive and store an instance of . + /// + /// + /// These tests do not run in NET452 OR NET46. + /// In these cases, the test runner is NET452 or NET46 and Azure.Core.TokenCredential is NOT SUPPORTED in these frameworks. + /// This does not affect the end user because we REQUIRE the end user to create their own instance of TokenCredential. + /// This ensures that the end user is consuming the AI SDK in one of the newer frameworks. + /// + [TestClass] + [TestCategory("AAD")] + public class TelemetryConfigurationCredentialEnvelopeTests + { + /// + /// This tests verifies that each supported language can create and set a Credential. + /// + [TestMethod] + public void VerifyCanSetCredential() + { + var mockCredential = new MockCredential(); + + var telemetryConfiguration = new TelemetryConfiguration(); + telemetryConfiguration.SetAzureTokenCredential(mockCredential); + + Assert.IsInstanceOfType(telemetryConfiguration.CredentialEnvelope, typeof(ReflectionCredentialEnvelope)); + Assert.AreEqual(mockCredential, telemetryConfiguration.CredentialEnvelope.Credential, "Credential should be the same instance that we pass in."); + } + + /// + /// TelemetryConfiguration accepts an parameter, and uses reflection to verify the type at runtime + /// This test is to verify that we cannot set invalid types. + /// + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void VerifyCannotSetInvalidObjectOnTelemetryConfiguration() + { + var telemetryConfiguration = new TelemetryConfiguration(); + telemetryConfiguration.SetAzureTokenCredential(Guid.Empty); + } + } +} +#endif diff --git a/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/Microsoft.ApplicationInsights.Tests.csproj b/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/Microsoft.ApplicationInsights.Tests.csproj index 6986444096..a333ed8d41 100644 --- a/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/Microsoft.ApplicationInsights.Tests.csproj +++ b/BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/Microsoft.ApplicationInsights.Tests.csproj @@ -41,6 +41,11 @@ + + + + + diff --git a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/Authentication/CredentialConstants.cs b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/Authentication/CredentialConstants.cs new file mode 100644 index 0000000000..3d6f253606 --- /dev/null +++ b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/Authentication/CredentialConstants.cs @@ -0,0 +1,24 @@ +namespace Microsoft.ApplicationInsights.Extensibility.Implementation.Authentication +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + + internal static class CredentialConstants + { + /// + /// Source: + /// (https://docs.microsoft.com/azure/active-directory/develop/msal-acquire-cache-tokens#scopes-when-acquiring-tokens). + /// (https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#the-default-scope). + /// + public const string AzureMonitorScope = "https://monitor.azure.com//.default"; // TODO: THIS SCOPE IS UNVERIFIED. WAITING FOR SERVICES TEAM TO PROVIDE AN INT ENVIRONMENT FOR E2E TESTING. + + /// + /// Get scopes for Azure Monitor as an array. + /// + /// An array of scopes. + public static string[] GetScopes() => new string[] { AzureMonitorScope }; + } +} diff --git a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/Authentication/ICredentialEnvelope.cs b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/Authentication/ICredentialEnvelope.cs new file mode 100644 index 0000000000..7deca591c6 --- /dev/null +++ b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/Authentication/ICredentialEnvelope.cs @@ -0,0 +1,27 @@ +namespace Microsoft.ApplicationInsights.Extensibility.Implementation.Authentication +{ + using System.Threading; + using System.Threading.Tasks; + + internal interface ICredentialEnvelope + { + /// + /// Gets the TokenCredential instance held by this class. + /// + object Credential { get; } + + /// + /// Gets an Azure.Core.AccessToken. + /// + /// The System.Threading.CancellationToken to use. + /// A valid Azure.Core.AccessToken. + string GetToken(CancellationToken cancellationToken = default); + + /// + /// Gets an Azure.Core.AccessToken. + /// + /// The System.Threading.CancellationToken to use. + /// A valid Azure.Core.AccessToken. + Task GetTokenAsync(CancellationToken cancellationToken = default); + } +} diff --git a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/Authentication/ReflectionCredentialEnvelope.cs b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/Authentication/ReflectionCredentialEnvelope.cs new file mode 100644 index 0000000000..6546b69d9a --- /dev/null +++ b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/Authentication/ReflectionCredentialEnvelope.cs @@ -0,0 +1,244 @@ +namespace Microsoft.ApplicationInsights.Extensibility.Implementation.Authentication +{ + using System; + using System.Linq.Expressions; + using System.Threading; + using System.Threading.Tasks; + + using Microsoft.ApplicationInsights.Extensibility.Implementation.Tracing; + + /// + /// This is an envelope for an instance of Azure.Core.TokenCredential. + /// This class uses reflection to interact with the Azure.Core library. + /// + /// + /// Our SDK currently targets net452, net46, and netstandard2.0. + /// Azure.Core.TokenCredential is only available for netstandard2.0. + /// + internal class ReflectionCredentialEnvelope : ICredentialEnvelope + { + private readonly object tokenCredential; + private readonly object tokenRequestContext; + + /// + /// Create an instance of . + /// + /// An instance of Azure.Core.TokenCredential. + public ReflectionCredentialEnvelope(object tokenCredential) + { + this.tokenCredential = tokenCredential ?? throw new ArgumentNullException(nameof(tokenCredential)); + + if (tokenCredential.GetType().IsSubclassOf(Type.GetType("Azure.Core.TokenCredential, Azure.Core"))) + { + this.tokenRequestContext = AzureCore.MakeTokenRequestContext(scopes: CredentialConstants.GetScopes()); + } + else + { + throw new ArgumentException($"The provided {nameof(tokenCredential)} must inherit Azure.Core.TokenCredential", nameof(tokenCredential)); + } + } + + /// + /// Gets the TokenCredential instance held by this class. + /// + public object Credential => this.tokenCredential; + + /// + /// Gets an Azure.Core.AccessToken. + /// + /// The System.Threading.CancellationToken to use. + /// A valid Azure.Core.AccessToken. + public string GetToken(CancellationToken cancellationToken = default) + { + SdkInternalOperationsMonitor.Enter(); + try + { + return AzureCore.InvokeGetToken(this.tokenCredential, this.tokenRequestContext, cancellationToken); + } + catch (Exception ex) + { + CoreEventSource.Log.FailedToGetToken(ex.ToInvariantString()); + return null; + } + finally + { + SdkInternalOperationsMonitor.Exit(); + } + } + + /// + /// Gets an Azure.Core.AccessToken. + /// + /// The System.Threading.CancellationToken to use. + /// A valid Azure.Core.AccessToken. + public async Task GetTokenAsync(CancellationToken cancellationToken = default) + { + SdkInternalOperationsMonitor.Enter(); + try + { + return await AzureCore.InvokeGetTokenAsync(this.tokenCredential, this.tokenRequestContext, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + CoreEventSource.Log.FailedToGetToken(ex.ToInvariantString()); + return null; + } + finally + { + SdkInternalOperationsMonitor.Exit(); + } + } + + /// + /// This class provides Reflection based wrappers around types found in the Azure.Core library. + /// Because of framework incompatibilities, we cannot take a direct reference on these types. + /// + /// This class uses compiled Expression Trees. Read more here: + /// (https://docs.microsoft.com/dotnet/csharp/programming-guide/concepts/expression-trees/). + /// (https://docs.microsoft.com/dotnet/csharp/expression-trees). + /// + internal static class AzureCore + { + private static readonly Delegate GetTokenValue; + private static readonly Delegate GetTokenAsyncValue; + private static readonly Delegate GetTokenProperty; + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1810:Initialize reference type static fields inline", Justification = "For both optimization and readability, I'm building these objects in the same method.")] + static AzureCore() + { + GetTokenValue = BuildDelegateGetToken(); + + var asyncDelegates = BuildDelegateGetTokenAsync(); + GetTokenAsyncValue = asyncDelegates[0]; + GetTokenProperty = asyncDelegates[1]; + } + + internal static string InvokeGetToken(object tokenCredential, object tokenRequestContext, CancellationToken cancellationToken) + { + return (string)GetTokenValue.DynamicInvoke(tokenCredential, tokenRequestContext, cancellationToken); + } + + internal static async Task InvokeGetTokenAsync(object tokenCredential, object tokenRequestContext, CancellationToken cancellationToken) + { + var task = (Task)GetTokenAsyncValue.DynamicInvoke(tokenCredential, tokenRequestContext, cancellationToken); + await task.ConfigureAwait(false); + return (string)GetTokenProperty.DynamicInvoke(task); + } + + /// + /// This is a wrapper for the following constructor: + /// public TokenRequestContext (string[] scopes, string? parentRequestId = default, string? claims = default); + /// (https://docs.microsoft.com/dotnet/api/azure.core.tokenrequestcontext.-ctor). + /// + internal static object MakeTokenRequestContext(string[] scopes) + { + return Activator.CreateInstance( + type: Type.GetType("Azure.Core.TokenRequestContext, Azure.Core"), + args: new object[] { scopes, null, }); + } + + /// This creates a wrapper for the following method: + /// public abstract Azure.Core.AccessToken GetToken (Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken). + /// (https://docs.microsoft.com/dotnet/api/azure.core.tokencredential.gettoken). + private static Delegate BuildDelegateGetToken() + { + Type typeTokenCredential = Type.GetType("Azure.Core.TokenCredential, Azure.Core"); + Type typeTokenRequestContext = Type.GetType("Azure.Core.TokenRequestContext, Azure.Core"); + Type typeCancellationToken = typeof(CancellationToken); + + var parameterExpression_tokenCredential = Expression.Parameter(type: typeTokenCredential, name: "parameterExpression_TokenCredential"); + var parameterExpression_requestContext = Expression.Parameter(type: typeTokenRequestContext, name: "parameterExpression_RequestContext"); + var parameterExpression_cancellationToken = Expression.Parameter(type: typeCancellationToken, name: "parameterExpression_CancellationToken"); + + var exprGetToken = Expression.Call( + instance: parameterExpression_tokenCredential, + method: typeTokenCredential.GetMethod(name: "GetToken", types: new Type[] { typeTokenRequestContext, typeCancellationToken }), + arg0: parameterExpression_requestContext, + arg1: parameterExpression_cancellationToken); + + var exprTokenProperty = Expression.Property( + expression: exprGetToken, + propertyName: "Token"); + + return Expression.Lambda( + body: exprTokenProperty, + parameters: new ParameterExpression[] + { + parameterExpression_tokenCredential, + parameterExpression_requestContext, + parameterExpression_cancellationToken, + }).Compile(); + } + + /// + /// This is a wrapper for the following method: + /// public abstract System.Threading.Tasks.ValueTask<Azure.Core.AccessToken> GetTokenAsync (Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken); + /// (https://docs.microsoft.com/dotnet/api/azure.core.tokencredential.gettokenasync). + /// + /// + /// The Expression Tree library cannot handle async methods. + /// As a workaround, this method returns two Delegates. + /// First; + /// The first Delegate is a wrapper around GetTokenAsync which returns a ValueTask of AccessToken. + /// Then calls ValueTask.GetTask to convert that to a Task which is a known type for older frameworks. + /// This Task can be awaited. + /// Second; + /// The second Delegate is a wrapper around Task.Result which returns the AccessToken. + /// Then calls AccessToken.Token to get the string token. + /// + private static Delegate[] BuildDelegateGetTokenAsync() + { + Type typeTokenCredential = Type.GetType("Azure.Core.TokenCredential, Azure.Core"); + Type typeTokenRequestContext = Type.GetType("Azure.Core.TokenRequestContext, Azure.Core"); + Type typeCancellationToken = typeof(CancellationToken); + + var parameterExpression_TokenCredential = Expression.Parameter(type: typeTokenCredential, name: "parameterExpression_TokenCredential"); + var parameterExpression_RequestContext = Expression.Parameter(type: typeTokenRequestContext, name: "parameterExpression_RequestContext"); + var parameterExpression_CancellationToken = Expression.Parameter(type: typeCancellationToken, name: "parameterExpression_CancellationToken"); + + // public abstract System.Threading.Tasks.ValueTask GetTokenAsync (Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken); + var methodInfo_GetTokenAsync = typeTokenCredential.GetMethod(name: "GetTokenAsync", types: new Type[] { typeTokenRequestContext, typeCancellationToken }); + + var exprGetTokenAsync = Expression.Call( + instance: parameterExpression_TokenCredential, + method: methodInfo_GetTokenAsync, + arg0: parameterExpression_RequestContext, + arg1: parameterExpression_CancellationToken); + + var methodInfo_AsTask = methodInfo_GetTokenAsync.ReturnType.GetMethod("AsTask"); + + var exprAsTask = Expression.Call( + instance: exprGetTokenAsync, + method: methodInfo_AsTask); + + var delegateGetTokenAsync = Expression.Lambda( + body: exprAsTask, + parameters: new ParameterExpression[] + { + parameterExpression_TokenCredential, + parameterExpression_RequestContext, + parameterExpression_CancellationToken, + }).Compile(); + + var parameterExpression_Task = Expression.Parameter(type: methodInfo_AsTask.ReturnType, name: "parameterExpression_Task"); + + var exprResultProperty = Expression.Property( + expression: parameterExpression_Task, + propertyName: "Result"); + + var exprTokenProperty = Expression.Property( + expression: exprResultProperty, + propertyName: "Token"); + + var delegateTokenProperty = Expression.Lambda( + body: exprTokenProperty, + parameters: new ParameterExpression[] + { + parameterExpression_Task, + }).Compile(); + + return new[] { delegateGetTokenAsync, delegateTokenProperty }; + } + } + } +} diff --git a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/Tracing/CoreEventSource.cs b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/Tracing/CoreEventSource.cs index 6c71a54476..fcdec90df0 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/Tracing/CoreEventSource.cs +++ b/BASE/src/Microsoft.ApplicationInsights/Extensibility/Implementation/Tracing/CoreEventSource.cs @@ -659,6 +659,9 @@ public void IngestionResponseTimeEventCounter(float responseDurationInMs) [Event(72, Keywords = Keywords.UserActionable, Message = "Failed to create file for self diagnostics at {0}. Error message: {1}.", Level = EventLevel.Error)] public void SelfDiagnosticsFileCreateException(string logDirectory, string exception, string appDomainName = "Incorrect") => this.WriteEvent(72, logDirectory, exception, this.nameProvider.Name); + [Event(73, Message = "Failed to get AAD Token. Error message: {0}.", Level = EventLevel.Error)] + public void FailedToGetToken(string exception, string appDomainName = "Incorrect") => this.WriteEvent(73, exception, this.nameProvider.Name); + [NonEvent] public void TransmissionStatusEventFailed(Exception ex) { diff --git a/BASE/src/Microsoft.ApplicationInsights/Extensibility/TelemetryConfiguration.cs b/BASE/src/Microsoft.ApplicationInsights/Extensibility/TelemetryConfiguration.cs index 31aa6282f4..2db25dba0a 100644 --- a/BASE/src/Microsoft.ApplicationInsights/Extensibility/TelemetryConfiguration.cs +++ b/BASE/src/Microsoft.ApplicationInsights/Extensibility/TelemetryConfiguration.cs @@ -11,6 +11,7 @@ using Microsoft.ApplicationInsights.DataContracts; using Microsoft.ApplicationInsights.Extensibility.Implementation; using Microsoft.ApplicationInsights.Extensibility.Implementation.ApplicationId; + using Microsoft.ApplicationInsights.Extensibility.Implementation.Authentication; using Microsoft.ApplicationInsights.Extensibility.Implementation.Endpoints; using Microsoft.ApplicationInsights.Extensibility.Implementation.Sampling; using Microsoft.ApplicationInsights.Extensibility.Implementation.Tracing; @@ -334,6 +335,11 @@ public string ConnectionString /// public TelemetrySink DefaultTelemetrySink => this.telemetrySinks.DefaultSink; + /// + /// Gets an envelope for Azure.Core.TokenCredential which provides an AAD Authenticated token. + /// + internal ICredentialEnvelope CredentialEnvelope { get; private set; } + /// /// Gets or sets the chain of processors. /// @@ -399,6 +405,17 @@ public void Dispose() GC.SuppressFinalize(this); } + /// + /// Set a TokenCredential for this configuration. + /// + /// + /// For more information on expected types, review the documentation for the Azure.Identity library. + /// (https://github.com/Azure/azure-sdk-for-net/tree/master/sdk/identity/Azure.Identity). + /// + /// An instance of Azure.Core.TokenCredential. + /// An ArgumentException is thrown if the provided object does not inherit Azure.Core.TokenCredential. + public void SetAzureTokenCredential(object tokenCredential) => this.CredentialEnvelope = new ReflectionCredentialEnvelope(tokenCredential); + internal MetricManager GetMetricManager(bool createIfNotExists) { MetricManager manager = this.metricManager;