diff --git a/src/Common/Commands.ResourceManager.Common/Commands.ResourceManager.Common.csproj b/src/Common/Commands.ResourceManager.Common/Commands.ResourceManager.Common.csproj index ff1ad1950375..62da2d902735 100644 --- a/src/Common/Commands.ResourceManager.Common/Commands.ResourceManager.Common.csproj +++ b/src/Common/Commands.ResourceManager.Common/Commands.ResourceManager.Common.csproj @@ -166,10 +166,22 @@ + + True + True + Resources.resx + + Designer + + + ResXFileCodeGenerator + Resources.Designer.cs + + diff --git a/src/Common/Commands.ResourceManager.Common/Properties/Resources.Designer.cs b/src/Common/Commands.ResourceManager.Common/Properties/Resources.Designer.cs new file mode 100644 index 000000000000..826b427af001 --- /dev/null +++ b/src/Common/Commands.ResourceManager.Common/Properties/Resources.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.Azure.Commands.ResourceManager.Common.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Azure.Commands.ResourceManager.Common.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Tenant '{0}' was not found. Please verify that your account has access to this tenant.. + /// + internal static string TenantNotFound { + get { + return ResourceManager.GetString("TenantNotFound", resourceCulture); + } + } + } +} diff --git a/src/Common/Commands.ResourceManager.Common/Properties/Resources.resx b/src/Common/Commands.ResourceManager.Common/Properties/Resources.resx new file mode 100644 index 000000000000..7e29fac28996 --- /dev/null +++ b/src/Common/Commands.ResourceManager.Common/Properties/Resources.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Tenant '{0}' was not found. Please verify that your account has access to this tenant. + + \ No newline at end of file diff --git a/src/Common/Commands.ResourceManager.Common/RMProfileClient.cs b/src/Common/Commands.ResourceManager.Common/RMProfileClient.cs new file mode 100644 index 000000000000..cd3908bafca1 --- /dev/null +++ b/src/Common/Commands.ResourceManager.Common/RMProfileClient.cs @@ -0,0 +1,164 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Hyak.Common; +using Microsoft.Azure.Common.Authentication; +using Microsoft.Azure.Common.Authentication.Factories; +using Microsoft.Azure.Common.Authentication.Models; +using Microsoft.Azure.Subscriptions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; +using System.Security; + +namespace Microsoft.Azure.Commands.ResourceManager.Common +{ + public class RMProfileClient + { + private AzureRMProfile _profile; + public Action WarningLog; + + public RMProfileClient(AzureRMProfile profile) + { + _profile = profile; + } + + public AzureRMProfile Login(AzureAccount account, AzureEnvironment environment, string tenantId, string subscriptionId, SecureString password) + { + AzureSubscription newSubscription = null; + AzureTenant newTenant = new AzureTenant(); + + // (tenant and subscription are present) OR + // (tenant is present and subscription is not provided) + if (!string.IsNullOrEmpty(tenantId)) + { + newTenant.Id = new Guid(tenantId); + ShowDialog promptBehavior = password == null ? ShowDialog.Always : ShowDialog.Never; + TryGetTenantSubscription(account, environment, tenantId, subscriptionId, password, promptBehavior, out newSubscription); + } + // (tenant is not provided and subscription is present) OR + // (tenant is not provided and subscription is not provided) + else + { + foreach(var tenant in ListAccountTenants(account, environment, password)) + { + if (TryGetTenantSubscription(account, environment, tenant, subscriptionId, password, ShowDialog.Auto, out newSubscription)) + { + newTenant.Id = new Guid(tenant); + break; + } + } + + } + + if (newSubscription == null) + { + throw new PSInvalidOperationException("Subscription was not found."); + } + + _profile.DefaultContext = new AzureContext(newSubscription, account, environment, newTenant); + + return _profile; + } + + private bool TryGetTenantSubscription( + AzureAccount account, + AzureEnvironment environment, + string tenantId, + string subscriptionId, + SecureString password, + ShowDialog promptBehavior, + out AzureSubscription subscription) + { + var accessToken = AzureSession.AuthenticationFactory.Authenticate( + account, + environment, + tenantId, + password, + promptBehavior); + using (var subscriptionClient = AzureSession.ClientFactory.CreateCustomClient( + new TokenCloudCredentials(accessToken.AccessToken), + environment.GetEndpointAsUri(AzureEnvironment.Endpoint.ResourceManager))) + { + Subscriptions.Models.Subscription subscriptionFromServer = null; + + try + { + if (subscriptionId != null) + { + subscriptionFromServer = subscriptionClient.Subscriptions.Get(subscriptionId).Subscription; + } + else + { + var subscriptions = subscriptionClient.Subscriptions.List().Subscriptions; + if (subscriptions != null) + { + if (subscriptions.Count > 1) + { + WriteWarningMessage(string.Format( + "Tenant '{0}' contains more than one subscription. First one will be selected for further use.", + tenantId)); + } + subscriptionFromServer = subscriptions.First(); + } + } + } + catch (CloudException ex) + { + WriteWarningMessage(ex.Message); + } + + if (subscriptionFromServer != null) + { + subscription = new AzureSubscription + { + Id = new Guid(subscriptionFromServer.SubscriptionId), + Account = accessToken.UserId, + Environment = environment.Name, + Name = subscriptionFromServer.DisplayName, + Properties = new Dictionary { { AzureSubscription.Property.Tenants, accessToken.TenantId } } + }; + return true; + } + + subscription = null; + return false; + } + } + + private string[] ListAccountTenants(AzureAccount account, AzureEnvironment environment, SecureString password) + { + ShowDialog promptBehavior = password == null ? ShowDialog.Always : ShowDialog.Never; + + var commonTenantToken = AzureSession.AuthenticationFactory.Authenticate(account, environment, + AuthenticationFactory.CommonAdTenant, password, promptBehavior); + + using (var subscriptionClient = AzureSession.ClientFactory.CreateCustomClient( + new TokenCloudCredentials(commonTenantToken.AccessToken), + environment.GetEndpointAsUri(AzureEnvironment.Endpoint.ResourceManager))) + { + return subscriptionClient.Tenants.List().TenantIds.Select(ti => ti.TenantId).ToArray(); + } + } + + private void WriteWarningMessage(string message) + { + if (WarningLog != null) + { + WarningLog(message); + } + } + } +} diff --git a/src/Common/Commands.ResourceManager.Profile.Test/Commands.ResourceManager.Profile.Test.csproj b/src/Common/Commands.ResourceManager.Profile.Test/Commands.ResourceManager.Profile.Test.csproj index a2007b8519c7..b7b9311fa65c 100644 --- a/src/Common/Commands.ResourceManager.Profile.Test/Commands.ResourceManager.Profile.Test.csproj +++ b/src/Common/Commands.ResourceManager.Profile.Test/Commands.ResourceManager.Profile.Test.csproj @@ -178,6 +178,7 @@ + diff --git a/src/Common/Commands.ResourceManager.Profile.Test/LoginCmdletTests.cs b/src/Common/Commands.ResourceManager.Profile.Test/LoginCmdletTests.cs new file mode 100644 index 000000000000..a881481c0e88 --- /dev/null +++ b/src/Common/Commands.ResourceManager.Profile.Test/LoginCmdletTests.cs @@ -0,0 +1,128 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.WindowsAzure.Commands.Utilities.Common; +using Microsoft.Azure.Commands.Profile; +using Microsoft.Azure.Commands.ResourceManager.Common; +using Microsoft.Azure.Common.Authentication; +using Microsoft.Azure.Common.Authentication.Models; +using Microsoft.WindowsAzure.Commands.Common.Test.Mocks; +using Microsoft.WindowsAzure.Commands.ScenarioTest; +using System.Linq; +using Xunit; +using System; +using Microsoft.WindowsAzure.Commands.Test.Utilities.Common; +using Hyak.Common; +using System.Management.Automation; + +namespace Microsoft.Azure.Commands.Profile.Test +{ + public class LoginCmdletTests + { + private MemoryDataStore dataStore; + private MockCommandRuntime commandRuntimeMock; + + public LoginCmdletTests() + { + dataStore = new MemoryDataStore(); + AzureSession.DataStore = dataStore; + commandRuntimeMock = new MockCommandRuntime(); + AzureRMCmdlet.DefaultProfile = new AzureRMProfile(); + } + + [Fact] + [Trait(Category.AcceptanceType, Category.LiveOnly)] + public void LoginWithSubscriptionAndTenant() + { + var cmdlt = new LoginAzureRMAccount(); + // Setup + cmdlt.CommandRuntime = commandRuntimeMock; + cmdlt.SubscriptionId = "2c224e7e-3ef5-431d-a57b-e71f4662e3a6"; + cmdlt.Tenant = "72f988bf-86f1-41af-91ab-2d7cd011db47"; + + // Act + cmdlt.InvokeBeginProcessing(); + cmdlt.ExecuteCmdlet(); + cmdlt.InvokeEndProcessing(); + + Assert.NotNull(AzureRMCmdlet.DefaultProfile.DefaultContext); + } + + [Fact] + [Trait(Category.AcceptanceType, Category.LiveOnly)] + public void LoginWithInvalidSubscriptionAndTenantThrowsCloudException() + { + var cmdlt = new LoginAzureRMAccount(); + // Setup + cmdlt.CommandRuntime = commandRuntimeMock; + cmdlt.SubscriptionId = "2c224e7e-3ef5-431d-a57b-e71f4662e3a5"; + cmdlt.Tenant = "72f988bf-86f1-41af-91ab-2d7cd011db47"; + + // Act + cmdlt.InvokeBeginProcessing(); + Assert.Throws(() => cmdlt.ExecuteCmdlet()); + cmdlt.InvokeEndProcessing(); + } + + [Fact] + [Trait(Category.AcceptanceType, Category.LiveOnly)] + public void LoginWithSubscriptionAndNoTenant() + { + var cmdlt = new LoginAzureRMAccount(); + // Setup + cmdlt.CommandRuntime = commandRuntimeMock; + cmdlt.SubscriptionId = "2c224e7e-3ef5-431d-a57b-e71f4662e3a6"; + + // Act + cmdlt.InvokeBeginProcessing(); + cmdlt.ExecuteCmdlet(); + cmdlt.InvokeEndProcessing(); + + Assert.NotNull(AzureRMCmdlet.DefaultProfile.DefaultContext); + } + + [Fact] + [Trait(Category.AcceptanceType, Category.LiveOnly)] + public void LoginWithNoSubscriptionAndNoTenant() + { + var cmdlt = new LoginAzureRMAccount(); + // Setup + cmdlt.CommandRuntime = commandRuntimeMock; + + // Act + cmdlt.InvokeBeginProcessing(); + cmdlt.ExecuteCmdlet(); + cmdlt.InvokeEndProcessing(); + + Assert.NotNull(AzureRMCmdlet.DefaultProfile.DefaultContext); + } + + [Fact] + [Trait(Category.AcceptanceType, Category.LiveOnly)] + public void LoginWithNoSubscriptionAndTenant() + { + var cmdlt = new LoginAzureRMAccount(); + // Setup + cmdlt.CommandRuntime = commandRuntimeMock; + cmdlt.Tenant = "72f988bf-86f1-41af-91ab-2d7cd011db47"; + + // Act + cmdlt.InvokeBeginProcessing(); + cmdlt.ExecuteCmdlet(); + cmdlt.InvokeEndProcessing(); + + Assert.NotNull(AzureRMCmdlet.DefaultProfile.DefaultContext); + } + } +} diff --git a/src/Common/Commands.ResourceManager.Profile/Commands.ResourceManager.Profile.csproj b/src/Common/Commands.ResourceManager.Profile/Commands.ResourceManager.Profile.csproj index c3b20d9827a2..5ff4ba4ddab5 100644 --- a/src/Common/Commands.ResourceManager.Profile/Commands.ResourceManager.Profile.csproj +++ b/src/Common/Commands.ResourceManager.Profile/Commands.ResourceManager.Profile.csproj @@ -132,6 +132,7 @@ + diff --git a/src/Common/Commands.ResourceManager.Profile/LoginAzureRMAccount.cs b/src/Common/Commands.ResourceManager.Profile/LoginAzureRMAccount.cs new file mode 100644 index 000000000000..3d8f5d85bf7f --- /dev/null +++ b/src/Common/Commands.ResourceManager.Profile/LoginAzureRMAccount.cs @@ -0,0 +1,117 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.ResourceManager.Common; +using System.Management.Automation; +using Microsoft.Azure.Common.Authentication.Models; +using System.Security; +using Microsoft.WindowsAzure.Commands.Utilities.Common; + +namespace Microsoft.Azure.Commands.Profile +{ + /// + /// Cmdlet to log into an environment and download the subscriptions + /// + [Cmdlet("Login", "AzureRMAccount", DefaultParameterSetName = "User")] + [OutputType(typeof(AzureRMProfile))] + public class LoginAzureRMAccount : AzurePSCmdlet + { + [Parameter(Mandatory = false, HelpMessage = "Environment containing the account to log into")] + [ValidateNotNullOrEmpty] + public AzureEnvironment Environment { get; set; } + + [Parameter(ParameterSetName = "User", Mandatory = false, HelpMessage = "Optional credential")] + [Parameter(ParameterSetName = "ServicePrincipal", Mandatory = true, HelpMessage = "Credential")] + public PSCredential Credential { get; set; } + + [Parameter(ParameterSetName = "ServicePrincipal", Mandatory = true)] + public SwitchParameter ServicePrincipal { get; set; } + + [Parameter(ParameterSetName = "User", Mandatory = false, HelpMessage = "Optional tenant name or ID")] + [Parameter(ParameterSetName = "ServicePrincipal", Mandatory = true, HelpMessage = "Tenant name or ID")] + [Parameter(ParameterSetName = "AccessToken", Mandatory = false, HelpMessage = "Tenant name or ID")] + [ValidateNotNullOrEmpty] + public string Tenant { get; set; } + + [Parameter(ParameterSetName = "AccessToken", Mandatory = true, HelpMessage = "AccessToken")] + [ValidateNotNullOrEmpty] + public string AccessToken { get; set; } + + [Parameter(Mandatory = false, HelpMessage = "Subscription")] + [ValidateNotNullOrEmpty] + public string SubscriptionId { get; set; } + + protected override AzureContext DefaultContext + { + get + { + return null; + } + } + + public LoginAzureRMAccount() + : base() + { + } + + protected override void BeginProcessing() + { + base.BeginProcessing(); + if (Environment == null) + { + Environment = AzureEnvironment.PublicEnvironments[EnvironmentName.AzureCloud]; + } + } + + protected override void ProcessRecord() + { + AzureAccount azureAccount = new AzureAccount(); + + if (!string.IsNullOrEmpty(AccessToken)) + { + azureAccount.Type = AzureAccount.AccountType.AccessToken; + } + else if (ServicePrincipal.IsPresent) + { + azureAccount.Type = AzureAccount.AccountType.ServicePrincipal; + } + else + { + azureAccount.Type = AzureAccount.AccountType.User; + + } + + SecureString password = null; + if (Credential != null) + { + azureAccount.Id = Credential.UserName; + password = Credential.Password; + } + + if (!string.IsNullOrEmpty(Tenant)) + { + azureAccount.SetProperty(AzureAccount.Property.Tenants, new[] { Tenant }); + } + + if( AzureRMCmdlet.DefaultProfile == null) + { + AzureRMCmdlet.DefaultProfile = new AzureRMProfile(); + } + + var profileClient = new RMProfileClient(AzureRMCmdlet.DefaultProfile); + + WriteObject(profileClient.Login(azureAccount, Environment, Tenant, SubscriptionId, password)); + } + } +} diff --git a/src/ResourceManager.sln b/src/ResourceManager.sln index b793d8fdf990..eae93fd6ef08 100644 --- a/src/ResourceManager.sln +++ b/src/ResourceManager.sln @@ -121,8 +121,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Commands.Websites.Test", "R EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Commands.Common", "Common\Commands.Common\Commands.Common.csproj", "{5EE72C53-1720-4309-B54B-5FB79703195F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Commands.Profile", "Common\Commands.Profile\Commands.Profile.csproj", "{C60342B1-47D3-4A0E-8081-9B97CE60B7AF}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -349,10 +347,6 @@ Global {5EE72C53-1720-4309-B54B-5FB79703195F}.Debug|Any CPU.Build.0 = Debug|Any CPU {5EE72C53-1720-4309-B54B-5FB79703195F}.Release|Any CPU.ActiveCfg = Release|Any CPU {5EE72C53-1720-4309-B54B-5FB79703195F}.Release|Any CPU.Build.0 = Release|Any CPU - {C60342B1-47D3-4A0E-8081-9B97CE60B7AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C60342B1-47D3-4A0E-8081-9B97CE60B7AF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C60342B1-47D3-4A0E-8081-9B97CE60B7AF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C60342B1-47D3-4A0E-8081-9B97CE60B7AF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -382,6 +376,5 @@ Global {152D78F0-A642-4D0E-B3A8-2FC64FFA9714} = {95C16AED-FD57-42A0-86C3-2CF4300A4817} {F220C306-29A3-4511-8518-A58A55C60D07} = {95C16AED-FD57-42A0-86C3-2CF4300A4817} {13E031E4-8A43-4B87-9D72-D70180C31C11} = {95C16AED-FD57-42A0-86C3-2CF4300A4817} - {C60342B1-47D3-4A0E-8081-9B97CE60B7AF} = {95C16AED-FD57-42A0-86C3-2CF4300A4817} EndGlobalSection EndGlobal