Skip to content

Commit b595765

Browse files
authored
Refill Credentials from AzKeyStore When Save AzContext (#22440)
* Refill Credentials from AzKeyStore When Save AzContext fix #22355 * Address review comments * Address review comments
1 parent 6148dba commit b595765

File tree

8 files changed

+194
-31
lines changed

8 files changed

+194
-31
lines changed

src/Accounts/Accounts.Test/ProfileCmdletTests.cs

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,22 @@
1919
using Microsoft.Azure.Commands.ScenarioTest;
2020
using Microsoft.Azure.Commands.TestFx.Mocks;
2121
using Microsoft.Azure.ServiceManagement.Common.Models;
22+
using Microsoft.WindowsAzure.Commands.Common;
2223
using Microsoft.WindowsAzure.Commands.Common.Test.Mocks;
2324
using Microsoft.WindowsAzure.Commands.ScenarioTest;
2425
using Microsoft.WindowsAzure.Commands.Test.Utilities.Common;
2526
using Microsoft.WindowsAzure.Commands.Utilities.Common;
27+
2628
using Moq;
29+
30+
using Newtonsoft.Json;
31+
2732
using System;
33+
using System.Collections.Generic;
2834
using System.Linq;
2935
using System.Management.Automation;
36+
using System.Text.RegularExpressions;
37+
3038
using Xunit;
3139
using Xunit.Abstractions;
3240

@@ -36,6 +44,7 @@ public class ProfileCmdletTests : RMTestBase
3644
{
3745
private MemoryDataStore dataStore;
3846
private MockCommandRuntime commandRuntimeMock;
47+
private List<byte> azKeyStoreData = new List<byte>();
3948
private AzKeyStore keyStore;
4049

4150
public ProfileCmdletTests(ITestOutputHelper output)
@@ -53,8 +62,8 @@ private AzKeyStore SetMockedAzKeyStore()
5362
{
5463
var storageMocker = new Mock<IStorage>();
5564
storageMocker.Setup(f => f.Create()).Returns(storageMocker.Object);
56-
storageMocker.Setup(f => f.ReadData()).Returns(new byte[0]);
57-
storageMocker.Setup(f => f.WriteData(It.IsAny<byte[]>())).Callback((byte[] s) => { });
65+
storageMocker.Setup(f => f.WriteData(It.IsAny<byte[]>())).Callback((byte[] s) => { azKeyStoreData.Clear(); azKeyStoreData.AddRange(s); });
66+
storageMocker.Setup(f => f.ReadData()).Returns(azKeyStoreData.ToArray());
5867
var keyStore = new AzKeyStore(AzureSession.Instance.ARMProfileDirectory, "azkeystore", false, storageMocker.Object);
5968
return keyStore;
6069
}
@@ -107,7 +116,7 @@ public void SelectAzureProfileFromDisk()
107116
{
108117
AzureSession.Instance.RegisterComponent(AzKeyStore.Name, () => keyStore, true);
109118
var profile = new AzureRmProfile();
110-
profile.EnvironmentTable.Add("foo", new AzureEnvironment(new AzureEnvironment( AzureEnvironment.PublicEnvironments.Values.FirstOrDefault())));
119+
profile.EnvironmentTable.Add("foo", new AzureEnvironment(new AzureEnvironment(AzureEnvironment.PublicEnvironments.Values.FirstOrDefault())));
111120
profile.EnvironmentTable["foo"].Name = "foo";
112121
profile.Save("X:\\foo.json");
113122
ImportAzureRMContextCommand cmdlt = new ImportAzureRMContextCommand();
@@ -166,19 +175,26 @@ public void SaveAzureProfileNull()
166175
Assert.Throws<ArgumentException>(() => cmdlt.ExecuteCmdlet());
167176
}
168177

178+
private const string password = "pa88w0rd!";
179+
169180
[Fact]
170181
[Trait(Category.AcceptanceType, Category.CheckIn)]
171182
public void SaveAzureProfileFromDefault()
172183
{
184+
string accountId = Guid.NewGuid().ToString(),
185+
tenantId = Guid.NewGuid().ToString(),
186+
subscriptionId = Guid.NewGuid().ToString(),
187+
subscriptionName = "Contoso Subscription",
188+
tenantName = "contoso.com";
189+
173190
AzureSession.Instance.RegisterComponent(AzKeyStore.Name, () => keyStore, true);
174-
var profile = new AzureRmProfile();
175-
profile.EnvironmentTable.Add("foo", new AzureEnvironment(AzureEnvironment.PublicEnvironments.Values.FirstOrDefault()));
176-
profile.DefaultContext = new AzureContext(new AzureSubscription(), new AzureAccount(), profile.EnvironmentTable["foo"]);
191+
var profile = GetProfile(accountId, tenantId, subscriptionId, subscriptionName, tenantName);
177192
AzureRmProfileProvider.Instance.Profile = profile;
178193
SaveAzureRMContextCommand cmdlt = new SaveAzureRMContextCommand();
179194
// Setup
180195
cmdlt.Path = "X:\\foo.json";
181196
cmdlt.CommandRuntime = commandRuntimeMock;
197+
cmdlt.WithCredential = true;
182198

183199
// Act
184200
cmdlt.InvokeBeginProcessing();
@@ -187,8 +203,47 @@ public void SaveAzureProfileFromDefault()
187203

188204
// Verify
189205
Assert.True(AzureSession.Instance.DataStore.FileExists("X:\\foo.json"));
190-
var profile2 = new AzureRmProfile("X:\\foo.json");
191-
Assert.True(profile2.EnvironmentTable.ContainsKey("foo"));
206+
var profileString = AzureSession.Instance.DataStore.ReadFileAsText("X:\\foo.json");
207+
profileString = Regex.Replace(profileString, @"[^\u0000-\u007F]+", string.Empty);
208+
var actual = JsonConvert.DeserializeObject<AzureRmProfile>(profileString, new AzureRmProfileConverter());
209+
Assert.Equal(password, actual.Contexts.First().Value.Account.GetProperty(AzureAccount.Property.ServicePrincipalSecret));
210+
Assert.Equal(accountId, actual.Contexts.First().Value.Account.Id);
211+
Assert.Equal(tenantId, actual.Contexts.First().Value.Tenant.Id);
212+
Assert.Equal(subscriptionId, actual.Contexts.First().Value.Subscription.Id);
213+
Assert.Equal(subscriptionName, actual.Contexts.First().Value.Subscription.Name);
214+
}
215+
216+
private AzureRmProfile GetProfile(string accountId, string tenantId, string subscriptionId, string subscriptionName, string tenantName)
217+
{
218+
var account = new AzureAccount()
219+
{
220+
Id = accountId,
221+
Type = AzureAccount.AccountType.ServicePrincipal
222+
};
223+
var tenant = new AzureTenant()
224+
{
225+
Directory = tenantName,
226+
Id = tenantId
227+
};
228+
account.SetTenants(tenant.Id);
229+
230+
keyStore.SaveSecureString(new ServicePrincipalKey(AzureAccount.Property.ServicePrincipalSecret, account.Id, tenant.Id), password.ConvertToSecureString());
231+
232+
var sub = new AzureSubscription()
233+
{
234+
Id = subscriptionId,
235+
Name = subscriptionName
236+
};
237+
238+
sub.SetAccount(account.Id);
239+
sub.SetEnvironment(EnvironmentName.AzureCloud);
240+
var context = new AzureContext(sub,
241+
account,
242+
AzureEnvironment.PublicEnvironments[EnvironmentName.AzureCloud],
243+
tenant);
244+
var profile = new AzureRmProfile();
245+
profile.TryAddContext(context, out _);
246+
return profile;
192247
}
193248
}
194249
}

src/Accounts/Accounts/ChangeLog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
-->
2020

2121
## Upcoming Release
22+
* Refilled credentials from `AzKeyStore` when run `Save-AzContext` [#22355]
2223
* Added config `DisableErrorRecordsPersistence` to disable writing error records to file system [#21732]
2324
* Updated Azure.Core to 1.34.0.
2425

src/Accounts/Accounts/Context/SaveAzureRMContext.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
using System.Management.Automation;
2626
using Microsoft.Azure.Commands.Common.Authentication;
2727
using Microsoft.Azure.Commands.Common.Authentication.Abstractions;
28+
using Microsoft.Azure.Commands.Common;
2829

2930
namespace Microsoft.Azure.Commands.Profile
3031
{
@@ -44,6 +45,9 @@ public class SaveAzureRMContextCommand : AzureRMCmdlet
4445
[Parameter(Mandatory=false, HelpMessage="Overwrite the given file if it exists")]
4546
public SwitchParameter Force { get; set; }
4647

48+
[Parameter(Mandatory = false, HelpMessage = "Export the credentials to the file")]
49+
public SwitchParameter WithCredential { get; set; }
50+
4751
public override void ExecuteCmdlet()
4852
{
4953
Path = this.ResolveUserPath(Path);
@@ -57,7 +61,13 @@ public override void ExecuteCmdlet()
5761
ShouldContinue(string.Format(Resources.FileOverwriteMessage, Path),
5862
Resources.FileOverwriteCaption))
5963
{
60-
Profile.Save(Path);
64+
var profile = Profile;
65+
if (WithCredential.IsPresent)
66+
{
67+
WriteWarning(string.Format(Resources.ProfileCredentialsWriteWarning, Path));
68+
profile = profile.RefillCredentialsFromKeyStore();
69+
}
70+
profile.Save(Path);
6171
WriteVerbose(string.Format(Resources.ProfileArgumentSaved, Path));
6272
}
6373
}
@@ -76,7 +86,13 @@ public override void ExecuteCmdlet()
7686
ShouldContinue(string.Format(Resources.FileOverwriteMessage, Path),
7787
Resources.FileOverwriteCaption))
7888
{
79-
AzureRmProfileProvider.Instance.GetProfile<AzureRmProfile>().Save(Path);
89+
var profile = AzureRmProfileProvider.Instance.GetProfile<AzureRmProfile>();
90+
if (WithCredential.IsPresent)
91+
{
92+
WriteWarning(string.Format(Resources.ProfileCredentialsWriteWarning, Path));
93+
profile = profile.RefillCredentialsFromKeyStore();
94+
}
95+
profile.Save(Path);
8096
WriteVerbose(string.Format(Resources.ProfileCurrentSaved, Path));
8197
}
8298
}

src/Accounts/Accounts/Properties/Resources.Designer.cs

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Accounts/Accounts/Properties/Resources.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,4 +592,7 @@
592592
<data name="TenantDomainToTenantIdMessage" xml:space="preserve">
593593
<value>The input domain is {0} and the tenant Id is {1}</value>
594594
</data>
595+
<data name="ProfileCredentialsWriteWarning" xml:space="preserve">
596+
<value>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.</value>
597+
</data>
595598
</root>

src/Accounts/Authentication.ResourceManager/AzureRmProfile.cs

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,13 +223,13 @@ private IAzureContext MigrateSecretToKeyStore(IAzureContext context, AzKeyStore
223223
if (keystore != null)
224224
{
225225
var account = context.Account;
226-
if (account.IsPropertySet(AzureAccount.Property.ServicePrincipalSecret))
226+
if (account != null && account.IsPropertySet(AzureAccount.Property.ServicePrincipalSecret))
227227
{
228228
keystore?.SaveSecureString(new ServicePrincipalKey(AzureAccount.Property.ServicePrincipalSecret, account.Id, account.GetTenants().First())
229229
, account.ExtendedProperties.GetProperty(AzureAccount.Property.ServicePrincipalSecret).ConvertToSecureString());
230230
account.ExtendedProperties.Remove(AzureAccount.Property.ServicePrincipalSecret);
231231
}
232-
if (account.IsPropertySet(AzureAccount.Property.CertificatePassword))
232+
if (account != null && account.IsPropertySet(AzureAccount.Property.CertificatePassword))
233233
{
234234
keystore?.SaveSecureString(new ServicePrincipalKey(AzureAccount.Property.CertificatePassword, account.Id, account.GetTenants().First())
235235
, account.ExtendedProperties.GetProperty(AzureAccount.Property.CertificatePassword).ConvertToSecureString());
@@ -243,6 +243,46 @@ private void LoadImpl(string contents)
243243
{
244244
}
245245

246+
/// <summary>
247+
/// Refill the credentials from AzKeyStore to profile. Used for profile export.
248+
/// </summary>
249+
public AzureRmProfile RefillCredentialsFromKeyStore()
250+
{
251+
AzKeyStore keystore = null;
252+
AzureSession.Instance.TryGetComponent(AzKeyStore.Name, out keystore);
253+
AzureRmProfile ret = this.DeepCopy();
254+
if (keystore != null)
255+
{
256+
foreach (var context in ret.Contexts)
257+
{
258+
var account = context.Value.Account;
259+
if (account?.Type == AzureAccount.AccountType.ServicePrincipal && !account.IsPropertySet(AzureAccount.Property.ServicePrincipalSecret))
260+
{
261+
try
262+
{
263+
var secret = keystore.GetSecureString(new ServicePrincipalKey(AzureAccount.Property.ServicePrincipalSecret, account.Id, account.GetTenants().First())).ConvertToString();
264+
account.ExtendedProperties.SetProperty(AzureAccount.Property.ServicePrincipalSecret, secret);
265+
}
266+
catch
267+
{
268+
}
269+
}
270+
if (account?.Type == AzureAccount.AccountType.ServicePrincipal && !account.IsPropertySet(AzureAccount.Property.CertificatePassword))
271+
{
272+
try
273+
{
274+
var secret = keystore.GetSecureString(new ServicePrincipalKey(AzureAccount.Property.CertificatePassword, account.Id, account.GetTenants().First())).ConvertToString();
275+
account.ExtendedProperties.SetProperty(AzureAccount.Property.CertificatePassword, secret);
276+
}
277+
catch
278+
{
279+
}
280+
}
281+
}
282+
283+
}
284+
return ret;
285+
}
246286

247287
/// <summary>
248288
/// Creates new instance of AzureRMProfile.
@@ -258,6 +298,18 @@ public AzureRmProfile()
258298
}
259299
}
260300

301+
302+
/// <summary>
303+
/// Creates new instance of AzureRMProfile with other EnvironmentTable..
304+
/// </summary>
305+
public AzureRmProfile(IDictionary<string, IAzureEnvironment> otherEnvironmentTable)
306+
{
307+
foreach (var environment in otherEnvironmentTable)
308+
{
309+
EnvironmentTable.Add(environment.Key, environment.Value.DeepCopy());
310+
}
311+
}
312+
261313
/// <summary>
262314
/// Initializes a new instance of AzureRMProfile and loads its content from specified path.
263315
/// </summary>
@@ -537,7 +589,6 @@ public bool TrySetContext(string name, IAzureContext context)
537589
Contexts[name] = context;
538590
result = true;
539591
}
540-
541592
return result;
542593
}
543594

@@ -689,6 +740,29 @@ public bool TryCopyProfile(AzureRmProfile other)
689740
return true;
690741
}
691742

743+
/// <summary>
744+
/// Deep clone the instance of AzureRMProfile.
745+
/// </summary>
746+
public AzureRmProfile DeepCopy()
747+
{
748+
var profile = new AzureRmProfile(this.EnvironmentTable);
749+
750+
foreach (var context in this.Contexts)
751+
{
752+
profile.Contexts.Add(context.Key, context.Value.DeepCopy());
753+
}
754+
755+
if (this.DefaultContext != null)
756+
{
757+
profile.DefaultContext = this.DefaultContext.DeepCopy();
758+
}
759+
profile.DefaultContextKey = this.DefaultContextKey;
760+
profile.ProfilePath = this.ProfilePath;
761+
profile.ShouldRefreshContextsFromCache = this.ShouldRefreshContextsFromCache;
762+
profile.CopyPropertiesFrom(this);
763+
return profile;
764+
}
765+
692766
public AzureRmProfile ToProfile()
693767
{
694768
return this;

src/Accounts/Authentication.ResourceManager/ContextModelExtensions.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,11 @@ public static IAzureEnvironment Merge(this IAzureEnvironment environment1, IAzur
150150
return mergedEnvironment;
151151
}
152152

153-
153+
public static IAzureEnvironment DeepCopy(this IAzureEnvironment environment)
154+
{
155+
var copy = new AzureEnvironment(environment);
156+
copy.Type = (environment as AzureEnvironment)?.Type ?? copy.Type;
157+
return copy;
158+
}
154159
}
155160
}

0 commit comments

Comments
 (0)