Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 63 additions & 8 deletions src/Accounts/Accounts.Test/ProfileCmdletTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -36,6 +44,7 @@ public class ProfileCmdletTests : RMTestBase
{
private MemoryDataStore dataStore;
private MockCommandRuntime commandRuntimeMock;
private List<byte> azKeyStoreData = new List<byte>();
private AzKeyStore keyStore;

public ProfileCmdletTests(ITestOutputHelper output)
Expand All @@ -53,8 +62,8 @@ private AzKeyStore SetMockedAzKeyStore()
{
var storageMocker = new Mock<IStorage>();
storageMocker.Setup(f => f.Create()).Returns(storageMocker.Object);
storageMocker.Setup(f => f.ReadData()).Returns(new byte[0]);
storageMocker.Setup(f => f.WriteData(It.IsAny<byte[]>())).Callback((byte[] s) => { });
storageMocker.Setup(f => f.WriteData(It.IsAny<byte[]>())).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;
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -166,19 +175,26 @@ public void SaveAzureProfileNull()
Assert.Throws<ArgumentException>(() => 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();
Expand All @@ -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<AzureRmProfile>(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;
}
}
}
1 change: 1 addition & 0 deletions src/Accounts/Accounts/ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
20 changes: 18 additions & 2 deletions src/Accounts/Accounts/Context/SaveAzureRMContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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);
Expand All @@ -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));
}
}
Expand All @@ -76,7 +86,13 @@ public override void ExecuteCmdlet()
ShouldContinue(string.Format(Resources.FileOverwriteMessage, Path),
Resources.FileOverwriteCaption))
{
AzureRmProfileProvider.Instance.GetProfile<AzureRmProfile>().Save(Path);
var profile = AzureRmProfileProvider.Instance.GetProfile<AzureRmProfile>();
if (WithCredential.IsPresent)
{
WriteWarning(string.Format(Resources.ProfileCredentialsWriteWarning, Path));
profile = profile.RefillCredentialsFromKeyStore();
}
profile.Save(Path);
WriteVerbose(string.Format(Resources.ProfileCurrentSaved, Path));
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/Accounts/Accounts/Properties/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Accounts/Accounts/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -592,4 +592,7 @@
<data name="TenantDomainToTenantIdMessage" xml:space="preserve">
<value>The input domain is {0} and the tenant Id is {1}</value>
</data>
<data name="ProfileCredentialsWriteWarning" xml:space="preserve">
<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>
</data>
</root>
80 changes: 77 additions & 3 deletions src/Accounts/Authentication.ResourceManager/AzureRmProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -243,6 +243,46 @@ private void LoadImpl(string contents)
{
}

/// <summary>
/// Refill the credentials from AzKeyStore to profile. Used for profile export.
/// </summary>
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;
}

/// <summary>
/// Creates new instance of AzureRMProfile.
Expand All @@ -258,6 +298,18 @@ public AzureRmProfile()
}
}


/// <summary>
/// Creates new instance of AzureRMProfile with other EnvironmentTable..
/// </summary>
public AzureRmProfile(IDictionary<string, IAzureEnvironment> otherEnvironmentTable)
{
foreach (var environment in otherEnvironmentTable)
{
EnvironmentTable.Add(environment.Key, environment.Value.DeepCopy());
}
}

/// <summary>
/// Initializes a new instance of AzureRMProfile and loads its content from specified path.
/// </summary>
Expand Down Expand Up @@ -537,7 +589,6 @@ public bool TrySetContext(string name, IAzureContext context)
Contexts[name] = context;
result = true;
}

return result;
}

Expand Down Expand Up @@ -689,6 +740,29 @@ public bool TryCopyProfile(AzureRmProfile other)
return true;
}

/// <summary>
/// Deep clone the instance of AzureRMProfile.
/// </summary>
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
Loading