diff --git a/src/Accounts/Accounts.Test/ProfileCmdletTests.cs b/src/Accounts/Accounts.Test/ProfileCmdletTests.cs index fdd16bf07357..3f648c2105e7 100644 --- a/src/Accounts/Accounts.Test/ProfileCmdletTests.cs +++ b/src/Accounts/Accounts.Test/ProfileCmdletTests.cs @@ -19,14 +19,22 @@ using Microsoft.Azure.Commands.ScenarioTest; using Microsoft.Azure.Commands.TestFx.Mocks; using Microsoft.Azure.ServiceManagement.Common.Models; +using Microsoft.WindowsAzure.Commands.Common; using Microsoft.WindowsAzure.Commands.Common.Test.Mocks; using Microsoft.WindowsAzure.Commands.ScenarioTest; using Microsoft.WindowsAzure.Commands.Test.Utilities.Common; using Microsoft.WindowsAzure.Commands.Utilities.Common; + using Moq; + +using Newtonsoft.Json; + using System; +using System.Collections.Generic; using System.Linq; using System.Management.Automation; +using System.Text.RegularExpressions; + using Xunit; using Xunit.Abstractions; @@ -36,6 +44,7 @@ public class ProfileCmdletTests : RMTestBase { private MemoryDataStore dataStore; private MockCommandRuntime commandRuntimeMock; + private List azKeyStoreData = new List(); private AzKeyStore keyStore; public ProfileCmdletTests(ITestOutputHelper output) @@ -53,8 +62,8 @@ private AzKeyStore SetMockedAzKeyStore() { var storageMocker = new Mock(); storageMocker.Setup(f => f.Create()).Returns(storageMocker.Object); - storageMocker.Setup(f => f.ReadData()).Returns(new byte[0]); - storageMocker.Setup(f => f.WriteData(It.IsAny())).Callback((byte[] s) => { }); + storageMocker.Setup(f => f.WriteData(It.IsAny())).Callback((byte[] s) => { azKeyStoreData.Clear(); azKeyStoreData.AddRange(s); }); + storageMocker.Setup(f => f.ReadData()).Returns(azKeyStoreData.ToArray()); var keyStore = new AzKeyStore(AzureSession.Instance.ARMProfileDirectory, "azkeystore", false, storageMocker.Object); return keyStore; } @@ -107,7 +116,7 @@ public void SelectAzureProfileFromDisk() { AzureSession.Instance.RegisterComponent(AzKeyStore.Name, () => keyStore, true); var profile = new AzureRmProfile(); - profile.EnvironmentTable.Add("foo", new AzureEnvironment(new AzureEnvironment( AzureEnvironment.PublicEnvironments.Values.FirstOrDefault()))); + profile.EnvironmentTable.Add("foo", new AzureEnvironment(new AzureEnvironment(AzureEnvironment.PublicEnvironments.Values.FirstOrDefault()))); profile.EnvironmentTable["foo"].Name = "foo"; profile.Save("X:\\foo.json"); ImportAzureRMContextCommand cmdlt = new ImportAzureRMContextCommand(); @@ -166,19 +175,26 @@ public void SaveAzureProfileNull() Assert.Throws(() => cmdlt.ExecuteCmdlet()); } + private const string password = "pa88w0rd!"; + [Fact] [Trait(Category.AcceptanceType, Category.CheckIn)] public void SaveAzureProfileFromDefault() { + string accountId = Guid.NewGuid().ToString(), + tenantId = Guid.NewGuid().ToString(), + subscriptionId = Guid.NewGuid().ToString(), + subscriptionName = "Contoso Subscription", + tenantName = "contoso.com"; + AzureSession.Instance.RegisterComponent(AzKeyStore.Name, () => keyStore, true); - var profile = new AzureRmProfile(); - profile.EnvironmentTable.Add("foo", new AzureEnvironment(AzureEnvironment.PublicEnvironments.Values.FirstOrDefault())); - profile.DefaultContext = new AzureContext(new AzureSubscription(), new AzureAccount(), profile.EnvironmentTable["foo"]); + var profile = GetProfile(accountId, tenantId, subscriptionId, subscriptionName, tenantName); AzureRmProfileProvider.Instance.Profile = profile; SaveAzureRMContextCommand cmdlt = new SaveAzureRMContextCommand(); // Setup cmdlt.Path = "X:\\foo.json"; cmdlt.CommandRuntime = commandRuntimeMock; + cmdlt.WithCredential = true; // Act cmdlt.InvokeBeginProcessing(); @@ -187,8 +203,47 @@ public void SaveAzureProfileFromDefault() // Verify Assert.True(AzureSession.Instance.DataStore.FileExists("X:\\foo.json")); - var profile2 = new AzureRmProfile("X:\\foo.json"); - Assert.True(profile2.EnvironmentTable.ContainsKey("foo")); + var profileString = AzureSession.Instance.DataStore.ReadFileAsText("X:\\foo.json"); + profileString = Regex.Replace(profileString, @"[^\u0000-\u007F]+", string.Empty); + var actual = JsonConvert.DeserializeObject(profileString, new AzureRmProfileConverter()); + Assert.Equal(password, actual.Contexts.First().Value.Account.GetProperty(AzureAccount.Property.ServicePrincipalSecret)); + Assert.Equal(accountId, actual.Contexts.First().Value.Account.Id); + Assert.Equal(tenantId, actual.Contexts.First().Value.Tenant.Id); + Assert.Equal(subscriptionId, actual.Contexts.First().Value.Subscription.Id); + Assert.Equal(subscriptionName, actual.Contexts.First().Value.Subscription.Name); + } + + private AzureRmProfile GetProfile(string accountId, string tenantId, string subscriptionId, string subscriptionName, string tenantName) + { + var account = new AzureAccount() + { + Id = accountId, + Type = AzureAccount.AccountType.ServicePrincipal + }; + var tenant = new AzureTenant() + { + Directory = tenantName, + Id = tenantId + }; + account.SetTenants(tenant.Id); + + keyStore.SaveSecureString(new ServicePrincipalKey(AzureAccount.Property.ServicePrincipalSecret, account.Id, tenant.Id), password.ConvertToSecureString()); + + var sub = new AzureSubscription() + { + Id = subscriptionId, + Name = subscriptionName + }; + + sub.SetAccount(account.Id); + sub.SetEnvironment(EnvironmentName.AzureCloud); + var context = new AzureContext(sub, + account, + AzureEnvironment.PublicEnvironments[EnvironmentName.AzureCloud], + tenant); + var profile = new AzureRmProfile(); + profile.TryAddContext(context, out _); + return profile; } } } diff --git a/src/Accounts/Accounts/ChangeLog.md b/src/Accounts/Accounts/ChangeLog.md index 060ed63d073c..8033bfe99f9f 100644 --- a/src/Accounts/Accounts/ChangeLog.md +++ b/src/Accounts/Accounts/ChangeLog.md @@ -19,6 +19,7 @@ --> ## Upcoming Release +* Refilled credentials from `AzKeyStore` when run `Save-AzContext` [#22355] * Added config `DisableErrorRecordsPersistence` to disable writing error records to file system [#21732] * Updated Azure.Core to 1.34.0. diff --git a/src/Accounts/Accounts/Context/SaveAzureRMContext.cs b/src/Accounts/Accounts/Context/SaveAzureRMContext.cs index 0df984f15d60..51494a6491a4 100644 --- a/src/Accounts/Accounts/Context/SaveAzureRMContext.cs +++ b/src/Accounts/Accounts/Context/SaveAzureRMContext.cs @@ -25,6 +25,7 @@ using System.Management.Automation; using Microsoft.Azure.Commands.Common.Authentication; using Microsoft.Azure.Commands.Common.Authentication.Abstractions; +using Microsoft.Azure.Commands.Common; namespace Microsoft.Azure.Commands.Profile { @@ -44,6 +45,9 @@ public class SaveAzureRMContextCommand : AzureRMCmdlet [Parameter(Mandatory=false, HelpMessage="Overwrite the given file if it exists")] public SwitchParameter Force { get; set; } + [Parameter(Mandatory = false, HelpMessage = "Export the credentials to the file")] + public SwitchParameter WithCredential { get; set; } + public override void ExecuteCmdlet() { Path = this.ResolveUserPath(Path); @@ -57,7 +61,13 @@ public override void ExecuteCmdlet() ShouldContinue(string.Format(Resources.FileOverwriteMessage, Path), Resources.FileOverwriteCaption)) { - Profile.Save(Path); + var profile = Profile; + if (WithCredential.IsPresent) + { + WriteWarning(string.Format(Resources.ProfileCredentialsWriteWarning, Path)); + profile = profile.RefillCredentialsFromKeyStore(); + } + profile.Save(Path); WriteVerbose(string.Format(Resources.ProfileArgumentSaved, Path)); } } @@ -76,7 +86,13 @@ public override void ExecuteCmdlet() ShouldContinue(string.Format(Resources.FileOverwriteMessage, Path), Resources.FileOverwriteCaption)) { - AzureRmProfileProvider.Instance.GetProfile().Save(Path); + var profile = AzureRmProfileProvider.Instance.GetProfile(); + if (WithCredential.IsPresent) + { + WriteWarning(string.Format(Resources.ProfileCredentialsWriteWarning, Path)); + profile = profile.RefillCredentialsFromKeyStore(); + } + profile.Save(Path); WriteVerbose(string.Format(Resources.ProfileCurrentSaved, Path)); } } diff --git a/src/Accounts/Accounts/Properties/Resources.Designer.cs b/src/Accounts/Accounts/Properties/Resources.Designer.cs index 35c4791c2a65..2298c200f30a 100644 --- a/src/Accounts/Accounts/Properties/Resources.Designer.cs +++ b/src/Accounts/Accounts/Properties/Resources.Designer.cs @@ -861,6 +861,15 @@ internal static string ProfileArgumentWrite { } } + /// + /// Looks up a localized string similar to Personally identifiable information and confidential data may be written to the file located at '{0}'. Please ensure that appropriate access controls are assigned to the saved file.. + /// + internal static string ProfileCredentialsWriteWarning { + get { + return ResourceManager.GetString("ProfileCredentialsWriteWarning", resourceCulture); + } + } + /// /// Looks up a localized string similar to The current profile was saved to the file system at '{0}'. This file may include personally identifiable information and secrets. Please ensure that the saved file is assigned appropriate access controls.. /// diff --git a/src/Accounts/Accounts/Properties/Resources.resx b/src/Accounts/Accounts/Properties/Resources.resx index e53f5b446d64..a8b3f3f8e0cd 100644 --- a/src/Accounts/Accounts/Properties/Resources.resx +++ b/src/Accounts/Accounts/Properties/Resources.resx @@ -592,4 +592,7 @@ The input domain is {0} and the tenant Id is {1} + + Personally identifiable information and confidential data may be written to the file located at '{0}'. Please ensure that appropriate access controls are assigned to the saved file. + \ No newline at end of file diff --git a/src/Accounts/Authentication.ResourceManager/AzureRmProfile.cs b/src/Accounts/Authentication.ResourceManager/AzureRmProfile.cs index a1a53393c7b2..5289e042d517 100644 --- a/src/Accounts/Authentication.ResourceManager/AzureRmProfile.cs +++ b/src/Accounts/Authentication.ResourceManager/AzureRmProfile.cs @@ -223,13 +223,13 @@ private IAzureContext MigrateSecretToKeyStore(IAzureContext context, AzKeyStore if (keystore != null) { var account = context.Account; - if (account.IsPropertySet(AzureAccount.Property.ServicePrincipalSecret)) + if (account != null && account.IsPropertySet(AzureAccount.Property.ServicePrincipalSecret)) { keystore?.SaveSecureString(new ServicePrincipalKey(AzureAccount.Property.ServicePrincipalSecret, account.Id, account.GetTenants().First()) , account.ExtendedProperties.GetProperty(AzureAccount.Property.ServicePrincipalSecret).ConvertToSecureString()); account.ExtendedProperties.Remove(AzureAccount.Property.ServicePrincipalSecret); } - if (account.IsPropertySet(AzureAccount.Property.CertificatePassword)) + if (account != null && account.IsPropertySet(AzureAccount.Property.CertificatePassword)) { keystore?.SaveSecureString(new ServicePrincipalKey(AzureAccount.Property.CertificatePassword, account.Id, account.GetTenants().First()) , account.ExtendedProperties.GetProperty(AzureAccount.Property.CertificatePassword).ConvertToSecureString()); @@ -243,6 +243,46 @@ private void LoadImpl(string contents) { } + /// + /// Refill the credentials from AzKeyStore to profile. Used for profile export. + /// + public AzureRmProfile RefillCredentialsFromKeyStore() + { + AzKeyStore keystore = null; + AzureSession.Instance.TryGetComponent(AzKeyStore.Name, out keystore); + AzureRmProfile ret = this.DeepCopy(); + if (keystore != null) + { + foreach (var context in ret.Contexts) + { + var account = context.Value.Account; + if (account?.Type == AzureAccount.AccountType.ServicePrincipal && !account.IsPropertySet(AzureAccount.Property.ServicePrincipalSecret)) + { + try + { + var secret = keystore.GetSecureString(new ServicePrincipalKey(AzureAccount.Property.ServicePrincipalSecret, account.Id, account.GetTenants().First())).ConvertToString(); + account.ExtendedProperties.SetProperty(AzureAccount.Property.ServicePrincipalSecret, secret); + } + catch + { + } + } + if (account?.Type == AzureAccount.AccountType.ServicePrincipal && !account.IsPropertySet(AzureAccount.Property.CertificatePassword)) + { + try + { + var secret = keystore.GetSecureString(new ServicePrincipalKey(AzureAccount.Property.CertificatePassword, account.Id, account.GetTenants().First())).ConvertToString(); + account.ExtendedProperties.SetProperty(AzureAccount.Property.CertificatePassword, secret); + } + catch + { + } + } + } + + } + return ret; + } /// /// Creates new instance of AzureRMProfile. @@ -258,6 +298,18 @@ public AzureRmProfile() } } + + /// + /// Creates new instance of AzureRMProfile with other EnvironmentTable.. + /// + public AzureRmProfile(IDictionary otherEnvironmentTable) + { + foreach (var environment in otherEnvironmentTable) + { + EnvironmentTable.Add(environment.Key, environment.Value.DeepCopy()); + } + } + /// /// Initializes a new instance of AzureRMProfile and loads its content from specified path. /// @@ -537,7 +589,6 @@ public bool TrySetContext(string name, IAzureContext context) Contexts[name] = context; result = true; } - return result; } @@ -689,6 +740,29 @@ public bool TryCopyProfile(AzureRmProfile other) return true; } + /// + /// Deep clone the instance of AzureRMProfile. + /// + public AzureRmProfile DeepCopy() + { + var profile = new AzureRmProfile(this.EnvironmentTable); + + foreach (var context in this.Contexts) + { + profile.Contexts.Add(context.Key, context.Value.DeepCopy()); + } + + if (this.DefaultContext != null) + { + profile.DefaultContext = this.DefaultContext.DeepCopy(); + } + profile.DefaultContextKey = this.DefaultContextKey; + profile.ProfilePath = this.ProfilePath; + profile.ShouldRefreshContextsFromCache = this.ShouldRefreshContextsFromCache; + profile.CopyPropertiesFrom(this); + return profile; + } + public AzureRmProfile ToProfile() { return this; diff --git a/src/Accounts/Authentication.ResourceManager/ContextModelExtensions.cs b/src/Accounts/Authentication.ResourceManager/ContextModelExtensions.cs index fee0ec08f27a..6f15327560c8 100644 --- a/src/Accounts/Authentication.ResourceManager/ContextModelExtensions.cs +++ b/src/Accounts/Authentication.ResourceManager/ContextModelExtensions.cs @@ -150,6 +150,11 @@ public static IAzureEnvironment Merge(this IAzureEnvironment environment1, IAzur return mergedEnvironment; } - + public static IAzureEnvironment DeepCopy(this IAzureEnvironment environment) + { + var copy = new AzureEnvironment(environment); + copy.Type = (environment as AzureEnvironment)?.Type ?? copy.Type; + return copy; + } } } diff --git a/tools/Common.Netcore.Dependencies.targets b/tools/Common.Netcore.Dependencies.targets index 2682944f68b8..14f42cf39811 100644 --- a/tools/Common.Netcore.Dependencies.targets +++ b/tools/Common.Netcore.Dependencies.targets @@ -3,22 +3,22 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + @@ -37,7 +37,7 @@ - $(NugetPackageRoot)\microsoft.azure.powershell.storage\1.3.83-preview\tools\ + $(NugetPackageRoot)\microsoft.azure.powershell.storage\1.3.84-preview\tools\