diff --git a/src/azure-cli-core/HISTORY.rst b/src/azure-cli-core/HISTORY.rst index 10109ff1ca2..79e46df4a1a 100644 --- a/src/azure-cli-core/HISTORY.rst +++ b/src/azure-cli-core/HISTORY.rst @@ -2,10 +2,9 @@ Release History =============== - 2.0.34 ++++++ -* Minor fixes. +* core: support cross tenant resource referencing * Improve telemetry upload reliability 1. Remove retry. Once failed stop uploading. 2. Update the process start configuration to prevent upload process from blocking the CLI process. diff --git a/src/azure-cli-core/azure/cli/core/_profile.py b/src/azure-cli-core/azure/cli/core/_profile.py index 511cdbd5c27..18f25933ead 100644 --- a/src/azure-cli-core/azure/cli/core/_profile.py +++ b/src/azure-cli-core/azure/cli/core/_profile.py @@ -439,14 +439,21 @@ def _try_parse_msi_account_name(account): return parts[0], (None if len(parts) <= 1 else parts[1]) return None, None - def get_login_credentials(self, resource=None, - subscription_id=None): + def get_login_credentials(self, resource=None, subscription_id=None, aux_subscriptions=None): account = self.get_subscription(subscription_id) user_type = account[_USER_ENTITY][_USER_TYPE] username_or_sp_id = account[_USER_ENTITY][_USER_NAME] resource = resource or self.cli_ctx.cloud.endpoints.active_directory_resource_id identity_type, identity_id = Profile._try_parse_msi_account_name(account) + + external_tenants_info = [] + ext_subs = [aux_sub for aux_sub in (aux_subscriptions or []) if aux_sub != subscription_id] + for ext_sub in ext_subs: + sub = self.get_subscription(ext_sub) + if sub[_TENANT_ID] != account[_TENANT_ID]: + external_tenants_info.append((sub[_USER_ENTITY][_USER_NAME], sub[_TENANT_ID])) + if identity_type is None: def _retrieve_token(): if in_cloud_console() and account[_USER_ENTITY].get(_CLOUD_SHELL_ID): @@ -455,8 +462,16 @@ def _retrieve_token(): return self._creds_cache.retrieve_token_for_user(username_or_sp_id, account[_TENANT_ID], resource) return self._creds_cache.retrieve_token_for_service_principal(username_or_sp_id, resource) + + def _retrieve_tokens_from_external_tenants(): + external_tokens = [] + for u, t in external_tenants_info: + external_tokens.append(self._creds_cache.retrieve_token_for_user(u, t, resource)) + return external_tokens + from azure.cli.core.adal_authentication import AdalAuthentication - auth_object = AdalAuthentication(_retrieve_token) + auth_object = AdalAuthentication(_retrieve_token, + _retrieve_tokens_from_external_tenants if external_tenants_info else None) else: if self._msi_creds is None: self._msi_creds = MsiAccountTypes.msi_auth_factory(identity_type, identity_id, resource) diff --git a/src/azure-cli-core/azure/cli/core/adal_authentication.py b/src/azure-cli-core/azure/cli/core/adal_authentication.py index 223fb479baa..4dc0c88873b 100644 --- a/src/azure-cli-core/azure/cli/core/adal_authentication.py +++ b/src/azure-cli-core/azure/cli/core/adal_authentication.py @@ -14,14 +14,17 @@ class AdalAuthentication(Authentication): # pylint: disable=too-few-public-methods - def __init__(self, token_retriever): + def __init__(self, token_retriever, external_tenant_token_retriever=None): self._token_retriever = token_retriever + self._external_tenant_token_retriever = external_tenant_token_retriever def signed_session(self, session=None): session = session or super(AdalAuthentication, self).signed_session() - + external_tenant_tokens = None try: scheme, token, _ = self._token_retriever() + if self._external_tenant_token_retriever: + external_tenant_tokens = self._external_tenant_token_retriever() except CLIError as err: if in_cloud_console(): AdalAuthentication._log_hostname() @@ -40,6 +43,9 @@ def signed_session(self, session=None): header = "{} {}".format(scheme, token) session.headers['Authorization'] = header + if external_tenant_tokens: + aux_tokens = ';'.join(['{} {}'.format(scheme2, tokens2) for scheme2, tokens2, _ in external_tenant_tokens]) + session.headers['x-ms-authorization-auxiliary'] = aux_tokens return session @staticmethod diff --git a/src/azure-cli-core/azure/cli/core/commands/client_factory.py b/src/azure-cli-core/azure/cli/core/commands/client_factory.py index 961adb89afb..9aaf86d4ac5 100644 --- a/src/azure-cli-core/azure/cli/core/commands/client_factory.py +++ b/src/azure-cli-core/azure/cli/core/commands/client_factory.py @@ -44,7 +44,11 @@ def resolve_client_arg_name(operation, kwargs): def get_mgmt_service_client(cli_ctx, client_or_resource_type, subscription_id=None, api_version=None, - **kwargs): + aux_subscriptions=None, **kwargs): + """ + :params subscription_id: the current account's subscription + :param aux_subscriptions: mainly for cross tenant scenarios, say vnet peering. + """ sdk_profile = None if isinstance(client_or_resource_type, (ResourceType, CustomResourceType)): # Get the versioned client @@ -57,7 +61,9 @@ def get_mgmt_service_client(cli_ctx, client_or_resource_type, subscription_id=No # Get the non-versioned client client_type = client_or_resource_type client, _ = _get_mgmt_service_client(cli_ctx, client_type, subscription_id=subscription_id, - api_version=api_version, sdk_profile=sdk_profile, **kwargs) + api_version=api_version, sdk_profile=sdk_profile, + aux_subscriptions=aux_subscriptions, + **kwargs) return client @@ -104,12 +110,14 @@ def _get_mgmt_service_client(cli_ctx, base_url_bound=True, resource=None, sdk_profile=None, + aux_subscriptions=None, **kwargs): from azure.cli.core._profile import Profile logger.debug('Getting management service client client_type=%s', client_type.__name__) resource = resource or cli_ctx.cloud.endpoints.active_directory_resource_id profile = Profile(cli_ctx=cli_ctx) - cred, subscription_id, _ = profile.get_login_credentials(subscription_id=subscription_id, resource=resource) + cred, subscription_id, _ = profile.get_login_credentials(subscription_id=subscription_id, resource=resource, + aux_subscriptions=aux_subscriptions) client_kwargs = {} if base_url_bound: diff --git a/src/azure-cli-core/azure/cli/core/tests/test_profile.py b/src/azure-cli-core/azure/cli/core/tests/test_profile.py index 64bec82e825..3858d1bae56 100644 --- a/src/azure-cli-core/azure/cli/core/tests/test_profile.py +++ b/src/azure-cli-core/azure/cli/core/tests/test_profile.py @@ -82,26 +82,6 @@ def setUpClass(cls): 'Q8U2g9kXHrbYFeY2gJxF_hnfLvNKxUKUBnftmyYxZwKi0GDS0BvdJnJnsqSRSpxUx__Ra9QJkG1IaDzj' 'ZcSZPHK45T6ohK9Hk9ktZo0crVl7Tmw') - cls.test_cloud_shell_msi_access_token = ( - 'yJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IlNTUWRoSTFjS3ZoUUVEU0p4RTJnR1lzNDBRMCIsImtpZCI6IlNTUWRoSTFjS3' - 'ZoUUVEU0p4RTJnR1lzNDBRMCJ9.eyJhdWQiOiJodHRwczovL21hbmFnZW1lbnQuY29yZS53aW5kb3dzLm5ldC8iLCJpc3MiOiJodHRwcz' - 'ovL3N0cy53aW5kb3dzLm5ldC81NDgyNmIyMi0zOGQ2LTRmYjItYmFkOS1iN2I5M2EzZTljNWEvIiwiaWF0IjoxNTIwMjgzODI3LCJuYmY' - 'iOjE1MjAyODM4MjcsImV4cCI6MTUyMDI4ODAyNiwiYWNyIjoiMSIsImFpbyI6IkFWUUFxLzhHQUFBQXppd1c2VE1heElJeGxxVkR3TnAx' - 'MkZvNG5IeVc3NnFXd0ZlS2VlanlYTmdrRUFlckNBM1JoQ0ZLU3VMOGRaQXVBQnd6cTErOTgzdlRoK1dHMTdqa0NWSWVtN1JwYXU5M3Zla' - '2RVbkxxdVpRPSIsImFsdHNlY2lkIjoiNTo6MTAwMzAwMDA4MDFDNDREMyIsImFtciI6WyJyc2EiXSwiYXBwaWQiOiJiNjc3YzI5MC1jZj' - 'RiLTRhOGUtYTYwZS05MWJhNjUwYTRhYmUiLCJhcHBpZGFjciI6IjIiLCJlX2V4cCI6MjYzMDk5LCJlbWFpbCI6Inl1Z2FuZ3dAbWljcm9' - 'zb2Z0LmNvbSIsImZhbWlseV9uYW1lIjoiV2FuZyIsImdpdmVuX25hbWUiOiJZdWdhbmciLCJncm91cHMiOlsiZTRiYjBiNTYtMTAxNC00' - 'MGY4LTg4YWItM2Q4YThjYjBlMDg2Il0sImlkcCI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0LzcyZjk4OGJmLTg2ZjEtNDFhZi05MWFiL' - 'TJkN2NkMDExZGI0Ny8iLCJpcGFkZHIiOiIxNjcuMjIwLjEuMjM0IiwibmFtZSI6Ill1Z2FuZyBXYW5nIiwib2lkIjoiODllZDViZTgtZm' - 'Y5Ny00MWI1LWFiMTEtMDU1ZTFlM2NjMzRiIiwicHVpZCI6IjEwMDNCRkZEOTU5Rjg5NTUiLCJzY3AiOiJ1c2VyX2ltcGVyc29uYXRpb24' - 'iLCJzdWIiOiIyRFhuT05jNUVBcjZhXzNVcmtSUmJRQXZHbnh6cUFhLUhMVnMxcld3Z3RJIiwidGlkIjoiNTQ4MjZiMjItMzhkNi00ZmIy' - 'LWJhZDktYjdiOTNhM2U5YzVhIiwidW5pcXVlX25hbWUiOiJ5dWdhbmd3QG1pY3Jvc29mdC5jb20iLCJ1dGkiOiJESGNDOFQwYkJrLTh5W' - 'VB2cjlBQ0FBIiwidmVyIjoiMS4wIiwid2lkcyI6WyI2MmU5MDM5NC02OWY1LTQyMzctOTE5MC0wMTIxNzcxNDVlMTAiXX0.U5rdKCPd_3' - 'EsleHmZhWaYe19I3jNzFSwvzn84f8cExXbgxkK-X8ejkE_J4A_SufHnaI1x_QHgEIpbIz6RD99tUyccI-emNCpJpM7Ucfhl779gAOdVzy' - '75Nc87RhXOXVObNlfvay_BKJ3bDEcayXeoRcPRa2uJ-4c8t6rAqFAHi8UrxOOo2lTTJqhWWlLJ00qY3y31MJQqR_ThwMyaHrORgrnMS6_' - '2if0WIg9-BMDbZYiSOIHKJApZNBi2W1Bl-S4FIkh_e70QWQn1h5p1D8eGmnI1vSyCwb6PpIYW93vldYe0Q4hketRlDXyGlOmRZywN7eHZ' - 'qUGFKxJnyEx9rKrvg') - def test_normalize(self): cli = TestCli() storage_mock = {'subscriptions': None} @@ -469,6 +449,53 @@ def test_get_login_credentials(self, mock_get_token, mock_read_cred_file): 'https://management.core.windows.net/') self.assertEqual(mock_get_token.call_count, 1) + @mock.patch('azure.cli.core._profile._load_tokens_from_file', autospec=True) + @mock.patch('azure.cli.core._profile.CredsCache.retrieve_token_for_user', autospec=True) + def test_get_login_credentials_aux_subscriptions(self, mock_get_token, mock_read_cred_file): + cli = TestCli() + raw_token2 = 'some...secrets2' + token_entry2 = { + "resource": "https://management.core.windows.net/", + "tokenType": "Bearer", + "_authority": "https://login.microsoftonline.com/common", + "accessToken": raw_token2, + } + some_token_type = 'Bearer' + mock_read_cred_file.return_value = [TestProfile.token_entry1, token_entry2] + mock_get_token.side_effect = [(some_token_type, TestProfile.raw_token1), (some_token_type, raw_token2)] + # setup + storage_mock = {'subscriptions': None} + profile = Profile(cli_ctx=cli, storage=storage_mock, use_global_creds_cache=False, async_persist=False) + test_subscription_id = '12345678-1bf0-4dda-aec3-cb9272f09590' + test_subscription_id2 = '12345678-1bf0-4dda-aec3-cb9272f09591' + test_tenant_id = '12345678-38d6-4fb2-bad9-b7b93a3e1234' + test_tenant_id2 = '12345678-38d6-4fb2-bad9-b7b93a3e4321' + test_subscription = SubscriptionStub('/subscriptions/{}'.format(test_subscription_id), + 'MSI-DEV-INC', self.state1, test_tenant_id) + test_subscription2 = SubscriptionStub('/subscriptions/{}'.format(test_subscription_id2), + 'MSI-DEV-INC2', self.state1, test_tenant_id2) + consolidated = profile._normalize_properties(self.user1, + [test_subscription, test_subscription2], + False) + profile._set_subscriptions(consolidated) + # action + cred, subscription_id, _ = profile.get_login_credentials(subscription_id=test_subscription_id, + aux_subscriptions=[test_subscription_id2]) + + # verify + self.assertEqual(subscription_id, test_subscription_id) + + # verify the cred._tokenRetriever is a working lambda + token_type, token = cred._token_retriever() + self.assertEqual(token, self.raw_token1) + self.assertEqual(some_token_type, token_type) + + token2 = cred._external_tenant_token_retriever() + self.assertEqual(len(token2), 1) + self.assertEqual(token2[0][1], raw_token2) + + self.assertEqual(mock_get_token.call_count, 2) + @mock.patch('azure.cli.core._profile._load_tokens_from_file', autospec=True) @mock.patch('msrestazure.azure_active_directory.MSIAuthentication', autospec=True) def test_get_login_credentials_msi_system_assigned(self, mock_msi_auth, mock_read_cred_file): diff --git a/src/command_modules/azure-cli-network/HISTORY.rst b/src/command_modules/azure-cli-network/HISTORY.rst index 6ae1a7956ca..04e7c3ca63a 100644 --- a/src/command_modules/azure-cli-network/HISTORY.rst +++ b/src/command_modules/azure-cli-network/HISTORY.rst @@ -2,6 +2,9 @@ Release History =============== +2.1.3 +++++++ +* `network vnet peering`: a few improvements 2.1.2 ++++++ diff --git a/src/command_modules/azure-cli-network/azure/cli/command_modules/network/_client_factory.py b/src/command_modules/azure-cli-network/azure/cli/command_modules/network/_client_factory.py index 3dc503a0b0d..7ca3ec8ab12 100644 --- a/src/command_modules/azure-cli-network/azure/cli/command_modules/network/_client_factory.py +++ b/src/command_modules/azure-cli-network/azure/cli/command_modules/network/_client_factory.py @@ -4,10 +4,11 @@ # -------------------------------------------------------------------------------------------- -def network_client_factory(cli_ctx, **_): +def network_client_factory(cli_ctx, aux_subscriptions=None, **_): from azure.cli.core.profiles import ResourceType from azure.cli.core.commands.client_factory import get_mgmt_service_client - return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_NETWORK) + return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_NETWORK, + aux_subscriptions=aux_subscriptions) def resource_client_factory(cli_ctx, **_): diff --git a/src/command_modules/azure-cli-network/azure/cli/command_modules/network/commands.py b/src/command_modules/azure-cli-network/azure/cli/command_modules/network/commands.py index a54c37cd21c..f53df2f1574 100644 --- a/src/command_modules/azure-cli-network/azure/cli/command_modules/network/commands.py +++ b/src/command_modules/azure-cli-network/azure/cli/command_modules/network/commands.py @@ -207,6 +207,8 @@ def load_command_table(self, _): client_factory=cf_packet_capture ) + network_custom = CliCommandType(operations_tmpl='azure.cli.command_modules.network.custom#{}') + # endregion # region NetworkRoot @@ -624,7 +626,7 @@ def _make_singular(value): g.command('show', 'get', exception_handler=empty_on_404) g.command('list', 'list') g.command('delete', 'delete') - g.generic_update_command('update', setter_arg_name='virtual_network_peering_parameters') + g.generic_update_command('update', setter_name='update_vnet_peering', setter_type=network_custom) with self.command_group('network vnet subnet', network_subnet_sdk) as g: g.command('delete', 'delete') diff --git a/src/command_modules/azure-cli-network/azure/cli/command_modules/network/custom.py b/src/command_modules/azure-cli-network/azure/cli/command_modules/network/custom.py index b6ab0d2c8bd..94fef6eeddd 100644 --- a/src/command_modules/azure-cli-network/azure/cli/command_modules/network/custom.py +++ b/src/command_modules/azure-cli-network/azure/cli/command_modules/network/custom.py @@ -2918,9 +2918,19 @@ def create_vnet_peering(cmd, resource_group_name, virtual_network_name, virtual_ allow_gateway_transit=allow_gateway_transit, allow_forwarded_traffic=allow_forwarded_traffic, use_remote_gateways=use_remote_gateways) - ncf = network_client_factory(cmd.cli_ctx) + aux_subscription = parse_resource_id(remote_virtual_network)['subscription'] + ncf = network_client_factory(cmd.cli_ctx, aux_subscriptions=[aux_subscription]) + return ncf.virtual_network_peerings.create_or_update( + resource_group_name, virtual_network_name, virtual_network_peering_name, peering) + + +def update_vnet_peering(cmd, resource_group_name, virtual_network_name, virtual_network_peering_name, **kwargs): + peering = kwargs['parameters'] + aux_subscription = parse_resource_id(peering.remote_virtual_network.id)['subscription'] + ncf = network_client_factory(cmd.cli_ctx, aux_subscriptions=[aux_subscription]) return ncf.virtual_network_peerings.create_or_update( resource_group_name, virtual_network_name, virtual_network_peering_name, peering) + # endregion diff --git a/src/command_modules/azure-cli-network/setup.py b/src/command_modules/azure-cli-network/setup.py index 55a451b8a25..83a8f0e5b07 100644 --- a/src/command_modules/azure-cli-network/setup.py +++ b/src/command_modules/azure-cli-network/setup.py @@ -14,7 +14,7 @@ logger.warn("Wheel is not available, disabling bdist_wheel hook") cmdclass = {} -VERSION = "2.1.2" +VERSION = "2.1.3" CLASSIFIERS = [ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers',