diff --git a/src/Microsoft.Performance.SDK.Runtime/ApplicationEnvironment.cs b/src/Microsoft.Performance.SDK.Runtime/ApplicationEnvironment.cs index 1c54e9c..319c76d 100644 --- a/src/Microsoft.Performance.SDK.Runtime/ApplicationEnvironment.cs +++ b/src/Microsoft.Performance.SDK.Runtime/ApplicationEnvironment.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using Microsoft.Performance.SDK.Auth; using Microsoft.Performance.SDK.Extensibility.DataCooking; using Microsoft.Performance.SDK.Extensibility.SourceParsing; using Microsoft.Performance.SDK.Processing; @@ -122,6 +123,14 @@ namespace Microsoft.Performance.SDK.Runtime args); } + /// + public virtual bool TryGetAuthProvider(out IAuthProvider provider) + where TAuth : IAuthMethod + { + provider = null; + return false; + } + private static MessageBoxIcon MapToImage(MessageType messageType) { switch (messageType) diff --git a/src/Microsoft.Performance.SDK.Tests/TestClasses/TestApplicationEnvironment.cs b/src/Microsoft.Performance.SDK.Tests/TestClasses/TestApplicationEnvironment.cs index 6c89acd..b68a987 100644 --- a/src/Microsoft.Performance.SDK.Tests/TestClasses/TestApplicationEnvironment.cs +++ b/src/Microsoft.Performance.SDK.Tests/TestClasses/TestApplicationEnvironment.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using Microsoft.Performance.SDK.Auth; using Microsoft.Performance.SDK.Extensibility.DataCooking; using Microsoft.Performance.SDK.Extensibility.SourceParsing; using Microsoft.Performance.SDK.Processing; @@ -35,5 +36,12 @@ namespace Microsoft.Performance.SDK.Tests.TestClasses { return ButtonResult.None; } + + public bool TryGetAuthProvider(out IAuthProvider provider) + where TAuth : IAuthMethod + { + provider = null; + return false; + } } } diff --git a/src/Microsoft.Performance.SDK/Auth/AzureAuthRequest.cs b/src/Microsoft.Performance.SDK/Auth/AzureAuthRequest.cs new file mode 100644 index 0000000..6e155e6 --- /dev/null +++ b/src/Microsoft.Performance.SDK/Auth/AzureAuthRequest.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Performance.SDK.Auth +{ + /// + /// An that acquires bearer tokens for accessing Azure services. + /// + public sealed class AzureBearerTokenAuth + : IAuthMethod + { + /// + /// Initializes a new instance of the class. + /// + /// + /// The of the Microsoft Entra ID (formerly Azure Active Directory) of the + /// application to authenticate to. + /// + /// + /// The required scopes that the returned bearer token must have. + /// + /// + /// A value indicating whether or not to allow the acquired token to come from a cache. This value may + /// be false if a token for these scopes was previously acquired, but the token does not have access + /// to the required resources. In this case, a different token (e.g. a token for a different user) may be + /// required. + /// + /// + /// A human-readable display name for the application the user is logging into. + /// + /// + /// An optional human-readable description to describe why the authentication is needed. For example, + /// "retrieve data from Azure DevOps". + /// + /// + /// The optional tenant the authenticated principal must be logged into. May be null. + /// + /// + /// The optional domain name of accounts which should be preferred for this authentication request. + /// May be null. + /// + public AzureBearerTokenAuth( + Guid appId, + string[] scopes, + bool allowCache, + string appName, + string authReason, + Guid? tenant, + string preferredAccountDomain) + { + AppId = appId; + Scopes = scopes; + AllowCache = allowCache; + AppName = appName; + AuthReason = authReason; + Tenant = tenant; + PreferredAccountDomain = preferredAccountDomain; + } + + /// + /// Gets the of the Microsoft Entra ID (formerly Azure Active Directory) of the + /// application to authenticate to. + /// + public Guid AppId { get; } + + /// + /// Gets the required scopes that the returned bearer token must have. + /// + public string[] Scopes { get; } + + /// + /// Gets a value indicating whether or not to allow the acquired token to come from a cache. This value may + /// be false if a token for these scopes was previously acquired, but the token does not have access + /// to the required resources. In this case, a different token (e.g. a token for a different user) may be + /// required. + /// + public bool AllowCache { get; } + + /// + /// Gets a human-readable display name for the application the user is logging into. + /// + public string AppName { get; } + + /// + /// Gets an optional human-readable description to describe why the authentication is needed. For example, + /// "retrieve data from Azure DevOps". + /// + public string AuthReason { get; } + + /// + /// Gets the optional tenant the authenticated principal must be logged into. May be null. + /// + public Guid? Tenant { get; } + + /// + /// Gets the optional domain name of accounts which should be preferred for this authentication request. + /// May be null. + /// + public string PreferredAccountDomain { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.Performance.SDK/Auth/IAuthMethod.cs b/src/Microsoft.Performance.SDK/Auth/IAuthMethod.cs new file mode 100644 index 0000000..d062520 --- /dev/null +++ b/src/Microsoft.Performance.SDK/Auth/IAuthMethod.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Performance.SDK.Auth +{ + /// + /// Represents an authentication method that can be used to authenticate to a service. + /// + /// + /// The type of the result of a successful authentication. + /// + public interface IAuthMethod + { + } +} \ No newline at end of file diff --git a/src/Microsoft.Performance.SDK/Auth/IAuthProvider`1.cs b/src/Microsoft.Performance.SDK/Auth/IAuthProvider`1.cs new file mode 100644 index 0000000..6ce45cf --- /dev/null +++ b/src/Microsoft.Performance.SDK/Auth/IAuthProvider`1.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; + +namespace Microsoft.Performance.SDK.Auth +{ + /// + /// Provides authentication for a given . + /// + /// + /// The type of the this provider can authenticate. + /// + /// + /// The type of the result of a successful authentication. + /// + public interface IAuthProvider + where TAuth : IAuthMethod + { + /// + /// Attempts to authenticate the given . + /// + /// + /// The to authenticate. + /// + /// + /// An awaitable task that, upon completion, will contain the result of the authentication request. + /// The result of the authentication will either be an instance of if + /// the authentication was successful, or null if the authentication failed. + /// + Task TryGetAuth(TAuth authRequest); + } +} \ No newline at end of file diff --git a/src/Microsoft.Performance.SDK/Processing/IApplicationEnvironment.cs b/src/Microsoft.Performance.SDK/Processing/IApplicationEnvironment.cs index c1e58b7..887d7fd 100644 --- a/src/Microsoft.Performance.SDK/Processing/IApplicationEnvironment.cs +++ b/src/Microsoft.Performance.SDK/Processing/IApplicationEnvironment.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using Microsoft.Performance.SDK.Auth; using Microsoft.Performance.SDK.Extensibility.DataCooking; using Microsoft.Performance.SDK.Extensibility.SourceParsing; @@ -117,5 +118,26 @@ namespace Microsoft.Performance.SDK.Processing string caption, string format, params object[] args); + + /// + /// Attempts to get an that can provide authentication + /// for of type . + /// + /// + /// The found provider, or null if no registered provider can provide authentication for + /// . + /// + /// + /// The type of the for which to attempt to get a provider. + /// + /// + /// The type of the result of a successful authentication for . + /// + /// + /// true if a provider was found; false otherwise. If false is returned, + /// will be null. + /// + bool TryGetAuthProvider(out IAuthProvider provider) + where TAuth : IAuthMethod; } } diff --git a/src/Microsoft.Performance.Testing.SDK/StubApplicationEnvironment.cs b/src/Microsoft.Performance.Testing.SDK/StubApplicationEnvironment.cs index 552e18b..f5d0879 100644 --- a/src/Microsoft.Performance.Testing.SDK/StubApplicationEnvironment.cs +++ b/src/Microsoft.Performance.Testing.SDK/StubApplicationEnvironment.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Microsoft.Performance.SDK.Auth; using Microsoft.Performance.SDK.Extensibility.DataCooking; using Microsoft.Performance.SDK.Extensibility.SourceParsing; using Microsoft.Performance.SDK.Processing; @@ -35,5 +36,12 @@ namespace Microsoft.Performance.Testing.SDK { return ButtonResult.None; } + + public bool TryGetAuthProvider(out IAuthProvider provider) + where TAuth : IAuthMethod + { + provider = null; + return false; + } } } diff --git a/src/Microsoft.Performance.Toolkit.Engine.Tests/TestCookers/Source123/Source123DataSource.cs b/src/Microsoft.Performance.Toolkit.Engine.Tests/TestCookers/Source123/Source123DataSource.cs index 21370d5..21fe862 100644 --- a/src/Microsoft.Performance.Toolkit.Engine.Tests/TestCookers/Source123/Source123DataSource.cs +++ b/src/Microsoft.Performance.Toolkit.Engine.Tests/TestCookers/Source123/Source123DataSource.cs @@ -43,6 +43,8 @@ namespace Microsoft.Performance.Toolkit.Engine.Tests.TestCookers.Source123 public bool IsDisposed => this.disposedValue; + internal IApplicationEnvironment ApplicationEnvironmentSpy => this.ApplicationEnvironment; + protected override ICustomDataProcessor CreateProcessorCore( IEnumerable dataSources, IProcessorEnvironment processorEnvironment, diff --git a/src/Microsoft.Performance.Toolkit.Engine.Tests/ToolkitEngineTests.cs b/src/Microsoft.Performance.Toolkit.Engine.Tests/ToolkitEngineTests.cs index f199deb..18f8faa 100644 --- a/src/Microsoft.Performance.Toolkit.Engine.Tests/ToolkitEngineTests.cs +++ b/src/Microsoft.Performance.Toolkit.Engine.Tests/ToolkitEngineTests.cs @@ -7,6 +7,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; +using System.Threading.Tasks; +using Microsoft.Performance.SDK.Auth; using Microsoft.Performance.SDK.Extensibility; using Microsoft.Performance.SDK.Processing; using Microsoft.Performance.SDK.Runtime; @@ -1131,6 +1133,55 @@ namespace Microsoft.Performance.Toolkit.Engine.Tests #endregion + #region Auth + + [TestMethod] + [UnitTest] + public void ProcessingSources_AreGiven_SpecifiedAuthProviders() + { + var givenProvider = new StubAuthProvider(); + + var info = new EngineCreateInfo(this.DefaultSet.AsReadOnly()); + info.WithAuthProvider(givenProvider); + using var sut = Engine.Create(info); + + var spy = sut.Plugins.ProcessingSourceReferences.First(psr => psr.Guid == Source123DataSource.Guid).Instance as Source123DataSource; + var success = spy.ApplicationEnvironmentSpy.TryGetAuthProvider(out var foundProvider); + + Assert.IsTrue(success); + Assert.AreEqual(givenProvider, foundProvider); + } + + [TestMethod] + [UnitTest] + public void TryGetAuthProvider_Fails_WhenNoProviderIsRegistered() + { + var info = new EngineCreateInfo(this.DefaultSet.AsReadOnly()); + using var sut = Engine.Create(info); + + var spy = sut.Plugins.ProcessingSourceReferences.First(psr => psr.Guid == Source123DataSource.Guid).Instance as Source123DataSource; + var success = spy.ApplicationEnvironmentSpy.TryGetAuthProvider(out var foundProvider); + + Assert.IsFalse(success); + Assert.IsNull(foundProvider); + } + + private class StubAuthMethod + : IAuthMethod + { + } + + private class StubAuthProvider + : IAuthProvider + { + public Task TryGetAuth(StubAuthMethod authRequest) + { + throw new NotImplementedException(); + } + } + + #endregion + public static DataOutputPath Parse(string dataCookerOutputPath) { var split = dataCookerOutputPath.Split('/'); diff --git a/src/Microsoft.Performance.Toolkit.Engine/Engine.cs b/src/Microsoft.Performance.Toolkit.Engine/Engine.cs index 2011390..544edcb 100644 --- a/src/Microsoft.Performance.Toolkit.Engine/Engine.cs +++ b/src/Microsoft.Performance.Toolkit.Engine/Engine.cs @@ -770,7 +770,7 @@ namespace Microsoft.Performance.Toolkit.Engine ? createInfo.ApplicationName : string.Empty; - instance.applicationEnvironment = new ApplicationEnvironment( + instance.applicationEnvironment = new EngineApplicationEnvironment( applicationName: applicationName, runtimeName: runtimeName, new RuntimeTableSynchronizer(), @@ -778,7 +778,8 @@ namespace Microsoft.Performance.Toolkit.Engine instance.Factory.CreateSourceSessionFactory(), createInfo.IsInteractive ? (IMessageBox)new InteractiveRuntimeMessageBox(instance.logger) - : (IMessageBox)new NonInteractiveMessageBox(instance.logger)) + : (IMessageBox)new NonInteractiveMessageBox(instance.logger), + createInfo.AuthProviders) { IsInteractive = createInfo.IsInteractive, }; diff --git a/src/Microsoft.Performance.Toolkit.Engine/EngineApplicationEnvironment.cs b/src/Microsoft.Performance.Toolkit.Engine/EngineApplicationEnvironment.cs new file mode 100644 index 0000000..a919c0f --- /dev/null +++ b/src/Microsoft.Performance.Toolkit.Engine/EngineApplicationEnvironment.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.ObjectModel; +using System.Diagnostics; +using Microsoft.Performance.SDK.Auth; +using Microsoft.Performance.SDK.Extensibility.DataCooking; +using Microsoft.Performance.SDK.Extensibility.SourceParsing; +using Microsoft.Performance.SDK.Processing; +using Microsoft.Performance.SDK.Runtime; + +namespace Microsoft.Performance.Toolkit.Engine +{ + internal sealed class EngineApplicationEnvironment + : ApplicationEnvironment + { + private readonly ReadOnlyDictionary authProviders; + + public EngineApplicationEnvironment( + string applicationName, + string runtimeName, + ITableDataSynchronization tableDataSynchronizer, + ISourceDataCookerFactoryRetrieval sourceDataCookerFactory, + ISourceSessionFactory sourceSessionFactory, + IMessageBox messageBox, + ReadOnlyDictionary authProviders) + : base( + applicationName, + runtimeName, + tableDataSynchronizer, + sourceDataCookerFactory, + sourceSessionFactory, + messageBox) + { + this.authProviders = authProviders; + } + + /// + public override bool TryGetAuthProvider(out IAuthProvider provider) + { + if (this.authProviders.TryGetValue(typeof(TAuth), out var authProvider)) + { + var authProviderTyped = authProvider as IAuthProvider; + + if (authProviderTyped == null) + { + Debug.Fail($"Expected registered {nameof(authProvider)} for type {typeof(TAuth)} to be of type {nameof(IAuthProvider)}"); + + provider = null; + return false; + } + + provider = authProviderTyped; + return true; + } + + return base.TryGetAuthProvider(out provider); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Performance.Toolkit.Engine/EngineCreateInfo.cs b/src/Microsoft.Performance.Toolkit.Engine/EngineCreateInfo.cs index 2162b2a..0735b73 100644 --- a/src/Microsoft.Performance.Toolkit.Engine/EngineCreateInfo.cs +++ b/src/Microsoft.Performance.Toolkit.Engine/EngineCreateInfo.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using Microsoft.Performance.SDK; +using Microsoft.Performance.SDK.Auth; using Microsoft.Performance.SDK.Processing; namespace Microsoft.Performance.Toolkit.Engine @@ -16,6 +18,8 @@ namespace Microsoft.Performance.Toolkit.Engine { private static string DefaultRuntimeName; + private readonly Dictionary authProviders = new Dictionary(); + /// /// Initializes the statc members of the /// class. @@ -97,6 +101,39 @@ namespace Microsoft.Performance.Toolkit.Engine return this; } + /// + /// Registers an of type to be available to plugins + /// via . + /// + /// + /// The to register. + /// + /// + /// The type of for which the is being registered. + /// + /// + /// The type of the result of successful authentication requests for . + /// + /// + /// The instance of . + /// + /// + /// An for the specified type already exists. + /// + public EngineCreateInfo WithAuthProvider(IAuthProvider provider) + where TAuth : IAuthMethod + { + Guard.NotNull(provider, nameof(provider)); + + var toRegister = typeof(TAuth); + if (!this.authProviders.TryAdd(toRegister, provider)) + { + throw new InvalidOperationException($"An auth provider for the specified type {toRegister} already exists."); + } + + return this; + } + /// /// Gets or sets the name of the runtime on which the application is built. /// @@ -136,5 +173,7 @@ namespace Microsoft.Performance.Toolkit.Engine /// will fail. By default, this property is false. /// public bool IsInteractive { get; set; } + + internal ReadOnlyDictionary AuthProviders => new ReadOnlyDictionary(this.authProviders); } }