diff --git a/src/Azure/Azure.Quantum.Client.Test/CredentialFactoryTests.cs b/src/Azure/Azure.Quantum.Client.Test/CredentialFactoryTests.cs new file mode 100644 index 00000000000..908224c54ef --- /dev/null +++ b/src/Azure/Azure.Quantum.Client.Test/CredentialFactoryTests.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +using Azure.Identity; + +using Microsoft.Azure.Quantum.Authentication; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.Azure.Quantum.Test +{ + [TestClass] + public class CredentialFactoryTests + { + private const string SUBSCRIPTION = "916dfd6d-030c-4bd9-b579-7bb6d1926e97"; + + [DataTestMethod] + [DataRow(CredentialType.Default, typeof(DefaultAzureCredential))] + [DataRow(CredentialType.Environment, typeof(EnvironmentCredential))] + [DataRow(CredentialType.ManagedIdentity, typeof(ManagedIdentityCredential))] + [DataRow(CredentialType.CLI, typeof(AzureCliCredential))] + [DataRow(CredentialType.SharedToken, typeof(SharedTokenCacheCredential))] + [DataRow(CredentialType.VisualStudio, typeof(VisualStudioCredential))] + [DataRow(CredentialType.VisualStudioCode, typeof(VisualStudioCodeCredential))] + [DataRow(CredentialType.Interactive, typeof(InteractiveBrowserCredential))] + [DataRow(CredentialType.DeviceCode, typeof(DeviceCodeCredential))] + public void TestCreateCredential(CredentialType credentialType, Type expectedType) + { + var actual = CredentialFactory.CreateCredential(credentialType); + Assert.IsNotNull(actual); + Assert.AreEqual(expectedType, actual.GetType()); + + // Now test with a specific subscription id: + actual = CredentialFactory.CreateCredential(credentialType, SUBSCRIPTION); + Assert.IsNotNull(actual); + Assert.AreEqual(expectedType, actual.GetType()); + } + + [TestMethod] + public void TestInvalidCredentialType() + { + Assert.ThrowsException(() => CredentialFactory.CreateCredential((CredentialType)9999)); + } + + [TestMethod] + public void TestGetTenantId() + { + var actual = CredentialFactory.GetTenantId(SUBSCRIPTION); + Assert.IsNotNull(actual); + Assert.IsFalse(string.IsNullOrWhiteSpace(actual)); + + var actual2 = CredentialFactory.GetTenantId(SUBSCRIPTION); + Assert.AreEqual(actual, actual2); + } + + [DataTestMethod] + [DataRow(null, null)] + [DataRow("", null)] + [DataRow("some random string", null)] + [DataRow("string,with,random,values", null)] + [DataRow("string=with,random=,key=values", null)] + [DataRow("string=with,random=,authorization_uri=", null)] + [DataRow("string=with,invalid=authorization_uri,authorization_uri=some-random-value", null)] + [DataRow("string=with,invalid=authorization_uri,authorization_uri=http://foo.bar.com/some-random-value", null)] + [DataRow("string=missing,tenant_id=authorization_uri,authorization_uri=\"http://foo.bar.com/", null)] + [DataRow("authorization_uri=\"https://login.microsoftonline.com/tenantId\",key1=value1s,etc...", "tenantId")] + public void TestExtractTenantIdFromBearer(string bearer, string expected) + { + var actual = CredentialFactory.ExtractTenantIdFromBearer(bearer); + Assert.AreEqual(expected, actual); + } + } +} diff --git a/src/Azure/Azure.Quantum.Client.Test/WorkspaceTest.cs b/src/Azure/Azure.Quantum.Client.Test/WorkspaceTest.cs index bb5fb81f7ba..e9c87e7ac03 100644 --- a/src/Azure/Azure.Quantum.Client.Test/WorkspaceTest.cs +++ b/src/Azure/Azure.Quantum.Client.Test/WorkspaceTest.cs @@ -29,8 +29,9 @@ public class WorkspaceTest * E2E_WORKSPACE_RG: the Azure Quantum workspace's resource group. * E2E_WORKSPACE_LOCATION: the Azure Quantum workspace's location (region). -You'll also need to authenticate with Azure using any of the methods listed in: +We'll also try to authenticate with Azure using an instance of DefaultCredential. See https://docs.microsoft.com/en-us/dotnet/api/overview/azure/identity-readme#authenticate-the-client +for details. Tests will be marked as Inconclusive if the pre-reqs are not correctly setup."; @@ -197,12 +198,15 @@ private IWorkspace GetLiveWorkspace() var options = new QuantumJobClientOptions(); options.Diagnostics.ApplicationId = "ClientTests"; + var credential = Authentication.CredentialFactory.CreateCredential(Authentication.CredentialType.Default); + return new Workspace( subscriptionId: System.Environment.GetEnvironmentVariable("E2E_SUBSCRIPTION_ID"), resourceGroupName: System.Environment.GetEnvironmentVariable("E2E_WORKSPACE_RG"), workspaceName: System.Environment.GetEnvironmentVariable("E2E_WORKSPACE_NAME"), location: System.Environment.GetEnvironmentVariable("E2E_WORKSPACE_LOCATION"), - options: options); + options: options, + credential: credential); } private static JobDetails CreateJobDetails(string jobId, string containerUri = null, string inputUri = null) diff --git a/src/Azure/Azure.Quantum.Client/Authentication/CredentialFactory.cs b/src/Azure/Azure.Quantum.Client/Authentication/CredentialFactory.cs new file mode 100644 index 00000000000..ce0f3347e04 --- /dev/null +++ b/src/Azure/Azure.Quantum.Client/Authentication/CredentialFactory.cs @@ -0,0 +1,279 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +namespace Microsoft.Azure.Quantum.Authentication +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net.Http; + + using global::Azure.Core; + using global::Azure.Identity; + + /// + /// The enumeration of supported Credential Classes supported out of the box for + /// authentication in Azure Quantum. + /// NOTE: For more information + /// about authentication with Azure services and the different Credential types see + /// https://docs.microsoft.com/en-us/dotnet/api/overview/azure/identity-readme. + /// + public enum CredentialType + { + /// + /// Provides a simplified authentication experience to quickly start developing applications run in the Azure cloud. + /// See: https://docs.microsoft.com/en-us/dotnet/api/azure.identity.defaultazurecredential + /// + Default, + + /// + /// Authenticates a service principal or user via credential information specified in environment variables. + /// See: https://docs.microsoft.com/en-us/dotnet/api/azure.identity.environmentcredential + /// + Environment, + + /// + /// Authenticates the managed identity of an azure resource. + /// See: https://docs.microsoft.com/en-us/dotnet/api/azure.identity.managedidentitycredential + /// + ManagedIdentity, + + /// + /// Authenticate in a development environment with the Azure CLI. + /// See https://docs.microsoft.com/en-us/dotnet/api/azure.identity.azureclicredential + /// + CLI, + + /// + /// Authenticate using tokens in the local cache shared between Microsoft applications. + /// See: https://docs.microsoft.com/en-us/dotnet/api/azure.identity.sharedtokencachecredential + /// + SharedToken, + + /// + /// Authenticate using data from Visual Studio. + /// See: https://docs.microsoft.com/en-us/dotnet/api/azure.identity.visualstudiocredential + /// + VisualStudio, + + /// + /// Authenticate in a development environment with Visual Studio Code. + /// See: https://docs.microsoft.com/en-us/dotnet/api/azure.identity.visualstudiocodecredential + /// + VisualStudioCode, + + /// + /// A TokenCredential implementation which launches the system default browser to interactively authenticate a user, + /// and obtain an access token. The browser will only be launched to authenticate the user once, + /// then will silently acquire access tokens through the users refresh token as long as it's valid. + /// See: https://docs.microsoft.com/en-us/dotnet/api/azure.identity.interactivebrowsercredential + /// + Interactive, + + /// + /// A TokenCredential implementation which authenticates a user using the device code flow, + /// and provides access tokens for that user account. + /// See: https://docs.microsoft.com/en-us/dotnet/api/azure.identity.devicecodecredential + /// + DeviceCode, + } + + public static class CredentialFactory + { + // Used to fetch the tenantId automatically from ARM + private static readonly HttpClient Client = new HttpClient(); + + // Used to catch all the TenantIds: + private static readonly Dictionary TenantIds = new Dictionary(); + + public static TokenCredential CreateCredential(CredentialType credentialType, string? subscriptionId = null) => credentialType switch + { + CredentialType.SharedToken => CreateCredential(credentialType, () => SharedTokenOptions(subscriptionId)), + CredentialType.VisualStudio => CreateCredential(credentialType, () => VisualStudioOptions(subscriptionId)), + CredentialType.VisualStudioCode => CreateCredential(credentialType, () => VisualStudioCodeOptions(subscriptionId)), + CredentialType.Interactive => CreateCredential(credentialType, () => InteractiveOptions(subscriptionId)), + CredentialType.DeviceCode => CreateCredential(credentialType, () => DeviceCodeOptions(subscriptionId)), + CredentialType.Default => CreateCredential(credentialType, () => DefaultOptions(subscriptionId)), + _ => CreateCredential(credentialType, () => DefaultOptions(subscriptionId)), + }; + + /// + /// Creates an instance of TokenCredential that corresponds to the given . + /// It creates an instance of the Credential Class with default parameters. + /// + /// The type of Credential Class to create. + /// A configuration method for the corresponding credential options (not used for Environment, ManagedIdentity or CLI credentials). + /// An instance of TokenCredential for the corresponding value. + public static TokenCredential CreateCredential(CredentialType credentialType, Func options) => credentialType switch + { + CredentialType.Environment => new EnvironmentCredential(), + CredentialType.ManagedIdentity => new ManagedIdentityCredential(), + CredentialType.CLI => new AzureCliCredential(), + CredentialType.DeviceCode => new DeviceCodeCredential(options: options?.Invoke() as DeviceCodeCredentialOptions), + CredentialType.SharedToken => new SharedTokenCacheCredential(options: options?.Invoke() as SharedTokenCacheCredentialOptions), + CredentialType.VisualStudio => new VisualStudioCredential(options: options?.Invoke() as VisualStudioCredentialOptions), + CredentialType.VisualStudioCode => new VisualStudioCodeCredential(options: options?.Invoke() as VisualStudioCodeCredentialOptions), + CredentialType.Interactive => new InteractiveBrowserCredential(options: options?.Invoke() as InteractiveBrowserCredentialOptions), + CredentialType.Default => new DefaultAzureCredential(options: options?.Invoke() as DefaultAzureCredentialOptions), + _ => throw new ArgumentException($"Credentials of type {credentialType} are not supported.") + }; + + /// + /// Returns an DefaultAzureCredentialOptions, populated with the TenantId for the given subscription. + /// We als disabilitate VisualStudio credentials, since they don't currently work with Azure Quantum. + /// + /// An subscription Id. + /// A new instance of InteractiveBrowserCredentialOptions with the TenantId populated + public static DefaultAzureCredentialOptions DefaultOptions(string? subscriptionid) + { + string? tenantId = GetTenantId(subscriptionid); + + return new DefaultAzureCredentialOptions + { + // Disable VS credentials until https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1332071 is fixed: + ExcludeVisualStudioCredential = true, + ExcludeInteractiveBrowserCredential = false, + + InteractiveBrowserTenantId = tenantId, + SharedTokenCacheTenantId = tenantId, + VisualStudioCodeTenantId = tenantId, + VisualStudioTenantId = tenantId, + }; + } + + /// + /// Returns an InteractiveBrowserCredentialOptions, populated with the TenantId for the given subscription. + /// + /// An subscription Id. + /// A new instance of InteractiveBrowserCredentialOptions with the TenantId populated + public static InteractiveBrowserCredentialOptions InteractiveOptions(string? subscriptionid) => + new InteractiveBrowserCredentialOptions + { + TenantId = GetTenantId(subscriptionid), + }; + + /// + /// Returns an VisualStudioCodeCredentialOptions, populated with the TenantId for the given subscription. + /// + /// An subscription Id. + /// A new instance of InteractiveBrowserCredentialOptions with the TenantId populated + public static VisualStudioCodeCredentialOptions VisualStudioCodeOptions(string? subscriptionid) => + new VisualStudioCodeCredentialOptions + { + TenantId = GetTenantId(subscriptionid), + }; + + /// + /// Returns an VisualStudioCredentialOptions, populated with the TenantId for the given subscription. + /// + /// An subscription Id. + /// A new instance of InteractiveBrowserCredentialOptions with the TenantId populated + public static VisualStudioCredentialOptions VisualStudioOptions(string? subscriptionid) => + new VisualStudioCredentialOptions + { + TenantId = GetTenantId(subscriptionid), + }; + + /// + /// Returns an SharedTokenCacheCredentialOptions, populated with the TenantId for the given subscription. + /// + /// An subscription Id. + /// A new instance of InteractiveBrowserCredentialOptions with the TenantId populated + public static SharedTokenCacheCredentialOptions SharedTokenOptions(string? subscriptionid) => + new SharedTokenCacheCredentialOptions + { + TenantId = GetTenantId(subscriptionid), + }; + + /// + /// Returns an VisualStudioCodeCredentialOptions, populated with the TenantId for the given subscription. + /// + /// An subscription Id. + /// A new instance of InteractiveBrowserCredentialOptions with the TenantId populated + public static DeviceCodeCredentialOptions DeviceCodeOptions(string? subscriptionid) => + new DeviceCodeCredentialOptions + { + TenantId = GetTenantId(subscriptionid), + }; + + /// + /// This gnarly piece of code is how we get the guest tenant + /// authority associated with the subscription. + /// We make a unauthenticated request to ARM and extract the tenant + /// authority from the WWW-Authenticate header in the response. + /// + /// The subscriptionId. + /// The tenantId for the given subscription; null if it can be found or for a null subscription. + public static string? GetTenantId(string? subscriptionId) + { + if (subscriptionId == null) + { + return null; + } + + if (TenantIds.TryGetValue(subscriptionId, out string? tenantId)) + { + return tenantId; + } + + try + { + string url = $"https://management.azure.com/subscriptions/{subscriptionId}?api-version=2020-01-01"; + HttpResponseMessage response = Client.GetAsync(url).Result; + var header = response + .Headers + .WwwAuthenticate + .FirstOrDefault(v => v.Scheme == "Bearer") + ?.Parameter; + + tenantId = ExtractTenantIdFromBearer(header); + TenantIds[subscriptionId] = tenantId; + + return tenantId; + } + catch + { + return null; + } + } + + /// + /// Here we parse WWW-Authenticate header in the response to match the tenant id. + /// The header is of the form: + /// Bearer authorization_uri="https://login.microsoftonline.com/tenantId",key1=value1s,etc... + /// + /// The value of the Bearer in the WWWAuthenticate header + /// The tenant-id, or null if it can't find it. + public static string? ExtractTenantIdFromBearer(string? bearer) + { + if (bearer == null) + { + return null; + } + + // Split the key=value comma seperated list and look for the "authorization_uri" key: + var auth_uri = bearer + .Split(',') + .Select(kv => kv.Split('=', 2)) + .FirstOrDefault(pair => pair[0] == "authorization_uri")?[1]; + + // If found an authorization_uri, find the tenant id from a URL surrounded by quotes, i.e.: + // "https://login.microsoftonline.com/tenantId" + if (auth_uri != null && auth_uri.StartsWith('"') && auth_uri.EndsWith('"')) + { + var id = auth_uri + [1 .. ^1] + [auth_uri.LastIndexOf('/') .. ]; + + return id; + } + else + { + return null; + } + + } + } +} diff --git a/src/Azure/Azure.Quantum.Client/Exceptions/WorkspaceClientException.cs b/src/Azure/Azure.Quantum.Client/Exceptions/WorkspaceClientException.cs index 9c70b86fdcd..8f5c2327b89 100644 --- a/src/Azure/Azure.Quantum.Client/Exceptions/WorkspaceClientException.cs +++ b/src/Azure/Azure.Quantum.Client/Exceptions/WorkspaceClientException.cs @@ -12,10 +12,6 @@ public class WorkspaceClientException : AzureQuantumException { private const string BaseMessage = "An exception related to the Azure workspace client occurred"; - public string ErrorCode { get; } - - public int Status { get; } - /// /// Initializes a new instance of the class with a default error message. /// @@ -82,6 +78,10 @@ public WorkspaceClientException( } } + public string ErrorCode { get; } + + public int Status { get; } + /// /// Formats the contents of the inner exception in so it can be included in the /// exception message and presented in an informative way. diff --git a/src/Azure/Azure.Quantum.Client/JobManagement/CloudJob.cs b/src/Azure/Azure.Quantum.Client/JobManagement/CloudJob.cs index 05c92de3e0b..aa7c5eb30fd 100644 --- a/src/Azure/Azure.Quantum.Client/JobManagement/CloudJob.cs +++ b/src/Azure/Azure.Quantum.Client/JobManagement/CloudJob.cs @@ -142,12 +142,7 @@ public async Task WaitForCompletion(int pollIntervalMilliseconds = 500, Cancella private Uri GenerateUri() { - if (!(this.Workspace is Workspace cloudWorkspace)) - { - throw new NotSupportedException($"{typeof(CloudJob)}'s Workspace is not of type {typeof(Workspace)} and does not have enough data to generate URI"); - } - - var uriStr = $"https://ms.portal.azure.com/#@microsoft.onmicrosoft.com/resource/subscriptions/{cloudWorkspace.SubscriptionId}/resourceGroups/{cloudWorkspace.ResourceGroupName}/providers/Microsoft.Quantum/Workspaces/{cloudWorkspace.WorkspaceName}/job_management?microsoft_azure_quantum_jobid={Id}"; + var uriStr = $"https://portal.azure.com/#@microsoft.onmicrosoft.com/resource/subscriptions/{Workspace.SubscriptionId}/resourceGroups/{Workspace.ResourceGroupName}/providers/Microsoft.Quantum/Workspaces/{Workspace.WorkspaceName}/job_management?microsoft_azure_quantum_jobid={Id}"; return new Uri(uriStr); } } diff --git a/src/Azure/Azure.Quantum.Client/JobManagement/Workspace.cs b/src/Azure/Azure.Quantum.Client/JobManagement/Workspace.cs index b8eb8349377..e6c9a7e1f97 100644 --- a/src/Azure/Azure.Quantum.Client/JobManagement/Workspace.cs +++ b/src/Azure/Azure.Quantum.Client/JobManagement/Workspace.cs @@ -15,6 +15,7 @@ namespace Microsoft.Azure.Quantum using global::Azure.Quantum.Jobs; using global::Azure.Quantum.Jobs.Models; + using Microsoft.Azure.Quantum.Authentication; using Microsoft.Azure.Quantum.Exceptions; using Microsoft.Azure.Quantum.Utility; @@ -48,17 +49,7 @@ public Workspace( Ensure.NotNullOrWhiteSpace(location, nameof(location)); // Optional parameters: - if (credential == null) - { - // We have to disable VisualStudio until 16.11 goes out, see: https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1332071 - var credOptions = new DefaultAzureCredentialOptions() - { - ExcludeVisualStudioCredential = true, - }; - - credential = new DefaultAzureCredential(credOptions); - } - + credential ??= CredentialFactory.CreateCredential(CredentialType.Default, subscriptionId); options ??= new QuantumJobClientOptions(); this.ResourceGroupName = resourceGroupName; diff --git a/src/Simulation/EntryPointDriver.Tests/Tests.Microsoft.Quantum.EntryPointDriver.sln b/src/Simulation/EntryPointDriver.Tests/Tests.Microsoft.Quantum.EntryPointDriver.sln new file mode 100644 index 00000000000..00e2b89cd81 Binary files /dev/null and b/src/Simulation/EntryPointDriver.Tests/Tests.Microsoft.Quantum.EntryPointDriver.sln differ diff --git a/src/Simulation/EntryPointDriver.Tests/Tests.fs b/src/Simulation/EntryPointDriver.Tests/Tests.fs index b5b9837b9d6..9d75e67c648 100644 --- a/src/Simulation/EntryPointDriver.Tests/Tests.fs +++ b/src/Simulation/EntryPointDriver.Tests/Tests.fs @@ -568,6 +568,7 @@ let ``Submit uses default values`` () = Storage: Base URI: Location: myLocation + Credential: Default Job Name: Shots: 500 Output: FriendlyUri @@ -587,6 +588,7 @@ let ``Submit uses default values with default target`` () = Storage: Base URI: Location: myLocation + Credential: Default Job Name: Shots: 500 Output: FriendlyUri @@ -608,6 +610,8 @@ let ``Submit allows overriding default values`` () = "myJobName" "--shots" "750" + "--credential" + "cli" ]) |> yields "Subscription: mySubscription Resource Group: myResourceGroup @@ -616,6 +620,7 @@ let ``Submit allows overriding default values`` () = Storage: myStorage Base URI: Location: myLocation + Credential: CLI Job Name: myJobName Shots: 750 Output: FriendlyUri @@ -637,6 +642,8 @@ let ``Submit extracts the location from a quantum endpoint`` () = "https://westus.quantum.microsoft.com/" "--job-name" "myJobName" + "--credential" + "VisualStudio" "--shots" "750" "--target" @@ -649,6 +656,7 @@ let ``Submit extracts the location from a quantum endpoint`` () = Storage: myStorage Base URI: https://westus.quantum.microsoft.com/ Location: westus + Credential: VisualStudio Job Name: myJobName Shots: 750 Output: FriendlyUri @@ -664,6 +672,8 @@ let ``Submit allows overriding default values with default target`` () = "--verbose" "--storage" "myStorage" + "--credential" + "Interactive" "--aad-token" "myToken" "--job-name" @@ -678,6 +688,7 @@ let ``Submit allows overriding default values with default target`` () = Storage: myStorage Base URI: Location: myLocation + Credential: Interactive Job Name: myJobName Shots: 750 Output: FriendlyUri @@ -712,6 +723,7 @@ let ``Submit allows to include --base-uri option when --location is not present` Storage: Base URI: http://mybaseuri.foo.com/ Location: mybaseuri + Credential: Default Job Name: Shots: 500 Output: FriendlyUri @@ -733,6 +745,7 @@ let ``Submit allows to include --location option when --base-uri is not present` Storage: Base URI: Location: myLocation + Credential: Default Job Name: Shots: 500 Output: FriendlyUri @@ -758,6 +771,7 @@ let ``Submit allows spaces for the --location option`` () = Storage: Base URI: Location: My Location + Credential: Default Job Name: Shots: 500 Output: FriendlyUri @@ -870,6 +884,7 @@ let ``Shows help text for submit command`` () = --resource-group (REQUIRED) The resource group name. --workspace (REQUIRED) The workspace name. --target (REQUIRED) The target device ID. + --credential The type of credential to use to authenticate with Azure. --storage The storage account connection string. --aad-token The Azure Active Directory authentication token. --base-uri The base URI of the Azure Quantum endpoint. @@ -899,6 +914,7 @@ let ``Shows help text for submit command with default target`` () = --resource-group (REQUIRED) The resource group name. --workspace (REQUIRED) The workspace name. --target The target device ID. + --credential The type of credential to use to authenticate with Azure. --storage The storage account connection string. --aad-token The Azure Active Directory authentication token. --base-uri The base URI of the Azure Quantum endpoint. diff --git a/src/Simulation/EntryPointDriver/Azure.cs b/src/Simulation/EntryPointDriver/Azure.cs index ab0813e0cc8..4cbd1b28c1d 100644 --- a/src/Simulation/EntryPointDriver/Azure.cs +++ b/src/Simulation/EntryPointDriver/Azure.cs @@ -3,11 +3,14 @@ using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Azure.Core; +using Azure.Identity; using Microsoft.Azure.Quantum; +using Microsoft.Azure.Quantum.Authentication; using Microsoft.Azure.Quantum.Exceptions; using Microsoft.Quantum.Runtime; using Microsoft.Quantum.Simulation.Common.Exceptions; @@ -212,6 +215,22 @@ public enum OutputFormat /// public sealed class AzureSettings { + private class AADTokenCredential : TokenCredential + { + AccessToken Token { get; } + + public AADTokenCredential(string token) + { + Token = new AccessToken(token, DateTime.Now.AddMinutes(5)); + } + + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) => + Token; + + public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) => + new ValueTask(Token); + } + /// /// The subscription ID. /// @@ -242,6 +261,14 @@ public sealed class AzureSettings /// public string? AadToken { get; set; } + /// + /// The type of Credentials to use to authenticate with Azure. For more information + /// about authentication with Azure services see: https://docs.microsoft.com/en-us/dotnet/api/overview/azure/identity-readme + /// NOTE: If both and properties are specified, takes precedence. + /// If none are provided, then it uses . + /// + public CredentialType? Credential { get; set; } + /// /// The base URI of the Azure Quantum endpoint. /// NOTE: This parameter is deprected, please always use . @@ -280,31 +307,15 @@ public sealed class AzureSettings /// public bool Verbose { get; set; } - /// - /// Print a warning about passing an AAD token. Using this is not supported anymore but we keep - /// the parameter to break any existing clients, like the az cli. - /// Once the known clients are updated we should remove the parameter too. - /// - internal void PrintAadWarning() + internal TokenCredential CreateCredentials() { - Console.ForegroundColor = ConsoleColor.Yellow; - if (!(AadToken is null)) { - try - { - Console.Error.WriteLine("----------------------------------------------------------------------------"); - Console.Error.WriteLine(" [Warning]"); - Console.Error.WriteLine(" The AadToken parameter is not supported anymore."); - Console.Error.WriteLine(" Take a look at the Azure Identity client library at"); - Console.Error.WriteLine(" https://docs.microsoft.com/en-us/dotnet/api/overview/azure/identity-readme"); - Console.Error.WriteLine(" for new authentication options."); - Console.Error.WriteLine("----------------------------------------------------------------------------"); - } - finally - { - Console.ResetColor(); - } + return new AADTokenCredential(AadToken); + } + else + { + return CredentialFactory.CreateCredential(Credential ?? CredentialType.Default, Subscription); } } @@ -314,15 +325,15 @@ internal void PrintAadWarning() /// The based on the settings. internal Workspace CreateWorkspace() { - PrintAadWarning(); - + var credentials = CreateCredentials(); var location = NormalizeLocation(Location ?? ExtractLocation(BaseUri)); return new Workspace( subscriptionId: Subscription, resourceGroupName: ResourceGroup, workspaceName: Workspace, - location: location); + location: location, + credential: credentials); } public override string ToString() => @@ -334,6 +345,7 @@ public override string ToString() => $"Storage: {Storage}", $"Base URI: {BaseUri}", $"Location: {Location ?? ExtractLocation(BaseUri)}", + $"Credential: {Credential}", $"Job Name: {JobName}", $"Shots: {Shots}", $"Output: {Output}", diff --git a/src/Simulation/EntryPointDriver/Driver.cs b/src/Simulation/EntryPointDriver/Driver.cs index 796cc4556a0..1a3943c0f00 100644 --- a/src/Simulation/EntryPointDriver/Driver.cs +++ b/src/Simulation/EntryPointDriver/Driver.cs @@ -14,6 +14,8 @@ using System.Text; using System.Threading.Tasks; +using Microsoft.Azure.Quantum.Authentication; + namespace Microsoft.Quantum.EntryPointDriver { using Validators = ImmutableList>; @@ -47,6 +49,14 @@ public sealed class Driver private static readonly OptionInfo StorageOption = new OptionInfo( ImmutableList.Create("--storage"), default, "The storage account connection string."); + /// + /// The credential option. + /// + private static readonly OptionInfo CredentialOption = new OptionInfo( + ImmutableList.Create("--credential"), + CredentialType.Default, + "The type of credential to use to authenticate with Azure."); + /// /// The AAD token option. /// @@ -417,7 +427,8 @@ private CommandWithValidators CreateSubmitEntryPointCommand(IEntryPoint entryPoi var validators = AddOptionIfAvailable(command, SubscriptionOption) .Concat(AddOptionIfAvailable(command, ResourceGroupOption)) .Concat(AddOptionIfAvailable(command, WorkspaceOption)) - .Concat(AddOptionIfAvailable(command, this.TargetOption)) + .Concat(AddOptionIfAvailable(command, TargetOption)) + .Concat(AddOptionIfAvailable(command, CredentialOption)) .Concat(AddOptionIfAvailable(command, StorageOption)) .Concat(AddOptionIfAvailable(command, AadTokenOption)) .Concat(AddOptionIfAvailable(command, BaseUriOption)) @@ -457,10 +468,11 @@ private Task Submit(ParseResult parseResult, AzureSettings azureSettings, I Subscription = azureSettings.Subscription, ResourceGroup = azureSettings.ResourceGroup, Workspace = azureSettings.Workspace, - Target = DefaultIfShadowed(entryPoint, this.TargetOption, azureSettings.Target), + Target = DefaultIfShadowed(entryPoint, TargetOption, azureSettings.Target), Storage = DefaultIfShadowed(entryPoint, StorageOption, azureSettings.Storage), BaseUri = DefaultIfShadowed(entryPoint, BaseUriOption, azureSettings.BaseUri), Location = DefaultIfShadowed(entryPoint, LocationOption, azureSettings.Location), + Credential = DefaultIfShadowed(entryPoint, CredentialOption, azureSettings.Credential), JobName = DefaultIfShadowed(entryPoint, JobNameOption, azureSettings.JobName), Shots = DefaultIfShadowed(entryPoint, ShotsOption, azureSettings.Shots), Output = DefaultIfShadowed(entryPoint, OutputOption, azureSettings.Output),