Skip to content
Closed
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
3 changes: 3 additions & 0 deletions src/azure-cli-core/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

Release History
===============
2.0.34
++++++
* core: support cross tenant resource referencing

2.0.33
++++++
Expand Down
20 changes: 17 additions & 3 deletions src/azure-cli-core/azure/cli/core/_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -437,14 +437,20 @@ 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 = []
for s in [x for x in (aux_subscriptions or []) if x != subscription_id]:
a = self.get_subscription(s)
if a[_TENANT_ID] != account[_TENANT_ID]:
external_tenants_info.append((a[_USER_ENTITY][_USER_NAME], a[_TENANT_ID]))

if identity_type is None:
def _retrieve_token():
if in_cloud_console() and account[_USER_ENTITY].get(_CLOUD_SHELL_ID):
Expand All @@ -453,8 +459,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)
Expand Down
10 changes: 8 additions & 2 deletions src/azure-cli-core/azure/cli/core/adal_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand Down
14 changes: 11 additions & 3 deletions src/azure-cli-core/azure/cli/core/commands/client_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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


Expand Down Expand Up @@ -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:
Expand Down
67 changes: 47 additions & 20 deletions src/azure-cli-core/azure/cli/core/tests/test_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion src/azure-cli-core/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
logger.warn("Wheel is not available, disabling bdist_wheel hook")
cmdclass = {}

VERSION = "2.0.33"
VERSION = "2.0.34"
# If we have source, validate that our version numbers match
# This should prevent uploading releases with mismatched versions.
try:
Expand Down
3 changes: 3 additions & 0 deletions src/command_modules/azure-cli-network/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

Release History
===============
2.1.3
++++++
* `network vnet peering create`: support cross tenant vnet peering

2.1.2
++++++
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, **_):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
2 changes: 1 addition & 1 deletion src/command_modules/azure-cli-network/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 4 additions & 0 deletions src/command_modules/azure-cli-vm/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

Release History
===============
2.0.33
++++++
* `vm/vmss create`: support create from managed images from other tenants

2.0.32
++++++
* BREAKING CHANGE: remove `--write-accelerator` from `vm create`. The same support
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
def _compute_client_factory(cli_ctx, **kwargs):
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_COMPUTE, subscription_id=kwargs.get('subscription_id'))
return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_COMPUTE, subscription_id=kwargs.get('subscription_id'),
aux_subscriptions=kwargs.get('aux_subscriptions'))


def cf_ni(cli_ctx, _):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ def load_arguments(self, _):
c.argument('application_security_groups', resource_type=ResourceType.MGMT_NETWORK, min_api='2017-09-01', nargs='+', options_list=['--asgs'], help='Space-separated list of existing application security groups to associate with the VM.', arg_group='Network', validator=validate_asg_names_or_ids)
c.argument('boot_diagnostics_storage',
help='pre-existing storage account name or its blob uri to capture boot diagnostics. Its sku should be one of Standard_GRS, Standard_LRS and Standard_RAGRS')
c.ignore('aux_subscriptions')

with self.argument_context('vm open-port') as c:
c.argument('vm_name', name_arg_type, help='The name of the virtual machine to open inbound traffic on.')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -408,11 +408,16 @@ def _validate_vm_create_storage_profile(cmd, namespace, for_scale_set=False):
if namespace.storage_profile == StorageProfile.ManagedCustomImage:
# extract additional information from a managed custom image
res = parse_resource_id(namespace.image)
compute_client = _compute_client_factory(cmd.cli_ctx, subscription_id=res['subscription'])
subscription_id = res['subscription']
compute_client = _compute_client_factory(cmd.cli_ctx, subscription_id=subscription_id)
image_info = compute_client.images.get(res['resource_group'], res['name'])
# pylint: disable=no-member
namespace.os_type = image_info.storage_profile.os_disk.os_type.value
image_data_disks = image_info.storage_profile.data_disks
if subscription_id:
if not namespace.aux_subscriptions:
namespace.aux_subscriptions = []
namespace.aux_subscriptions.append(subscription_id)

elif namespace.storage_profile == StorageProfile.ManagedSpecializedOSDisk:
# accept disk name or ID
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,7 @@ def create_vm(cmd, vm_name, resource_group_name, image=None, size='Standard_DS1_
validate=False, custom_data=None, secrets=None, plan_name=None, plan_product=None, plan_publisher=None,
plan_promotion_code=None, license_type=None, assign_identity=None, identity_scope=None,
identity_role='Contributor', identity_role_id=None, application_security_groups=None, zone=None,
boot_diagnostics_storage=None):
boot_diagnostics_storage=None, aux_subscriptions=None):
from azure.cli.core.commands.client_factory import get_subscription_id
from azure.cli.core.util import random_string, hash_string
from azure.cli.core.commands.arm import ArmTemplateBuilder
Expand Down Expand Up @@ -648,7 +648,8 @@ def create_vm(cmd, vm_name, resource_group_name, image=None, size='Standard_DS1_

# deploy ARM template
deployment_name = 'vm_deploy_' + random_string(32)
client = get_mgmt_service_client(cmd.cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES).deployments
client = get_mgmt_service_client(cmd.cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES,
aux_subscriptions=aux_subscriptions).deployments
DeploymentProperties = cmd.get_models('DeploymentProperties', resource_type=ResourceType.MGMT_RESOURCE_RESOURCES)
properties = DeploymentProperties(template=template, parameters=parameters, mode='incremental')
if validate:
Expand Down Expand Up @@ -1775,7 +1776,7 @@ def create_vmss(cmd, vmss_name, resource_group_name, image,
single_placement_group=None, custom_data=None, secrets=None, platform_fault_domain_count=None,
plan_name=None, plan_product=None, plan_publisher=None, plan_promotion_code=None, license_type=None,
assign_identity=None, identity_scope=None, identity_role='Contributor',
identity_role_id=None, zones=None, priority=None, eviction_policy=None):
identity_role_id=None, zones=None, priority=None, eviction_policy=None, aux_subscriptions=None):
from azure.cli.core.commands.client_factory import get_subscription_id
from azure.cli.core.util import random_string, hash_string
from azure.cli.core.commands.arm import ArmTemplateBuilder
Expand Down Expand Up @@ -2006,7 +2007,8 @@ def _get_public_ip_address_allocation(value, sku):

# deploy ARM template
deployment_name = 'vmss_deploy_' + random_string(32)
client = get_mgmt_service_client(cmd.cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES).deployments
client = get_mgmt_service_client(cmd.cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES,
aux_subscriptions=aux_subscriptions).deployments
DeploymentProperties = cmd.get_models('DeploymentProperties', resource_type=ResourceType.MGMT_RESOURCE_RESOURCES)

properties = DeploymentProperties(template=template, parameters=parameters, mode='incremental')
Expand Down
Loading