diff --git a/azure-cli.pyproj b/azure-cli.pyproj index c58774488..f6e2c9069 100644 --- a/azure-cli.pyproj +++ b/azure-cli.pyproj @@ -12,7 +12,7 @@ . {888888a0-9f3d-457c-b088-3a5042f75d52} Standard Python launcher - {1dd9c42b-5980-42ce-a2c3-46d3bf0eede4} + {54f4b6dc-0859-46dc-99bb-b275c9d0aca3} 3.5 False @@ -36,6 +36,10 @@ + + + Code + @@ -178,6 +182,19 @@ + + + {54f4b6dc-0859-46dc-99bb-b275c9d0aca3} + {2af0f10d-7135-4994-9156-5d01c9c11b7e} + 3.5 + env (Python 3.5) + Scripts\python.exe + Scripts\pythonw.exe + Lib\ + PYTHONPATH + X86 + + \ No newline at end of file diff --git a/setup.py b/setup.py index d14b04370..72951f40a 100644 --- a/setup.py +++ b/setup.py @@ -21,13 +21,14 @@ from codecs import open from setuptools import setup VERSION = '0.0.32' -INSTALL_FROM_PUBLIC = False PRIVATE_PYPI_URL_ENV_NAME = 'AZURE_CLI_PRIVATE_PYPI_URL' PRIVATE_PYPI_URL = os.environ.get(PRIVATE_PYPI_URL_ENV_NAME) PRIVATE_PYPI_HOST_ENV_NAME = 'AZURE_CLI_PRIVATE_PYPI_HOST' PRIVATE_PYPI_HOST = os.environ.get(PRIVATE_PYPI_HOST_ENV_NAME) +INSTALL_FROM_PRIVATE = bool(PRIVATE_PYPI_URL and PRIVATE_PYPI_HOST) + # If we have source, validate that our version numbers match # This should prevent uploading releases with mismatched versions. try: @@ -83,10 +84,7 @@ def _post_install(dir): from subprocess import check_call # Upgrade/update will install if it doesn't exist. # We do this so these components are updated when the user updates the CLI. - if INSTALL_FROM_PUBLIC: - pip.main(['install', '--upgrade', 'azure-cli-component', '--disable-pip-version-check']) - check_call(['az', 'component', 'update', '-n', 'profile']) - else: + if INSTALL_FROM_PRIVATE: # use private PyPI server. if not PRIVATE_PYPI_URL: raise RuntimeError('{} environment variable not set.'.format(PRIVATE_PYPI_URL_ENV_NAME)) @@ -96,6 +94,9 @@ def _post_install(dir): PRIVATE_PYPI_URL, '--trusted-host', PRIVATE_PYPI_HOST, '--disable-pip-version-check']) check_call(['az', 'component', 'update', '-n', 'profile', '-p']) + else: + pip.main(['install', '--upgrade', 'azure-cli-component', '--disable-pip-version-check']) + check_call(['az', 'component', 'update', '-n', 'profile']) class OnInstall(install): def run(self): diff --git a/src/azure/cli/_profile.py b/src/azure/cli/_profile.py index 5945913f4..50fcb5b71 100644 --- a/src/azure/cli/_profile.py +++ b/src/azure/cli/_profile.py @@ -326,15 +326,15 @@ class CredsCache(object): self.adal_token_cache = adal.TokenCache(json.dumps(real_token)) return self.adal_token_cache - def save_service_principal_cred(self, client_id, secret, tenant): + def save_service_principal_cred(self, service_principal_id, secret, tenant): entry = { - _SERVICE_PRINCIPAL_ID: client_id, + _SERVICE_PRINCIPAL_ID: service_principal_id, _SERVICE_PRINCIPAL_TENANT: tenant, _ACCESS_TOKEN: secret } matched = [x for x in self._service_principal_creds - if client_id == x[_SERVICE_PRINCIPAL_ID] and + if service_principal_id == x[_SERVICE_PRINCIPAL_ID] and tenant == x[_SERVICE_PRINCIPAL_TENANT]] state_changed = False if matched: diff --git a/src/azure/cli/tests/test_profile.py b/src/azure/cli/tests/test_profile.py index 2116e98ff..8d8f5b1db 100644 --- a/src/azure/cli/tests/test_profile.py +++ b/src/azure/cli/tests/test_profile.py @@ -226,26 +226,181 @@ class Test_Profile(unittest.TestCase): self.assertEqual(mock_read_cred_file.call_count, 1) self.assertEqual(mock_persist_creds.call_count, 1) - def test_find_subscriptions_thru_username_password(self): - finder = SubscriptionFinder(lambda _,_2:AuthenticationContextStub(Test_Profile), + @mock.patch('adal.AuthenticationContext', autospec=True) + def test_find_subscriptions_thru_username_password(self, mock_auth_context): + mock_auth_context.acquire_token_with_username_password.return_value = self.token_entry1 + mock_auth_context.acquire_token.return_value = self.token_entry1 + mock_arm_client = mock.MagicMock() + mock_arm_client.tenants.list.return_value = [TenantStub(self.tenant_id)] + mock_arm_client.subscriptions.list.return_value = [self.subscription1] + finder = SubscriptionFinder(lambda _,_2: mock_auth_context, None, - lambda _: ArmClientStub(Test_Profile)) - subs = finder.find_from_user_account('foo', 'bar') - self.assertEqual([self.subscription1], subs) + lambda _: mock_arm_client) - def test_find_through_interactive_flow(self): - finder = SubscriptionFinder(lambda _,_2:AuthenticationContextStub(Test_Profile), + #action + subs = finder.find_from_user_account(self.user1, 'bar') + + #assert + self.assertEqual([self.subscription1], subs) + mock_auth_context.acquire_token_with_username_password.assert_called_once_with( + 'https://management.core.windows.net/', self.user1, 'bar', mock.ANY) + mock_auth_context.acquire_token.assert_called_once_with( + 'https://management.core.windows.net/', self.user1, mock.ANY) + + @mock.patch('adal.AuthenticationContext', autospec=True) + def test_find_subscriptions_through_interactive_flow(self, mock_auth_context): + test_nonsense_code = {'message':'magic code for you'} + mock_auth_context.acquire_user_code.return_value = test_nonsense_code + mock_auth_context.acquire_token_with_device_code.return_value = self.token_entry1 + mock_arm_client = mock.MagicMock() + mock_arm_client.tenants.list.return_value = [TenantStub(self.tenant_id)] + mock_arm_client.subscriptions.list.return_value = [self.subscription1] + finder = SubscriptionFinder(lambda _,_2: mock_auth_context, None, - lambda _: ArmClientStub(Test_Profile)) + lambda _: mock_arm_client) + + #action subs = finder.find_through_interactive_flow() + + #assert self.assertEqual([self.subscription1], subs) - - def test_find_from_service_principal_id(self): - finder = SubscriptionFinder(lambda _,_2:AuthenticationContextStub(Test_Profile), + mock_auth_context.acquire_user_code.assert_called_once_with( + 'https://management.core.windows.net/', mock.ANY) + mock_auth_context.acquire_token_with_device_code.assert_called_once_with( + 'https://management.core.windows.net/', test_nonsense_code, mock.ANY) + mock_auth_context.acquire_token.assert_called_once_with( + 'https://management.core.windows.net/', self.user1, mock.ANY) + + @mock.patch('adal.AuthenticationContext', autospec=True) + def test_find_subscriptions_from_service_principal_id(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 = [self.subscription1] + finder = SubscriptionFinder(lambda _,_2:mock_auth_context, None, - lambda _: ArmClientStub(Test_Profile)) + lambda _: mock_arm_client) + #action subs = finder.find_from_service_principal_id('my app', 'my secret', self.tenant_id) + + #assert self.assertEqual([self.subscription1], subs) + mock_arm_client.tenants.list.assert_not_called() + mock_auth_context.acquire_token.assert_not_called() + mock_auth_context.acquire_token_with_client_credentials.assert_called_once_with( + 'https://management.core.windows.net/', 'my app', 'my secret') + + @mock.patch('azure.cli._profile._read_file_content', autospec=True) + def test_credscache_load_tokens_and_sp_creds(self, mock_read_file): + test_sp = { + "servicePrincipalId": "myapp", + "servicePrincipalTenant": "mytenant", + "accessToken": "Secret" + } + mock_read_file.return_value = json.dumps([self.token_entry1, test_sp]) + + #action + creds_cache = CredsCache() + + #assert + token_entries = [entry for _, entry in creds_cache.adal_token_cache.read_items()] + self.assertEqual(token_entries, [self.token_entry1]) + self.assertEqual(creds_cache._service_principal_creds,[test_sp]) + + @mock.patch('azure.cli._profile._read_file_content', autospec=True) + @mock.patch('azure.cli._profile.codecs_open', autospec=True) + def test_credscache_add_new_sp_creds(self, mock_open_for_write, mock_read_file): + test_sp = { + "servicePrincipalId": "myapp", + "servicePrincipalTenant": "mytenant", + "accessToken": "Secret" + } + test_sp2 = { + "servicePrincipalId": "myapp2", + "servicePrincipalTenant": "mytenant2", + "accessToken": "Secret2" + } + mock_open_for_write.return_value = FileHandleStub() + mock_read_file.return_value = json.dumps([self.token_entry1, test_sp]) + creds_cache = CredsCache() + + #action + creds_cache.save_service_principal_cred( + test_sp2['servicePrincipalId'], + test_sp2['accessToken'], + test_sp2['servicePrincipalTenant']) + + #assert + token_entries = [entry for _, entry in creds_cache.adal_token_cache.read_items()] + self.assertEqual(token_entries, [self.token_entry1]) + self.assertEqual(creds_cache._service_principal_creds,[test_sp, test_sp2]) + mock_open_for_write.assert_called_with(mock.ANY, 'w', encoding='ascii') + + @mock.patch('azure.cli._profile._read_file_content', autospec=True) + @mock.patch('azure.cli._profile.codecs_open', autospec=True) + def test_credscache_remove_creds(self, mock_open_for_write, mock_read_file): + test_sp = { + "servicePrincipalId": "myapp", + "servicePrincipalTenant": "mytenant", + "accessToken": "Secret" + } + mock_open_for_write.return_value = FileHandleStub() + mock_read_file.return_value = json.dumps([self.token_entry1, test_sp]) + creds_cache = CredsCache() + + #action #1, logout a user + creds_cache.remove_cached_creds(self.user1) + + #assert #1 + token_entries = [entry for _, entry in creds_cache.adal_token_cache.read_items()] + self.assertEqual(token_entries, []) + + #action #2 logout a service principal + creds_cache.remove_cached_creds('myapp') + + #assert #2 + self.assertEqual(creds_cache._service_principal_creds,[]) + + mock_open_for_write.assert_called_with(mock.ANY, 'w', encoding='ascii') + self.assertEqual(mock_open_for_write.call_count, 2) + + @mock.patch('azure.cli._profile._read_file_content', autospec=True) + @mock.patch('azure.cli._profile.codecs_open', autospec=True) + @mock.patch('adal.AuthenticationContext', autospec=True) + def test_credscache_new_token_added_by_adal(self, mock_adal_auth_context, mock_open_for_write, mock_read_file): + token_entry2 = { + "accessToken": "new token", + "userId": self.user1 + } + def acquire_token_side_effect(*args): + creds_cache.adal_token_cache.has_state_changed = True + return token_entry2 + def get_auth_context(authority, **kwargs): + mock_adal_auth_context.cache = kwargs['cache'] + return mock_adal_auth_context + + mock_adal_auth_context.acquire_token.side_effect = acquire_token_side_effect + mock_open_for_write.return_value = FileHandleStub() + mock_read_file.return_value = json.dumps([self.token_entry1]) + creds_cache = CredsCache(auth_ctx_factory=get_auth_context) + token = creds_cache.retrieve_token_for_user(self.user1, self.tenant_id) + + #action + mock_adal_auth_context.acquire_token.assert_called_once_with( + 'https://management.core.windows.net/', + self.user1, + mock.ANY) + + #assert + mock_open_for_write.assert_called_with(mock.ANY, 'w', encoding='ascii') + self.assertEqual(token, 'new token') + +class FileHandleStub: + def write(self, content): + pass + def __enter__(self): + return self + def __exit__(self, _2, _3, _4): + pass class SubscriptionStub: def __init__(self, id, display_name, state, tenant_id): @@ -254,47 +409,9 @@ class SubscriptionStub: self.state = state self.tenant_id = tenant_id -class AuthenticationContextStub: - def __init__(self, test_profile_cls, return_token1=True): - #we need to reference some pre-defined test artifacts in Test_Profile - self._test_profile_cls = test_profile_cls - if not return_token1: - raise ValueError('Please update to return other test tokens') - - def acquire_token_with_username_password(self, _, _2, _3, _4): - return self._test_profile_cls.token_entry1 - - def acquire_token_with_device_code(self, _, _2, _3): - return self._test_profile_cls.token_entry1 - - def acquire_token_with_client_credentials(self, _, _2, _3): - return self._test_profile_cls.token_entry1 - - def acquire_token(self, _, _2, _3): - return self._test_profile_cls.token_entry1 - - def acquire_user_code(self, _, _2): - return {'message': 'secret code for you'} - -class ArmClientStub: - class TenantStub: - def __init__(self, tenant_id): - self.tenant_id = tenant_id - - class OperationsStub: - def __init__(self, list_result): - self._list_result = list_result - - def list(self): - return self._list_result - - def __init__(self, test_profile_cls, use_tenant1_and_subscription1=True): - self._test_profile_cls = test_profile_cls - if use_tenant1_and_subscription1: - self.tenants = ArmClientStub.OperationsStub([ArmClientStub.TenantStub(test_profile_cls.tenant_id)]) - self.subscriptions = ArmClientStub.OperationsStub([test_profile_cls.subscription1]) - else: - raise ValueError('Please update to return other test subscriptions') +class TenantStub: + def __init__(self, tenant_id): + self.tenant_id = tenant_id if __name__ == '__main__': unittest.main() diff --git a/src/command_modules/azure-cli-resource/azure/cli/command_modules/resource/__init__.py b/src/command_modules/azure-cli-resource/azure/cli/command_modules/resource/__init__.py index 0dccadbfc..95fa914cc 100644 --- a/src/command_modules/azure-cli-resource/azure/cli/command_modules/resource/__init__.py +++ b/src/command_modules/azure-cli-resource/azure/cli/command_modules/resource/__init__.py @@ -6,8 +6,8 @@ from azure.cli._locale import L command_table = CommandTable() def _resource_client_factory(*args): # pylint: disable=unused-argument - from azure.mgmt.resource.resources import (ResourceManagementClient, - ResourceManagementClientConfiguration) +from azure.mgmt.resource.resources import (ResourceManagementClient, + ResourceManagementClientConfiguration) return get_mgmt_service_client(ResourceManagementClient, ResourceManagementClientConfiguration) @command_table.command('resource group list', description=L('List resource groups')) @@ -38,7 +38,7 @@ def list_groups(args): help=L('the resource type in format: /'), required=True) @command_table.option('--api-version -o', help=L('the API version of the resource provider')) -@command_table.option('--parent', +@command_table.option('--parent', default='', help=L('the name of the parent resource (if needed), ' + \ 'in / format')) def show_resource(args): @@ -56,9 +56,8 @@ def show_resource(args): raise IncorrectUsageError( L('API version is required and could not be resolved for resource {}' .format(full_type))) - results = rmc.resources.get( - resource_group_name=args.get('resource_group'), + resource_group_name=args.get('resourcegroup'), resource_name=args.get('name'), resource_provider_namespace=provider_namespace, resource_type=resource_type, @@ -88,18 +87,12 @@ def _resolve_api_version(args, rmc): raise IncorrectUsageError('Parameter --parent must be in / format.') resource_type = "{}/{}".format(parent_type, resource_type) - else: - resource_type = resource_type provider = rmc.providers.get(provider_namespace) - for t in provider.resource_types: - if t.resource_type == resource_type: - # Return first non-preview version - for version in t.api_versions: - if not version.find('preview'): - return version - # No non-preview version found. Take first preview version - try: - return t.api_versions[0] - except IndexError: - return None + + rt = [t for t in provider.resource_types if t.resource_type == resource_type] + if not rt: + raise IncorrectUsageError('Resource type {} not found.'.format(full_type)) + if len(rt) == 1 and rt[0].api_versions: + npv = [v for v in rt[0].api_versions if "preview" not in v] + return npv[0] if npv else rt[0].api_versions[0] return None diff --git a/src/command_modules/azure-cli-resource/azure/cli/command_modules/resource/tests/test_api_check.py b/src/command_modules/azure-cli-resource/azure/cli/command_modules/resource/tests/test_api_check.py new file mode 100644 index 000000000..c3d346d93 --- /dev/null +++ b/src/command_modules/azure-cli-resource/azure/cli/command_modules/resource/tests/test_api_check.py @@ -0,0 +1,64 @@ +import unittest +try: + from unittest.mock import MagicMock +except ImportError: + from mock import MagicMock + +from azure.cli.command_modules.resource import _resolve_api_version as resolve_api_version + +class TestApiCheck(unittest.TestCase): + + @classmethod + def setUpClass(cls): + pass + + @classmethod + def tearDownClass(cls): + pass + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_resolve_api_max_priority_option(self): + """ Verifies the --api-version parameter has maximum priority. """ + args = {'api-version': '2015-01-01', 'resource-type': 'Mock/test'} + self.assertEqual(resolve_api_version(args, self._get_mock_client()), "2015-01-01") + + def test_resolve_api_provider_backup(self): + """ Verifies provider is used as backup if api-version not specified. """ + args = {'resource-type': 'Mock/test'} + self.assertEqual(resolve_api_version(args, self._get_mock_client()), "2016-01-01") + + def test_resolve_api_provider_with_parent_backup(self): + """ Verifies provider (with parent) is used as backup if api-version not specified. """ + args = {'resource-type': 'Mock/bar', 'parent': 'foo/testfoo123'} + self.assertEqual(resolve_api_version(args, self._get_mock_client()), "1999-01-01") + + def test_resolve_api_all_previews(self): + """ Verifies most recent preview version returned only if there are no non-preview versions. """ + args = {'resource-type': 'Mock/preview'} + self.assertEqual(resolve_api_version(args, self._get_mock_client()), "2005-01-01-preview") + + def _get_mock_client(self): + client = MagicMock() + provider = MagicMock() + provider.resource_types = [ + self._get_mock_resource_type('skip', ['2000-01-01-preview', '2000-01-01']), + self._get_mock_resource_type('test', ['2016-01-01-preview', '2016-01-01']), + self._get_mock_resource_type('foo/bar', ['1999-01-01-preview', '1999-01-01']), + self._get_mock_resource_type('preview', ['2005-01-01-preview', '2004-01-01-preview']) + ] + client.providers.get.return_value = provider + return client + + def _get_mock_resource_type(self, name, api_versions): + rt = MagicMock() + rt.resource_type = name + rt.api_versions = api_versions + return rt + +if __name__ == '__main__': + unittest.main()