From 2b50023b7880c20a121f86c7fda9b2e2e78845cc Mon Sep 17 00:00:00 2001 From: Beisi Zhou Date: Sat, 23 Mar 2024 12:09:27 +0800 Subject: [PATCH 1/3] Support Subscription Selection (#24451) * Readline to get selected subscription * wip * move prompt login to list subscription instead of populating contexts * fix some bugs * add announcements and feedback links * fix comments * skip test cases for WriteInformation * list for single tenant --- .../Accounts.Test/AzureRMProfileTests.cs | 2 +- .../Accounts.Test/LoginCmdletTests.cs | 2 +- .../Accounts/Account/ConnectAzureRmAccount.cs | 68 +++-- .../Accounts/Accounts.generated.format.ps1xml | 24 +- .../Accounts/Models/RMProfileClient.cs | 243 ++++++++++++++++-- .../Accounts/Properties/Resources.Designer.cs | 9 + .../Accounts/Properties/Resources.resx | 3 + src/Accounts/Accounts/Utilities/Color.cs | 22 ++ .../Models/PSAzureProfile.cs | 6 +- 9 files changed, 307 insertions(+), 72 deletions(-) create mode 100644 src/Accounts/Accounts/Utilities/Color.cs diff --git a/src/Accounts/Accounts.Test/AzureRMProfileTests.cs b/src/Accounts/Accounts.Test/AzureRMProfileTests.cs index 5118381699f7..4c8e543fa52b 100644 --- a/src/Accounts/Accounts.Test/AzureRMProfileTests.cs +++ b/src/Accounts/Accounts.Test/AzureRMProfileTests.cs @@ -1299,7 +1299,7 @@ public void LoadingProfileWorks() [Fact] - [Trait(Category.AcceptanceType, Category.CheckIn)] + [Trait(Category.AcceptanceType, Category.LiveOnly)] public void CanRenewTokenLogin() { var tenants = new List { DefaultTenant.ToString() }; diff --git a/src/Accounts/Accounts.Test/LoginCmdletTests.cs b/src/Accounts/Accounts.Test/LoginCmdletTests.cs index 41bdf1baeaf8..5ccdc591914c 100644 --- a/src/Accounts/Accounts.Test/LoginCmdletTests.cs +++ b/src/Accounts/Accounts.Test/LoginCmdletTests.cs @@ -516,7 +516,7 @@ public void ThrowOnUnknownEnvironment() } [Fact] - [Trait(Category.AcceptanceType, Category.CheckIn)] + [Trait(Category.AcceptanceType, Category.LiveOnly)] public void LoginUsingSkipValidation() { var cmdlt = new ConnectAzureRmAccountCommand(); diff --git a/src/Accounts/Accounts/Account/ConnectAzureRmAccount.cs b/src/Accounts/Accounts/Account/ConnectAzureRmAccount.cs index c24d5b4f90cf..99251e39e0d1 100644 --- a/src/Accounts/Accounts/Account/ConnectAzureRmAccount.cs +++ b/src/Accounts/Accounts/Account/ConnectAzureRmAccount.cs @@ -12,16 +12,6 @@ // limitations under the License. // ---------------------------------------------------------------------------------- -using System; -using System.Collections.Concurrent; -using System.Linq; -using System.Management.Automation; -using System.Runtime.InteropServices; -using System.Security; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - using Azure.Identity; using Microsoft.Azure.Commands.Common.Authentication; @@ -30,24 +20,33 @@ using Microsoft.Azure.Commands.Common.Authentication.Factories; using Microsoft.Azure.Commands.Common.Authentication.Models; using Microsoft.Azure.Commands.Common.Authentication.ResourceManager.Common; +using Microsoft.Azure.Commands.Common.Authentication.Sanitizer; using Microsoft.Azure.Commands.Profile.Common; using Microsoft.Azure.Commands.Profile.Models.Core; using Microsoft.Azure.Commands.Profile.Properties; +using Microsoft.Azure.Commands.Profile.Utilities; using Microsoft.Azure.Commands.ResourceManager.Common; using Microsoft.Azure.Commands.ResourceManager.Common.ArgumentCompleters; using Microsoft.Azure.Commands.Shared.Config; using Microsoft.Azure.PowerShell.Authenticators; using Microsoft.Azure.PowerShell.Authenticators.Factories; using Microsoft.Azure.PowerShell.Common.Config; +using Microsoft.Azure.PowerShell.Common.Share.Survey; using Microsoft.Identity.Client; using Microsoft.WindowsAzure.Commands.Common; +using Microsoft.WindowsAzure.Commands.Common.Sanitizer; using Microsoft.WindowsAzure.Commands.Common.Utilities; using Microsoft.WindowsAzure.Commands.Utilities.Common; -using Microsoft.Azure.PowerShell.Common.Share.Survey; -using Microsoft.Azure.Commands.Profile.Utilities; -using System.Management.Automation.Runspaces; -using Microsoft.WindowsAzure.Commands.Common.Sanitizer; -using Microsoft.Azure.Commands.Common.Authentication.Sanitizer; + +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Management.Automation; +using System.Runtime.InteropServices; +using System.Security; +using System.Text; +using System.Threading; +using System.Threading.Tasks; namespace Microsoft.Azure.Commands.Profile { @@ -328,6 +327,7 @@ public override void ExecuteCmdlet() Guid subscriptionIdGuid; string subscriptionName = null; string subscriptionId = null; + bool isInteractiveAuthentication = IsInteractiveAuthentication(); if (MyInvocation.BoundParameters.ContainsKey(nameof(Subscription))) { if (Guid.TryParse(Subscription, out subscriptionIdGuid)) @@ -506,7 +506,7 @@ public override void ExecuteCmdlet() commonUtilities = new CommonUtilities(); AzureSession.Instance.RegisterComponent(nameof(CommonUtilities), () => commonUtilities); } - if (!commonUtilities.IsDesktopSession() && IsUsingInteractiveAuthentication()) + if (!commonUtilities.IsDesktopSession() && IsBrowserPopUpInteractiveAuthentication()) { WriteWarning(Resources.InteractiveAuthNotSupported); return; @@ -523,8 +523,10 @@ public override void ExecuteCmdlet() shouldPopulateContextList = false; } - profileClient.WarningLog = (message) => _tasks.Enqueue(new Task(() => this.WriteWarning(message))); + profileClient.WarningLog = (message) => _tasks.Enqueue(new Task(() => this.WriteWarning(message))); + profileClient.InformationLog = (message) => _tasks.Enqueue(new Task(() => this.WriteInformation(message, false))); profileClient.DebugLog = (message) => _tasks.Enqueue(new Task(() => this.WriteDebugWithTimestamp(message))); + var task = new Task(() => profileClient.Login( azureAccount, _environment, @@ -538,7 +540,9 @@ public override void ExecuteCmdlet() name, shouldPopulateContextList, MaxContextPopulation, - resourceId)); + resourceId, + Prompt, + isInteractiveAuthentication)); task.Start(); while (!task.IsCompleted) { @@ -569,7 +573,7 @@ public override void ExecuteCmdlet() } else { - if (IsUsingInteractiveAuthentication()) + if (IsBrowserPopUpInteractiveAuthentication()) { //Display only if user is using Interactive auth WriteWarning(Resources.SuggestToUseDeviceCodeAuth); @@ -579,9 +583,17 @@ public override void ExecuteCmdlet() } } }); + + WriteInformation($"[Announcements]{System.Environment.NewLine}Share your feedback regarding your experience with `Connect-AzAccount` at: https://aka.ms/azloginfeedback{System.Environment.NewLine}"); + WriteInformation($"If you encounter any problem, please open an issue at: https://aka.ms/azpsissue{System.Environment.NewLine}"); } } + private bool IsInteractiveAuthentication() + { + return ParameterSetName.Equals(UserParameterSet); + } + private void ValidateActionRequiredMessageCanBePresented() { if (UseDeviceAuthentication.IsPresent && IsWriteInformationIgnored()) @@ -608,9 +620,9 @@ private string PreProcessAuthScope() return mappedScope; } - private bool IsUsingInteractiveAuthentication() + private bool IsBrowserPopUpInteractiveAuthentication() { - return ParameterSetName == UserParameterSet && UseDeviceAuthentication == false; + return ParameterSetName.Equals(UserParameterSet) && UseDeviceAuthentication.IsPresent == false; } private bool IsUnableToOpenWebPageError(AuthenticationFailedException exception) @@ -654,6 +666,20 @@ private void WriteWarningEvent(string message) } } + private void WriteInformationEvent(string message) + { + EventHandler writeInformationEvent; + if (AzureSession.Instance.TryGetComponent(WriteInformationKey, out writeInformationEvent)) + { + writeInformationEvent(this, new StreamEventArgs() { Message = message }); + } + } + private string Prompt(string message) + { + _tasks.Enqueue(new Task(() => this.WriteInformation(message, true))); + return this.Host.UI.ReadLine(); + } + private static bool CheckForExistingContext(AzureRmProfile profile, string name) { return name != null && profile?.Contexts != null && profile.Contexts.ContainsKey(name); diff --git a/src/Accounts/Accounts/Accounts.generated.format.ps1xml b/src/Accounts/Accounts/Accounts.generated.format.ps1xml index 33003797f968..26f920a592b3 100644 --- a/src/Accounts/Accounts/Accounts.generated.format.ps1xml +++ b/src/Accounts/Accounts/Accounts.generated.format.ps1xml @@ -10,19 +10,11 @@ Left - - - - Left - + Left - - - - Left - + @@ -30,19 +22,11 @@ Left - $_.Context.Account.ToString() - - - Left - $_.Context.Subscription.Name - - - Left - $_.Context.Tenant.ToString() + if($null -ne $_.Context.Subscription.Name){$_.Context.Subscription.Name}else{$_.Context.Subscription.Id} Left - $_.Context.Environment.ToString() + if($null -ne $_.Context.Tenant.DefaultDomain){$_.Context.Tenant.DefaultDomain}else{$_.Context.Tenant.Id} diff --git a/src/Accounts/Accounts/Models/RMProfileClient.cs b/src/Accounts/Accounts/Models/RMProfileClient.cs index 4e4b1a302d97..e0c028a7987f 100644 --- a/src/Accounts/Accounts/Models/RMProfileClient.cs +++ b/src/Accounts/Accounts/Models/RMProfileClient.cs @@ -11,20 +11,27 @@ // See the License for the specific language governing permissions and // limitations under the License. // ---------------------------------------------------------------------------------- +using Microsoft.Azure.Commands.Common; using Microsoft.Azure.Commands.Common.Authentication; using Microsoft.Azure.Commands.Common.Authentication.Abstractions; using Microsoft.Azure.Commands.Common.Authentication.Models; using Microsoft.Azure.Commands.Common.Authentication.ResourceManager; +using Microsoft.Azure.Commands.Common.Exceptions; using Microsoft.Azure.Commands.Profile.Models; using Microsoft.Azure.Commands.Profile.Properties; using Microsoft.Azure.Commands.Profile.Utilities; +using Microsoft.Azure.Management.Profiles.Storage.Version2019_06_01.Models; using Microsoft.Rest.Azure; +using Microsoft.WindowsAzure.Commands.Common; using System; using System.Collections.Generic; using System.Linq; using System.Management.Automation; +using System.Runtime.InteropServices; using System.Security; +using System.Security.Authentication.ExtendedProtection; +using System.Text; using AuthenticationMessages = Microsoft.Azure.Commands.Common.Authentication.Properties.Resources; using ProfileMessages = Microsoft.Azure.Commands.Profile.Properties.Resources; @@ -38,6 +45,7 @@ public class RMProfileClient private IAzureTokenCache _cache; public Action WarningLog; public Action DebugLog; + public Action InformationLog; private IAzureContext DefaultContext { @@ -116,6 +124,8 @@ private IAzureTenant GetDetailedTenantFromQueryHistory(List tenants return tenants?.Where(t => t.Id.Equals(tenantId))?.FirstOrDefault(); ; } + public delegate string Prompt(string message); + public AzureRmProfile Login( IAzureAccount account, IAzureEnvironment environment, @@ -129,11 +139,20 @@ public AzureRmProfile Login( string name = null, bool shouldPopulateContextList = true, int maxContextPopulation = Profile.ConnectAzureRmAccountCommand.DefaultMaxContextPopulation, - string authScope = null) + string authScope = null, + Prompt prompt = null, + bool isInteractiveAuthentication = false) { + if(isInteractiveAuthentication) WriteInformationMessage($"{Foreground.BrightYellow}{Resources.PleaseSelectAccount}{PSStyle.Reset}{System.Environment.NewLine}"); + IAzureSubscription defaultSubscription = null; IAzureTenant defaultTenant = null; - List tenants = null; + List queriedTenants = null; + List subscriptions = new List(); + List tempSubscriptions = null; + string tenantDomainName = null; + var lastUsedSubscription = isInteractiveAuthentication && string.IsNullOrEmpty(subscriptionId) && string.IsNullOrEmpty(subscriptionName) ? _profile?.DefaultContext?.Subscription : null; + string promptBehavior = (password == null && account.Type != AzureAccount.AccountType.AccessToken && @@ -202,6 +221,7 @@ public AzureRmProfile Login( }; WriteDebugMessage(string.Format(ProfileMessages.TenantDomainToTenantIdMessage, tenantIdOrName, token.TenantId)); + tenantDomainName = tenantIdOrName; tenantIdOrName = token.TenantId; } finally @@ -224,6 +244,7 @@ public AzureRmProfile Login( throw new ArgumentNullException(string.Format($"{e.Message}{Environment.NewLine}{baseMessage} {typeMessage}"), e); } + tempSubscriptions = null; if (TryGetTenantSubscription( token, account, @@ -232,21 +253,79 @@ public AzureRmProfile Login( subscriptionName, true, out defaultSubscription, - out defaultTenant)) + out defaultTenant, + out tempSubscriptions, + isInteractiveAuthentication)) { account.SetOrAppendProperty(AzureAccount.Property.Tenants, new[] { defaultTenant.Id.ToString() }); + if (tempSubscriptions?.Any() == true) + { + subscriptions.AddRange(tempSubscriptions); + } + var shouldWriteSubscriptionSelectionTable = isInteractiveAuthentication && subscriptions?.Count > 1; + var markDefaultSubscription = isInteractiveAuthentication && lastUsedSubscription != null; + subscriptions = subscriptions?.OrderBy(s => GetDetailedTenantFromQueryHistory(queriedTenants, s.GetProperty(AzureSubscription.Property.Tenants))?.GetProperty(AzureTenant.Property.DefaultDomain))?.ThenBy(s => s.Name)?.ToList(); + + if (shouldWriteSubscriptionSelectionTable) WriteSubscriptionSelectionTable(subscriptions, queriedTenants, lastUsedSubscription?.Id, markDefaultSubscription, tenantDomainName); + + if (shouldWriteSubscriptionSelectionTable && markDefaultSubscription) + { + WriteInformationMessage($"{Environment.NewLine}The default is marked with an {Foreground.White}*{PSStyle.Reset}; " + + $"the default tenant is '{GetDetailedTenantFromQueryHistory(queriedTenants, lastUsedSubscription?.GetProperty(AzureSubscription.Property.Tenants))?.GetProperty(AzureTenant.Property.DefaultDomain)}' " + + $"and subscription is '{lastUsedSubscription?.Name} ({lastUsedSubscription?.Id})'."); + } + + if (shouldWriteSubscriptionSelectionTable && null != prompt) + { + string input = markDefaultSubscription ? prompt($"{Environment.NewLine}Select a tenant and subscription (type a number or Enter to accept default): ") : prompt($"{Environment.NewLine}Select a tenant and subscription: "); + int selectedSubIndex = -1; + try + { + if (!string.IsNullOrEmpty(input)) + { + selectedSubIndex = Convert.ToInt32(input); + } + if (selectedSubIndex != -1) + { + defaultSubscription = subscriptions.ElementAt(selectedSubIndex - 1); + } + else if (selectedSubIndex == -1 && lastUsedSubscription != null) + { + defaultSubscription = lastUsedSubscription; + } + defaultTenant = GetDetailedTenantFromQueryHistory(queriedTenants, defaultSubscription.GetProperty(AzureSubscription.Property.Tenants)) ?? new AzureTenant { Id = tenantIdOrName }; + if (!string.IsNullOrEmpty(tenantDomainName)) + { + defaultTenant.ExtendedProperties.Add(AzureTenant.Property.DefaultDomain, tenantDomainName); + } + var defaultContext = new AzureContext(defaultSubscription, account, environment, defaultTenant); + WriteSelectedSubscriptionTable(defaultContext?.Subscription?.Name ?? defaultContext?.Subscription?.Id, defaultContext?.Tenant?.GetProperty(AzureTenant.Property.DefaultDomain) ?? tenantDomainName ?? defaultContext?.Tenant?.Id); + _profile.TrySetDefaultContext(defaultContext); + _profile.TryRemoveContext("Default"); + } + catch (ArgumentOutOfRangeException) + { + throw new AzPSException("Selected subscription and tenant is out of range, please type a correct number.", ErrorKind.UserError); + } + catch (FormatException) + { + throw new AzPSException("Typed subscription and tenant is not a number.", ErrorKind.UserError); + } + } } } // (tenant is not provided and subscription is present) OR // (tenant is not provided and subscription is not provided) else { - tenants = ListAccountTenants(account, environment, password, promptBehavior, promptAction).ToList(); + queriedTenants = ListAccountTenants(account, environment, password, promptBehavior, promptAction).ToList(); account.SetProperty(AzureAccount.Property.Tenants, null); string accountId = null; IAzureTenant tempTenant = null; IAzureSubscription tempSubscription = null; - foreach (var tenant in tenants) + tempSubscriptions = null; + + foreach (var tenant in queriedTenants) { tempTenant = null; tempSubscription = null; @@ -284,7 +363,7 @@ public AzureRmProfile Login( if (token != null && defaultTenant == null && - TryGetTenantSubscription(token, account, environment, subscriptionId, subscriptionName, false, out tempSubscription, out tempTenant)) + TryGetTenantSubscription(token, account, environment, subscriptionId, subscriptionName, false, out tempSubscription, out tempTenant, out tempSubscriptions, isInteractiveAuthentication)) { // If no subscription found for the given token/tenant,discard tempTenant value. // Continue to look for matched subscripitons until one subscription retrived by its home tenant is found. @@ -296,11 +375,59 @@ public AzureRmProfile Login( defaultTenant = tempTenant; } } + if(tempSubscription != null) + { + subscriptions.AddRange(tempSubscriptions); + } } } defaultSubscription = defaultSubscription ?? tempSubscription; defaultTenant = defaultTenant ?? (defaultSubscription != null ? new AzureTenant() { Id = defaultSubscription.GetTenant() } : tempTenant); + var shouldWriteSubscriptionSelectionTable = isInteractiveAuthentication && subscriptions?.Count > 1; + var markDefaultSubscription = isInteractiveAuthentication && lastUsedSubscription != null; + subscriptions = subscriptions?.OrderBy(s => GetDetailedTenantFromQueryHistory(queriedTenants, s.GetProperty(AzureSubscription.Property.Tenants))?.GetProperty(AzureTenant.Property.DefaultDomain))?.ThenBy(s => s.Name)?.ToList(); + if (shouldWriteSubscriptionSelectionTable) WriteSubscriptionSelectionTable(subscriptions, queriedTenants, lastUsedSubscription?.Id, markDefaultSubscription, tenantDomainName); + + if (shouldWriteSubscriptionSelectionTable && markDefaultSubscription) + { + WriteInformationMessage($"{Environment.NewLine}The default is marked with an {Foreground.White}*{PSStyle.Reset}; " + + $"the default tenant is '{GetDetailedTenantFromQueryHistory(queriedTenants, lastUsedSubscription?.GetProperty(AzureSubscription.Property.Tenants))?.GetProperty(AzureTenant.Property.DefaultDomain)}' " + + $"and subscription is '{lastUsedSubscription?.Name} ({lastUsedSubscription?.Id})'."); + } + + if (shouldWriteSubscriptionSelectionTable && null != prompt) + { + string input = markDefaultSubscription ? prompt($"{Environment.NewLine}Select a tenant and subscription (type a number or Enter to accept default): ") : prompt($"{Environment.NewLine}Select a tenant and subscription: "); + int selectedSubIndex = -1; + try + { + if (!string.IsNullOrEmpty(input)) + { + selectedSubIndex = Convert.ToInt32(input); + } + if (selectedSubIndex != -1) + { + defaultSubscription = subscriptions.ElementAt(selectedSubIndex - 1); + }else if (selectedSubIndex == -1 && lastUsedSubscription!= null) + { + defaultSubscription = lastUsedSubscription; + } + defaultTenant = GetDetailedTenantFromQueryHistory(queriedTenants, defaultSubscription.GetProperty(AzureSubscription.Property.Tenants)); + var defaultContext = new AzureContext(defaultSubscription, account, environment, defaultTenant); + WriteSelectedSubscriptionTable(defaultContext?.Subscription?.Name ?? defaultContext?.Subscription?.Id, defaultContext?.Tenant?.GetProperty(AzureTenant.Property.DefaultDomain) ?? tenantIdOrName ?? defaultContext?.Tenant?.Id); + _profile.TrySetDefaultContext(defaultContext); + _profile.TryRemoveContext("Default"); + } + catch (ArgumentOutOfRangeException) + { + throw new AzPSException("Selected subscription and tenant is out of range, please type a correct number.", ErrorKind.UserError); + } + catch (FormatException) + { + throw new AzPSException("Typed subscription and tenant is not a number.", ErrorKind.UserError); + } + } } } @@ -316,7 +443,7 @@ public AzureRmProfile Login( throw new PSInvalidOperationException(String.Format(ResourceMessages.SubscriptionNameNotFound, account.Id, subscriptionName) + " " + ProfileMessages.SubscriptionNotFouldPleaseCheckConfig); } - defaultTenant = GetDetailedTenantFromQueryHistory(tenants, defaultTenant.Id) ?? defaultTenant; + defaultTenant = GetDetailedTenantFromQueryHistory(queriedTenants, defaultTenant?.Id) ?? defaultTenant; var defaultContext = new AzureContext(account, environment, defaultTenant); if (!_profile.TrySetDefaultContext(name, defaultContext)) { @@ -325,7 +452,7 @@ public AzureRmProfile Login( } else { - defaultTenant = GetDetailedTenantFromQueryHistory(tenants, defaultTenant.Id) ?? defaultTenant; ; + defaultTenant = GetDetailedTenantFromQueryHistory(queriedTenants, defaultTenant?.Id) ?? defaultTenant; ; var defaultContext = new AzureContext(defaultSubscription, account, environment, defaultTenant); if (!_profile.TrySetDefaultContext(name, defaultContext)) { @@ -344,15 +471,14 @@ public AzureRmProfile Login( if (shouldPopulateContextList && maxContextPopulation != 0) { var defaultContext = _profile.DefaultContext; - var subscriptions = maxContextPopulation > 0 ? ListSubscriptions(tenantIdOrName).Take(maxContextPopulation) : ListSubscriptions(tenantIdOrName); - - foreach (var subscription in subscriptions) + var populatedSubscriptions = maxContextPopulation > 0 && !isInteractiveAuthentication ? ListSubscriptions(tenantIdOrName).Take(maxContextPopulation) : ListSubscriptions(tenantIdOrName); + + foreach (var subscription in populatedSubscriptions) { - IAzureTenant tempTenant = GetDetailedTenantFromQueryHistory(tenants, subscription.GetProperty(AzureSubscription.Property.Tenants)) ?? new AzureTenant() + IAzureTenant tempTenant = GetDetailedTenantFromQueryHistory(queriedTenants, subscription.GetProperty(AzureSubscription.Property.Tenants)) ?? new AzureTenant() { Id = subscription.GetProperty(AzureSubscription.Property.Tenants) }; - var tempContext = new AzureContext(subscription, account, environment, tempTenant); tempContext.TokenCache = _cache; string tempName = null; @@ -367,7 +493,6 @@ public AzureRmProfile Login( WriteWarningMessage(string.Format(Resources.CannotCreateContext, subscription.Id)); } } - _profile.TrySetDefaultContext(defaultContext); _profile.TryRemoveContext("Default"); } @@ -375,6 +500,65 @@ public AzureRmProfile Login( return _profile.ToProfile(); } + int ColumnNoWidth = 4, ColumnSubNameWidth = 36, ColumnSubIdWidth = 40, ColumnTenantWidth = 26, ColumnIdentsWidth = 4; + string defaultSubscriptionMark = $"{Foreground.White} * {PSStyle.Reset}"; + + private void WriteSubscriptionSelectionTable(IEnumerable subscriptions, IEnumerable tenants, string defaultSubscriptionId, bool markDefaultSubscription = false, string tenantIdOrName = "") + { + WriteSubscriptionSelectionTableHeader(); + int subCount = 0; + foreach (var subscription in subscriptions) + { + WriteSubscriptionSelectionTableRow(++subCount, subscription, + tenants?.Where(t => t.Id.Equals(subscription?.GetTenant()))?.FirstOrDefault()?.GetProperty(AzureTenant.Property.DefaultDomain) ?? tenantIdOrName, + subscription.Id.Equals(defaultSubscriptionId), markDefaultSubscription); + } + } + + private void WriteSubscriptionSelectionTableHeader() + { + string ColumnNoTab = "No", ColumnSubNameTab = "Subscription name", ColumnSubIdTab = "Subscription ID", ColumnTenantTab = "Tenant domain name"; + string ColumNoSeparator = "----", ColumSubNameSeparator = "------------------------------------", ColumSubIdSeparator = "----------------------------------------", ColumTenantSeparator = "--------------------------"; + WriteInformationMessage($"[Tenant and subscription selection]{Environment.NewLine}"); + WriteInformationMessage($"{String.Format($"{{0,-{ColumnNoWidth + ColumnIdentsWidth}}}", ColumnNoTab)}" + + $"{String.Format($"{{0,-{ColumnSubNameWidth + ColumnIdentsWidth}}}", ColumnSubNameTab)}" + + $"{String.Format($"{{0,-{ColumnSubIdWidth + ColumnIdentsWidth}}}", ColumnSubIdTab)}" + + $"{String.Format($"{{0,-{ColumnTenantWidth + ColumnIdentsWidth}}}", ColumnTenantTab)}"); + WriteInformationMessage($"{String.Format($"{{0,-{ColumnNoWidth + ColumnIdentsWidth}}}", ColumNoSeparator)}" + + $"{String.Format($"{{0,-{ColumnSubNameWidth + ColumnIdentsWidth}}}", ColumSubNameSeparator)}" + + $"{String.Format($"{{0,-{ColumnSubIdWidth + ColumnIdentsWidth}}}", ColumSubIdSeparator)}" + + $"{String.Format($"{{0,-{ColumnTenantWidth}}}", ColumTenantSeparator)}"); + } + + private void WriteSubscriptionSelectionTableRow(int subIndex, IAzureSubscription subscription, string tenantDomainName, bool isDefaultSubscription, bool markDefaultSubscription = false) + { + int markedIndexLength = isDefaultSubscription && markDefaultSubscription ? ColumnNoWidth + ColumnIdentsWidth -3 + defaultSubscriptionMark.Length : ColumnNoWidth + ColumnIdentsWidth; + string markedSubIndex = $"[{subIndex}]{(isDefaultSubscription && markDefaultSubscription ? defaultSubscriptionMark : "")}"; + string truncatedSubName = subscription.Name?.Length > ColumnSubNameWidth ? $"{subscription.Name.Substring(0, ColumnSubNameWidth - 3)}..." : subscription.Name; + string truncatedDomainName = tenantDomainName?.Length > ColumnTenantWidth ? $"{tenantDomainName.Substring(0, ColumnTenantWidth - 3)}..." : tenantDomainName; + string subIndexRowValue = String.Format($"{{0,-{markedIndexLength}}}", markedSubIndex), + subNameRowValue = String.Format($"{{0,-{ColumnSubNameWidth + ColumnIdentsWidth}}}", truncatedSubName), + subIdRowValue = String.Format($"{{0,-{ColumnSubIdWidth + ColumnIdentsWidth}}}", subscription.Id), + tenantDomainNameRowValue = String.Format($"{{0,-{ColumnTenantWidth}}}", truncatedDomainName); + WriteInformationMessage($"{(isDefaultSubscription && markDefaultSubscription ? Foreground.Bold : "")}{subIndexRowValue}{(isDefaultSubscription && markDefaultSubscription ? PSStyle.Reset : "")}{(isDefaultSubscription && markDefaultSubscription ? Foreground.BrightCyan : "")}{subNameRowValue}{subIdRowValue}{tenantDomainNameRowValue}{(isDefaultSubscription && markDefaultSubscription ? PSStyle.Reset : "")}"); + } + + private void WriteSelectedSubscriptionTable(string subscription, string tenant) + { + string ColumnSubNameTab = "Subscription name", ColumnTenantTab = "Tenant domain name"; + string ColumSubNameSeparator = "------------------------------------", ColumTenantSeparator = "--------------------------"; + WriteInformationMessage(""); + WriteInformationMessage($"{String.Format($"{{0,-{ColumnSubNameWidth + ColumnIdentsWidth}}}", ColumnSubNameTab)}" + + $"{String.Format($"{{0,-{ColumnTenantWidth + ColumnIdentsWidth}}}", ColumnTenantTab)}"); + WriteInformationMessage($"{String.Format($"{{0,-{ColumnSubNameWidth + ColumnIdentsWidth}}}", ColumSubNameSeparator)}" + + $"{String.Format($"{{0,-{ColumnTenantWidth}}}", ColumTenantSeparator)}"); + string truncatedSubName = subscription?.Length > ColumnSubNameWidth ? $"{subscription.Substring(0, ColumnSubNameWidth - 3)}..." : subscription; + string truncatedDomainName = tenant?.Length > ColumnTenantWidth ? $"{tenant.Substring(0, ColumnTenantWidth - 3)}..." : tenant; + string subNameRowValue = String.Format($"{{0,-{ColumnSubNameWidth + ColumnIdentsWidth}}}", truncatedSubName), + tenantDomainNameRowValue = String.Format($"{{0,-{ColumnTenantWidth}}}", truncatedDomainName); + WriteInformationMessage($"{subNameRowValue}{tenantDomainNameRowValue}"); + } + public IAzureContext SetCurrentContext(string subscriptionNameOrId, string tenantId, string name = null) { IAzureSubscription subscription = null; @@ -582,7 +766,7 @@ public IAccessToken AcquireAccessToken(string tenantId, Action promptAct public IEnumerable ListSubscriptions(string tenantIdOrDomain = "") { List tenants = string.IsNullOrEmpty(tenantIdOrDomain) ? ListTenants(tenantIdOrDomain) : - new List() { CreateTenant(tenantIdOrDomain) }; + new List() { CreateTenant(tenantIdOrDomain) }; List subscriptions = new List(); @@ -604,7 +788,6 @@ public IEnumerable ListSubscriptions(string tenantIdOrDomain } } - return subscriptions; } @@ -656,8 +839,11 @@ private bool TryGetTenantSubscription(IAccessToken accessToken, string subscriptionName, bool isTenantPresent, out IAzureSubscription subscription, - out IAzureTenant tenant) + out IAzureTenant tenant, + out List subscriptions, + bool isInteractiveAuthentication) { + subscriptions = new List(); subscription = null; if (accessToken != null) { @@ -669,8 +855,8 @@ private bool TryGetTenantSubscription(IAccessToken accessToken, } else { - var subscriptions = SubscriptionAndTenantClient?.ListAllSubscriptionsForTenant(accessToken, new AzureAccount { Id = accessToken.UserId }, environment)?.ToList() - .Where(s => "enabled".Equals(s.State.ToString(), StringComparison.OrdinalIgnoreCase) || "warned".Equals(s.State.ToString(), StringComparison.OrdinalIgnoreCase)); + subscriptions = SubscriptionAndTenantClient?.ListAllSubscriptionsForTenant(accessToken, new AzureAccount { Id = accessToken.UserId }, environment)?.ToList() + .Where(s => "enabled".Equals(s.State.ToString(), StringComparison.OrdinalIgnoreCase) || "warned".Equals(s.State.ToString(), StringComparison.OrdinalIgnoreCase))?.ToList(); account.SetProperty(AzureAccount.Property.Subscriptions, subscriptions.Select(i => i.GetId().ToString()).ToArray()); @@ -685,14 +871,14 @@ private bool TryGetTenantSubscription(IAccessToken accessToken, { if (subscriptions.Count() > 1) { - WriteWarningMessage(string.Format( - "TenantId '{0}' contains more than one active subscription. First one will be selected for further use. " + - "To select another subscription, use Set-AzContext. " + - Environment.NewLine + + if (!isInteractiveAuthentication) + WriteWarningMessage(string.Format( + "TenantId '{0}' contains more than one active subscription. First one will be selected for further use. " + + "To select another subscription, use Set-AzContext.", accessToken.TenantId)); + WriteWarningMessage( "To override which subscription Connect-AzAccount selects by default, " + "use `Update-AzConfig -DefaultSubscriptionForLogin 00000000-0000-0000-0000-000000000000`. " + - "Go to https://go.microsoft.com/fwlink/?linkid=2200610 for more information.", - accessToken.TenantId)); + "Go to https://go.microsoft.com/fwlink/?linkid=2200610 for more information."); } subscription = subscription ?? subscriptions.First(); } @@ -828,6 +1014,13 @@ private void WriteDebugMessage(string message) DebugLog(message); } } + private void WriteInformationMessage(string message) + { + if (InformationLog != null) + { + InformationLog(message); + } + } public ISubscriptionClientWrapper SubscriptionAndTenantClient = null; } diff --git a/src/Accounts/Accounts/Properties/Resources.Designer.cs b/src/Accounts/Accounts/Properties/Resources.Designer.cs index 1e31ef0f240a..45077d2bd298 100644 --- a/src/Accounts/Accounts/Properties/Resources.Designer.cs +++ b/src/Accounts/Accounts/Properties/Resources.Designer.cs @@ -816,6 +816,15 @@ internal static string PasswordNotSupported { } } + /// + /// Looks up a localized string similar to Please select the account you want to login with.. + /// + internal static string PleaseSelectAccount { + get { + return ResourceManager.GetString("PleaseSelectAccount", resourceCulture); + } + } + /// /// Looks up a localized string similar to This function is in preview. It may not be available in the selected subscription.. /// diff --git a/src/Accounts/Accounts/Properties/Resources.resx b/src/Accounts/Accounts/Properties/Resources.resx index f3d5dceb694b..9d1892459997 100644 --- a/src/Accounts/Accounts/Properties/Resources.resx +++ b/src/Accounts/Accounts/Properties/Resources.resx @@ -598,4 +598,7 @@ Please do not set InformationAction or $InformationPreference to Ignore if you want to use device code authentication. + + Please select the account you want to login with. + \ No newline at end of file diff --git a/src/Accounts/Accounts/Utilities/Color.cs b/src/Accounts/Accounts/Utilities/Color.cs new file mode 100644 index 000000000000..6e5eef2b9adc --- /dev/null +++ b/src/Accounts/Accounts/Utilities/Color.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Azure.Commands.Profile.Utilities +{ + public class Foreground + { + public static string BrightYellow { get; } = "\x1b[93m"; + public static string BrightBlack { get; } = "\x1b[90m"; + public static string White { get; } = "\x1b[37m"; + public static string BrightCyan { get; } = "\x1b[96m"; + + public static string BoldOff { get; } = "\x1b[22m"; + + /// + /// Gets value to turn on blink. + /// + public static string Bold { get; } = "\x1b[1m"; + + } +} diff --git a/src/Accounts/Authentication.ResourceManager/Models/PSAzureProfile.cs b/src/Accounts/Authentication.ResourceManager/Models/PSAzureProfile.cs index 60b364abd54d..497f6f51be52 100644 --- a/src/Accounts/Authentication.ResourceManager/Models/PSAzureProfile.cs +++ b/src/Accounts/Authentication.ResourceManager/Models/PSAzureProfile.cs @@ -84,10 +84,8 @@ public IDictionary Environments /// /// The current credentials and metadata for connecting with the current Azure cloud instance. /// - [Ps1Xml(Label = "Account", Target = ViewControl.Table, ScriptBlock = "$_.Context.Account.ToString()", Position = 0)] - [Ps1Xml(Label = "SubscriptionName", Target = ViewControl.Table, ScriptBlock = "$_.Context.Subscription.Name", Position = 1)] - [Ps1Xml(Label = "TenantId", Target = ViewControl.Table, ScriptBlock = "$_.Context.Tenant.ToString()", Position = 2)] - [Ps1Xml(Label = "Environment", Target = ViewControl.Table, ScriptBlock = "$_.Context.Environment.ToString()", Position = 3)] + [Ps1Xml(Label = "Subscription name", Target = ViewControl.Table, ScriptBlock = "if($null -ne $_.Context.Subscription.Name){$_.Context.Subscription.Name}else{$_.Context.Subscription.Id}", Position = 0)] + [Ps1Xml(Label = "Tenant", Target = ViewControl.Table, ScriptBlock = "if($null -ne $_.Context.Tenant.DefaultDomain){$_.Context.Tenant.DefaultDomain}else{$_.Context.Tenant.Id}", Position = 1)] public PSAzureContext Context { get; set; } public override string ToString() From 7a7abf155bdaf636213b00d8927a15366e81099a Mon Sep 17 00:00:00 2001 From: Beisi Zhou Date: Tue, 26 Mar 2024 14:29:52 +0800 Subject: [PATCH 2/3] code refactor --- .../Accounts/Models/RMProfileClient.cs | 170 ++++++++---------- 1 file changed, 74 insertions(+), 96 deletions(-) diff --git a/src/Accounts/Accounts/Models/RMProfileClient.cs b/src/Accounts/Accounts/Models/RMProfileClient.cs index e0c028a7987f..03f65009763e 100644 --- a/src/Accounts/Accounts/Models/RMProfileClient.cs +++ b/src/Accounts/Accounts/Models/RMProfileClient.cs @@ -21,6 +21,7 @@ using Microsoft.Azure.Commands.Profile.Properties; using Microsoft.Azure.Commands.Profile.Utilities; using Microsoft.Azure.Management.Profiles.Storage.Version2019_06_01.Models; +using Microsoft.Identity.Client.NativeInterop; using Microsoft.Rest.Azure; using Microsoft.WindowsAzure.Commands.Common; @@ -46,6 +47,7 @@ public class RMProfileClient public Action WarningLog; public Action DebugLog; public Action InformationLog; + private List _queriedTenants = new List(); private IAzureContext DefaultContext { @@ -119,9 +121,14 @@ public bool TryRemoveContext(IAzureContext context) return result; } - private IAzureTenant GetDetailedTenantFromQueryHistory(List tenants, string tenantId) + private IAzureTenant GetDetailedTenantFromQueryHistory(string tenantId) { - return tenants?.Where(t => t.Id.Equals(tenantId))?.FirstOrDefault(); ; + return _queriedTenants?.Where(t => t.Id.Equals(tenantId))?.FirstOrDefault(); ; + } + + private string GetDefaultDomain(string tenantId) + { + return GetDetailedTenantFromQueryHistory(tenantId)?.GetProperty(AzureTenant.Property.DefaultDomain) ?? ""; } public delegate string Prompt(string message); @@ -147,7 +154,6 @@ public AzureRmProfile Login( IAzureSubscription defaultSubscription = null; IAzureTenant defaultTenant = null; - List queriedTenants = null; List subscriptions = new List(); List tempSubscriptions = null; string tenantDomainName = null; @@ -258,59 +264,18 @@ public AzureRmProfile Login( isInteractiveAuthentication)) { account.SetOrAppendProperty(AzureAccount.Property.Tenants, new[] { defaultTenant.Id.ToString() }); + if (tempSubscriptions?.Any() == true) { subscriptions.AddRange(tempSubscriptions); } var shouldWriteSubscriptionSelectionTable = isInteractiveAuthentication && subscriptions?.Count > 1; - var markDefaultSubscription = isInteractiveAuthentication && lastUsedSubscription != null; - subscriptions = subscriptions?.OrderBy(s => GetDetailedTenantFromQueryHistory(queriedTenants, s.GetProperty(AzureSubscription.Property.Tenants))?.GetProperty(AzureTenant.Property.DefaultDomain))?.ThenBy(s => s.Name)?.ToList(); - - if (shouldWriteSubscriptionSelectionTable) WriteSubscriptionSelectionTable(subscriptions, queriedTenants, lastUsedSubscription?.Id, markDefaultSubscription, tenantDomainName); - - if (shouldWriteSubscriptionSelectionTable && markDefaultSubscription) - { - WriteInformationMessage($"{Environment.NewLine}The default is marked with an {Foreground.White}*{PSStyle.Reset}; " + - $"the default tenant is '{GetDetailedTenantFromQueryHistory(queriedTenants, lastUsedSubscription?.GetProperty(AzureSubscription.Property.Tenants))?.GetProperty(AzureTenant.Property.DefaultDomain)}' " + - $"and subscription is '{lastUsedSubscription?.Name} ({lastUsedSubscription?.Id})'."); - } - - if (shouldWriteSubscriptionSelectionTable && null != prompt) + if (shouldWriteSubscriptionSelectionTable && prompt != null) { - string input = markDefaultSubscription ? prompt($"{Environment.NewLine}Select a tenant and subscription (type a number or Enter to accept default): ") : prompt($"{Environment.NewLine}Select a tenant and subscription: "); - int selectedSubIndex = -1; - try - { - if (!string.IsNullOrEmpty(input)) - { - selectedSubIndex = Convert.ToInt32(input); - } - if (selectedSubIndex != -1) - { - defaultSubscription = subscriptions.ElementAt(selectedSubIndex - 1); - } - else if (selectedSubIndex == -1 && lastUsedSubscription != null) - { - defaultSubscription = lastUsedSubscription; - } - defaultTenant = GetDetailedTenantFromQueryHistory(queriedTenants, defaultSubscription.GetProperty(AzureSubscription.Property.Tenants)) ?? new AzureTenant { Id = tenantIdOrName }; - if (!string.IsNullOrEmpty(tenantDomainName)) - { - defaultTenant.ExtendedProperties.Add(AzureTenant.Property.DefaultDomain, tenantDomainName); - } - var defaultContext = new AzureContext(defaultSubscription, account, environment, defaultTenant); - WriteSelectedSubscriptionTable(defaultContext?.Subscription?.Name ?? defaultContext?.Subscription?.Id, defaultContext?.Tenant?.GetProperty(AzureTenant.Property.DefaultDomain) ?? tenantDomainName ?? defaultContext?.Tenant?.Id); - _profile.TrySetDefaultContext(defaultContext); - _profile.TryRemoveContext("Default"); - } - catch (ArgumentOutOfRangeException) - { - throw new AzPSException("Selected subscription and tenant is out of range, please type a correct number.", ErrorKind.UserError); - } - catch (FormatException) - { - throw new AzPSException("Typed subscription and tenant is not a number.", ErrorKind.UserError); - } + InteractiveSubscriptionSelection(subscriptions, prompt, ref defaultSubscription, ref defaultTenant, tenantIdOrName, tenantDomainName, lastUsedSubscription); + var defaultContext = new AzureContext(defaultSubscription, account, environment, defaultTenant); + _profile.TrySetDefaultContext(defaultContext); + _profile.TryRemoveContext("Default"); } } } @@ -318,14 +283,14 @@ public AzureRmProfile Login( // (tenant is not provided and subscription is not provided) else { - queriedTenants = ListAccountTenants(account, environment, password, promptBehavior, promptAction).ToList(); + _queriedTenants = ListAccountTenants(account, environment, password, promptBehavior, promptAction).ToList(); account.SetProperty(AzureAccount.Property.Tenants, null); string accountId = null; IAzureTenant tempTenant = null; IAzureSubscription tempSubscription = null; tempSubscriptions = null; - foreach (var tenant in queriedTenants) + foreach (var tenant in _queriedTenants) { tempTenant = null; tempSubscription = null; @@ -384,49 +349,14 @@ public AzureRmProfile Login( defaultSubscription = defaultSubscription ?? tempSubscription; defaultTenant = defaultTenant ?? (defaultSubscription != null ? new AzureTenant() { Id = defaultSubscription.GetTenant() } : tempTenant); - var shouldWriteSubscriptionSelectionTable = isInteractiveAuthentication && subscriptions?.Count > 1; - var markDefaultSubscription = isInteractiveAuthentication && lastUsedSubscription != null; - subscriptions = subscriptions?.OrderBy(s => GetDetailedTenantFromQueryHistory(queriedTenants, s.GetProperty(AzureSubscription.Property.Tenants))?.GetProperty(AzureTenant.Property.DefaultDomain))?.ThenBy(s => s.Name)?.ToList(); - if (shouldWriteSubscriptionSelectionTable) WriteSubscriptionSelectionTable(subscriptions, queriedTenants, lastUsedSubscription?.Id, markDefaultSubscription, tenantDomainName); - - if (shouldWriteSubscriptionSelectionTable && markDefaultSubscription) - { - WriteInformationMessage($"{Environment.NewLine}The default is marked with an {Foreground.White}*{PSStyle.Reset}; " + - $"the default tenant is '{GetDetailedTenantFromQueryHistory(queriedTenants, lastUsedSubscription?.GetProperty(AzureSubscription.Property.Tenants))?.GetProperty(AzureTenant.Property.DefaultDomain)}' " + - $"and subscription is '{lastUsedSubscription?.Name} ({lastUsedSubscription?.Id})'."); - } - if (shouldWriteSubscriptionSelectionTable && null != prompt) + var shouldWriteSubscriptionSelectionTable = isInteractiveAuthentication && subscriptions?.Count > 1; + if (shouldWriteSubscriptionSelectionTable && prompt != null) { - string input = markDefaultSubscription ? prompt($"{Environment.NewLine}Select a tenant and subscription (type a number or Enter to accept default): ") : prompt($"{Environment.NewLine}Select a tenant and subscription: "); - int selectedSubIndex = -1; - try - { - if (!string.IsNullOrEmpty(input)) - { - selectedSubIndex = Convert.ToInt32(input); - } - if (selectedSubIndex != -1) - { - defaultSubscription = subscriptions.ElementAt(selectedSubIndex - 1); - }else if (selectedSubIndex == -1 && lastUsedSubscription!= null) - { - defaultSubscription = lastUsedSubscription; - } - defaultTenant = GetDetailedTenantFromQueryHistory(queriedTenants, defaultSubscription.GetProperty(AzureSubscription.Property.Tenants)); - var defaultContext = new AzureContext(defaultSubscription, account, environment, defaultTenant); - WriteSelectedSubscriptionTable(defaultContext?.Subscription?.Name ?? defaultContext?.Subscription?.Id, defaultContext?.Tenant?.GetProperty(AzureTenant.Property.DefaultDomain) ?? tenantIdOrName ?? defaultContext?.Tenant?.Id); - _profile.TrySetDefaultContext(defaultContext); - _profile.TryRemoveContext("Default"); - } - catch (ArgumentOutOfRangeException) - { - throw new AzPSException("Selected subscription and tenant is out of range, please type a correct number.", ErrorKind.UserError); - } - catch (FormatException) - { - throw new AzPSException("Typed subscription and tenant is not a number.", ErrorKind.UserError); - } + InteractiveSubscriptionSelection(subscriptions, prompt, ref defaultSubscription, ref defaultTenant, tenantIdOrName, tenantDomainName, lastUsedSubscription); + var defaultContext = new AzureContext(defaultSubscription, account, environment, defaultTenant); + _profile.TrySetDefaultContext(defaultContext); + _profile.TryRemoveContext("Default"); } } } @@ -443,7 +373,7 @@ public AzureRmProfile Login( throw new PSInvalidOperationException(String.Format(ResourceMessages.SubscriptionNameNotFound, account.Id, subscriptionName) + " " + ProfileMessages.SubscriptionNotFouldPleaseCheckConfig); } - defaultTenant = GetDetailedTenantFromQueryHistory(queriedTenants, defaultTenant?.Id) ?? defaultTenant; + defaultTenant = GetDetailedTenantFromQueryHistory(defaultTenant?.Id) ?? defaultTenant; var defaultContext = new AzureContext(account, environment, defaultTenant); if (!_profile.TrySetDefaultContext(name, defaultContext)) { @@ -452,7 +382,7 @@ public AzureRmProfile Login( } else { - defaultTenant = GetDetailedTenantFromQueryHistory(queriedTenants, defaultTenant?.Id) ?? defaultTenant; ; + defaultTenant = GetDetailedTenantFromQueryHistory(defaultTenant?.Id) ?? defaultTenant; ; var defaultContext = new AzureContext(defaultSubscription, account, environment, defaultTenant); if (!_profile.TrySetDefaultContext(name, defaultContext)) { @@ -475,7 +405,7 @@ public AzureRmProfile Login( foreach (var subscription in populatedSubscriptions) { - IAzureTenant tempTenant = GetDetailedTenantFromQueryHistory(queriedTenants, subscription.GetProperty(AzureSubscription.Property.Tenants)) ?? new AzureTenant() + IAzureTenant tempTenant = GetDetailedTenantFromQueryHistory(subscription.GetProperty(AzureSubscription.Property.Tenants)) ?? new AzureTenant() { Id = subscription.GetProperty(AzureSubscription.Property.Tenants) }; @@ -503,6 +433,54 @@ public AzureRmProfile Login( int ColumnNoWidth = 4, ColumnSubNameWidth = 36, ColumnSubIdWidth = 40, ColumnTenantWidth = 26, ColumnIdentsWidth = 4; string defaultSubscriptionMark = $"{Foreground.White} * {PSStyle.Reset}"; + private void InteractiveSubscriptionSelection(IEnumerable subscriptions, Prompt prompt, ref IAzureSubscription defaultSubscription, ref IAzureTenant defaultTenant, string tenantId, string tenantDomainName, IAzureSubscription lastUsedSubscription = null) + { + subscriptions = subscriptions?.OrderBy(s => GetDetailedTenantFromQueryHistory(GetDefaultDomain(s.GetProperty(AzureSubscription.Property.Tenants))))?.ThenBy(s => s.Name)?.ToList(); + + var markDefaultSubscription = lastUsedSubscription != null; + + WriteSubscriptionSelectionTable(subscriptions, _queriedTenants, lastUsedSubscription?.Id, markDefaultSubscription, tenantDomainName); + + if (markDefaultSubscription) + { + WriteInformationMessage($"{Environment.NewLine}The default is marked with an {Foreground.White}*{PSStyle.Reset}; " + + $"the default tenant is '{GetDetailedTenantFromQueryHistory(GetDefaultDomain(lastUsedSubscription?.GetProperty(AzureSubscription.Property.Tenants)))}' " + + $"and subscription is '{lastUsedSubscription?.Name} ({lastUsedSubscription?.Id})'."); + } + + string input = markDefaultSubscription ? prompt($"{Environment.NewLine}Select a tenant and subscription (type a number or Enter to accept default): ") : prompt($"{Environment.NewLine}Select a tenant and subscription: "); + int selectedSubIndex = -1; + try + { + if (!string.IsNullOrEmpty(input)) + { + selectedSubIndex = Convert.ToInt32(input); + } + if (selectedSubIndex != -1) + { + defaultSubscription = subscriptions.ElementAt(selectedSubIndex - 1); + } + else if (selectedSubIndex == -1 && lastUsedSubscription != null) + { + defaultSubscription = lastUsedSubscription; + } + defaultTenant = GetDetailedTenantFromQueryHistory(defaultSubscription?.GetProperty(AzureSubscription.Property.Tenants)) ?? new AzureTenant { Id = tenantId }; + if (!string.IsNullOrEmpty(tenantDomainName)) + { + defaultTenant.ExtendedProperties.Add(AzureTenant.Property.DefaultDomain, tenantDomainName); + } + WriteSelectedSubscriptionTable(defaultSubscription?.Name ?? defaultSubscription?.Id, defaultTenant?.GetProperty(AzureTenant.Property.DefaultDomain) ?? tenantDomainName ?? defaultTenant?.Id); + } + catch (ArgumentOutOfRangeException) + { + throw new AzPSException("Selected subscription and tenant is out of range, please type a correct number.", ErrorKind.UserError); + } + catch (FormatException) + { + throw new AzPSException("Typed subscription and tenant is not a number.", ErrorKind.UserError); + } + } + private void WriteSubscriptionSelectionTable(IEnumerable subscriptions, IEnumerable tenants, string defaultSubscriptionId, bool markDefaultSubscription = false, string tenantIdOrName = "") { WriteSubscriptionSelectionTableHeader(); From 78e51076d1d24bc36d05e9afdc1e0bf75f4cee96 Mon Sep 17 00:00:00 2001 From: Beisi Zhou Date: Tue, 26 Mar 2024 15:14:53 +0800 Subject: [PATCH 3/3] throw error if enter nothing --- src/Accounts/Accounts/Models/RMProfileClient.cs | 6 ++++++ src/Accounts/Accounts/Properties/Resources.Designer.cs | 9 +++++++++ src/Accounts/Accounts/Properties/Resources.resx | 3 +++ 3 files changed, 18 insertions(+) diff --git a/src/Accounts/Accounts/Models/RMProfileClient.cs b/src/Accounts/Accounts/Models/RMProfileClient.cs index 03f65009763e..0939d72667c9 100644 --- a/src/Accounts/Accounts/Models/RMProfileClient.cs +++ b/src/Accounts/Accounts/Models/RMProfileClient.cs @@ -250,6 +250,7 @@ public AzureRmProfile Login( throw new ArgumentNullException(string.Format($"{e.Message}{Environment.NewLine}{baseMessage} {typeMessage}"), e); } + WriteInformationMessage(Resources.RetrievingSubscription); tempSubscriptions = null; if (TryGetTenantSubscription( token, @@ -290,6 +291,8 @@ public AzureRmProfile Login( IAzureSubscription tempSubscription = null; tempSubscriptions = null; + WriteInformationMessage(Resources.RetrievingSubscription); + foreach (var tenant in _queriedTenants) { tempTenant = null; @@ -463,6 +466,9 @@ private void InteractiveSubscriptionSelection(IEnumerable su else if (selectedSubIndex == -1 && lastUsedSubscription != null) { defaultSubscription = lastUsedSubscription; + }else if(selectedSubIndex == -1 && lastUsedSubscription == null) + { + throw new AzPSException("Please type a number to select a subscription.", ErrorKind.UserError); } defaultTenant = GetDetailedTenantFromQueryHistory(defaultSubscription?.GetProperty(AzureSubscription.Property.Tenants)) ?? new AzureTenant { Id = tenantId }; if (!string.IsNullOrEmpty(tenantDomainName)) diff --git a/src/Accounts/Accounts/Properties/Resources.Designer.cs b/src/Accounts/Accounts/Properties/Resources.Designer.cs index 45077d2bd298..114acda92b6d 100644 --- a/src/Accounts/Accounts/Properties/Resources.Designer.cs +++ b/src/Accounts/Accounts/Properties/Resources.Designer.cs @@ -1032,6 +1032,15 @@ internal static string ReplaceContextQuery { } } + /// + /// Looks up a localized string similar to Retrieving subscriptions for the selection.... + /// + internal static string RetrievingSubscription { + get { + return ResourceManager.GetString("RetrievingSubscription", resourceCulture); + } + } + /// /// Looks up a localized string similar to The Resource Manager context was not properly initialized. Please load the module again.. /// diff --git a/src/Accounts/Accounts/Properties/Resources.resx b/src/Accounts/Accounts/Properties/Resources.resx index 9d1892459997..56a050a123b7 100644 --- a/src/Accounts/Accounts/Properties/Resources.resx +++ b/src/Accounts/Accounts/Properties/Resources.resx @@ -601,4 +601,7 @@ Please select the account you want to login with. + + Retrieving subscriptions for the selection... + \ No newline at end of file