diff --git a/src/azure-cli-core/azure/cli/core/_profile.py b/src/azure-cli-core/azure/cli/core/_profile.py index e0d45196a51..9788066e48a 100644 --- a/src/azure-cli-core/azure/cli/core/_profile.py +++ b/src/azure-cli-core/azure/cli/core/_profile.py @@ -106,6 +106,7 @@ def find_subscriptions_on_login(self, # pylint: disable=too-many-arguments password, is_service_principal, tenant, + allow_no_subscriptions=False, subscription_finder=None): from azure.cli.core._debug import allow_debug_adal_connection allow_debug_adal_connection() @@ -128,18 +129,26 @@ def find_subscriptions_on_login(self, # pylint: disable=too-many-arguments subscriptions = subscription_finder.find_from_user_account( username, password, tenant, self._ad_resource_uri) - if not subscriptions: - raise CLIError('No subscriptions found for this account.') + if not allow_no_subscriptions and not subscriptions: + raise CLIError("No subscriptions were found for '{}'. If this is expected, use " + "'--allow-no-subscriptions' to have tenant level accesses".format( + username)) if is_service_principal: self._creds_cache.save_service_principal_cred(sp_auth.get_entry_to_persist(username, tenant)) - if self._creds_cache.adal_token_cache.has_state_changed: self._creds_cache.persist_cached_creds() + + if allow_no_subscriptions: + t_list = [s.tenant_id for s in subscriptions] + bare_tenants = [t for t in subscription_finder.tenants if t not in t_list] + subscriptions = Profile._build_tenant_level_accounts(bare_tenants) + consolidated = Profile._normalize_properties(subscription_finder.user_id, subscriptions, is_service_principal) + self._set_subscriptions(consolidated) # use deepcopy as we don't want to persist these changes to file. return deepcopy(consolidated) @@ -162,6 +171,24 @@ def _normalize_properties(user, subscriptions, is_service_principal): }) return consolidated + @staticmethod + def _build_tenant_level_accounts(tenants): + from azure.cli.core.profiles import get_sdk, ResourceType + SubscriptionType = get_sdk(ResourceType.MGMT_RESOURCE_SUBSCRIPTIONS, + 'Subscription', mod='models') + StateType = get_sdk(ResourceType.MGMT_RESOURCE_SUBSCRIPTIONS, + 'SubscriptionState', mod='models') + result = [] + for t in tenants: + s = SubscriptionType() + s.id = '/subscriptions/' + t + s.subscription = t + s.tenant_id = t + s.display_name = 'N/A(tenant level account)' + s.state = StateType.enabled + result.append(s) + return result + def _set_subscriptions(self, new_subscriptions): existing_ones = self.load_cached_subscriptions(all_clouds=True) active_one = next((x for x in existing_ones if x.get(_IS_DEFAULT_SUBSCRIPTION)), None) @@ -345,6 +372,7 @@ def create_arm_client_factory(config): config, base_url=CLOUD.endpoints.resource_manager)) self._arm_client_factory = create_arm_client_factory + self.tenants = [] def find_from_user_account(self, username, password, tenant, resource): context = self._create_auth_context(tenant or _COMMON_TENANT) @@ -379,6 +407,7 @@ def find_from_service_principal_id(self, client_id, sp_auth, tenant, resource): token_entry = sp_auth.acquire_token(context, resource, client_id) self.user_id = client_id result = self._find_using_specific_tenant(tenant, token_entry[_ACCESS_TOKEN]) + self.tenants = [tenant] return result def _create_auth_context(self, tenant, use_token_cache=True): @@ -410,6 +439,7 @@ def _find_using_common_tenant(self, access_token, resource): temp_credentials[_ACCESS_TOKEN]) all_subscriptions.extend(subscriptions) + self.tenants = tenants return all_subscriptions def _find_using_specific_tenant(self, tenant, access_token): @@ -422,6 +452,7 @@ def _find_using_specific_tenant(self, tenant, access_token): for s in subscriptions: setattr(s, 'tenant_id', tenant) all_subscriptions.append(s) + self.tenants = [tenant] return all_subscriptions diff --git a/src/azure-cli-core/tests/test_profile.py b/src/azure-cli-core/tests/test_profile.py index e344826e285..7c092e55f31 100644 --- a/src/azure-cli-core/tests/test_profile.py +++ b/src/azure-cli-core/tests/test_profile.py @@ -243,6 +243,7 @@ def test_get_expanded_subscription_info_for_logged_in_service_principal(self, 'my-secret', True, self.tenant_id, + False, finder) # action extended_info = profile.get_expanded_subscription_info() @@ -253,6 +254,35 @@ def test_get_expanded_subscription_info_for_logged_in_service_principal(self, self.assertEqual('https://login.microsoftonline.com', extended_info['endpoints'].active_directory) + @mock.patch('adal.AuthenticationContext', autospec=True) + def test_create_account_without_subscriptions(self, mock_auth_context): + mock_auth_context.acquire_token_with_client_credentials.return_value = self.token_entry1 + mock_arm_client = mock.MagicMock() + mock_arm_client.subscriptions.list.return_value = [] + finder = SubscriptionFinder(lambda _, _2: mock_auth_context, + None, + lambda _: mock_arm_client) + + storage_mock = {'subscriptions': []} + profile = Profile(storage_mock) + profile._management_resource_uri = 'https://management.core.windows.net/' + + # action + result = profile.find_subscriptions_on_login(False, + '1234', + 'my-secret', + True, + self.tenant_id, + allow_no_subscriptions=True, + subscription_finder=finder) + + # assert + self.assertTrue(1, len(result)) + self.assertEqual(result[0]['id'], self.tenant_id) + self.assertEqual(result[0]['state'], 'Enabled') + self.assertEqual(result[0]['tenantId'], self.tenant_id) + self.assertEqual(result[0]['name'], 'N/A(tenant level account)') + @mock.patch('azure.cli.core._profile._load_tokens_from_file', autospec=True) def test_get_current_account_user(self, mock_read_cred_file): # setup diff --git a/src/command_modules/azure-cli-profile/HISTORY.rst b/src/command_modules/azure-cli-profile/HISTORY.rst index 3392ef64a97..5b8cee30e3e 100644 --- a/src/command_modules/azure-cli-profile/HISTORY.rst +++ b/src/command_modules/azure-cli-profile/HISTORY.rst @@ -2,6 +2,9 @@ Release History =============== +2.0.4 (unreleased) +++++++++++++++++++ +* Support login when there are no subscriptions found (#2560) 2.0.3 (2017-04-17) ++++++++++++++++++ diff --git a/src/command_modules/azure-cli-profile/azure/cli/command_modules/profile/_params.py b/src/command_modules/azure-cli-profile/azure/cli/command_modules/profile/_params.py index 494a62dbb1a..2b552e0d6c2 100644 --- a/src/command_modules/azure-cli-profile/azure/cli/command_modules/profile/_params.py +++ b/src/command_modules/azure-cli-profile/azure/cli/command_modules/profile/_params.py @@ -21,6 +21,7 @@ def get_subscription_id_list(prefix, **kwargs): # pylint: disable=unused-argume register_cli_argument('login', 'service_principal', action='store_true', help='The credential representing a service principal.') register_cli_argument('login', 'username', options_list=('--username', '-u'), help='Organization id or service principal') register_cli_argument('login', 'tenant', options_list=('--tenant', '-t'), help='The AAD tenant, must provide when using service principals.') +register_cli_argument('login', 'allow_no_subscriptions', action='store_true', help="Support access tenants without subscriptions. It's uncommon but useful to run tenant level commands, such as 'az ad'") register_cli_argument('logout', 'username', help='account user, if missing, logout the current active account') diff --git a/src/command_modules/azure-cli-profile/azure/cli/command_modules/profile/custom.py b/src/command_modules/azure-cli-profile/azure/cli/command_modules/profile/custom.py index f163f8122f7..089fdc1fed8 100644 --- a/src/command_modules/azure-cli-profile/azure/cli/command_modules/profile/custom.py +++ b/src/command_modules/azure-cli-profile/azure/cli/command_modules/profile/custom.py @@ -54,7 +54,8 @@ def account_clear(): profile.logout_all() -def login(username=None, password=None, service_principal=None, tenant=None): +def login(username=None, password=None, service_principal=None, tenant=None, + allow_no_subscriptions=False): """Log in to access Azure subscriptions""" from adal.adal_error import AdalError import requests @@ -76,7 +77,8 @@ def login(username=None, password=None, service_principal=None, tenant=None): username, password, service_principal, - tenant) + tenant, + allow_no_subscriptions=allow_no_subscriptions) except AdalError as err: # try polish unfriendly server errors if username: