{Core} Honor scopes specified by Track 2 SDK (#15184)

This commit is contained in:
Jiashuo Li 2020-10-27 10:06:21 +08:00 коммит произвёл GitHub
Родитель 00cf5e3b3e
Коммит a804e5f8eb
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
7 изменённых файлов: 105 добавлений и 20 удалений

Просмотреть файл

@ -559,26 +559,35 @@ class Profile:
external_tenants_info.append(sub[_TENANT_ID])
if identity_type is None:
def _retrieve_token():
def _retrieve_token(sdk_resource=None):
# When called by
# - Track 1 SDK, use `resource` specified by CLI
# - Track 2 SDK, use `sdk_resource` specified by SDK and ignore `resource` specified by CLI
token_resource = sdk_resource or resource
logger.debug("Retrieving token from ADAL for resource %r", token_resource)
if in_cloud_console() and account[_USER_ENTITY].get(_CLOUD_SHELL_ID):
return self._get_token_from_cloud_shell(resource)
return self._get_token_from_cloud_shell(token_resource)
if user_type == _USER:
return self._creds_cache.retrieve_token_for_user(username_or_sp_id,
account[_TENANT_ID], resource)
account[_TENANT_ID], token_resource)
use_cert_sn_issuer = account[_USER_ENTITY].get(_SERVICE_PRINCIPAL_CERT_SN_ISSUER_AUTH)
return self._creds_cache.retrieve_token_for_service_principal(username_or_sp_id, resource,
return self._creds_cache.retrieve_token_for_service_principal(username_or_sp_id, token_resource,
account[_TENANT_ID],
use_cert_sn_issuer)
def _retrieve_tokens_from_external_tenants():
def _retrieve_tokens_from_external_tenants(sdk_resource=None):
token_resource = sdk_resource or resource
logger.debug("Retrieving token from ADAL for external tenants and resource %r", token_resource)
external_tokens = []
for sub_tenant_id in external_tenants_info:
if user_type == _USER:
external_tokens.append(self._creds_cache.retrieve_token_for_user(
username_or_sp_id, sub_tenant_id, resource))
username_or_sp_id, sub_tenant_id, token_resource))
else:
external_tokens.append(self._creds_cache.retrieve_token_for_service_principal(
username_or_sp_id, resource, sub_tenant_id, resource))
username_or_sp_id, token_resource, sub_tenant_id, token_resource))
return external_tokens
from azure.cli.core.adal_authentication import AdalAuthentication
@ -621,6 +630,8 @@ class Profile:
return username_or_sp_id, sp_secret, None, str(account[_TENANT_ID])
def get_raw_token(self, resource=None, subscription=None, tenant=None):
logger.debug("Profile.get_raw_token invoked with resource=%r, subscription=%r, tenant=%r",
resource, subscription, tenant)
if subscription and tenant:
raise CLIError("Please specify only one of subscription and tenant, not both")
account = self.get_subscription(subscription)

Просмотреть файл

@ -10,9 +10,12 @@ import adal
from msrest.authentication import Authentication
from msrestazure.azure_active_directory import MSIAuthentication
from azure.core.credentials import AccessToken
from azure.cli.core.util import in_cloud_console
from azure.cli.core.util import in_cloud_console, scopes_to_resource
from knack.util import CLIError
from knack.log import get_logger
logger = get_logger(__name__)
class AdalAuthentication(Authentication): # pylint: disable=too-few-public-methods
@ -21,12 +24,15 @@ class AdalAuthentication(Authentication): # pylint: disable=too-few-public-meth
self._token_retriever = token_retriever
self._external_tenant_token_retriever = external_tenant_token_retriever
def _get_token(self):
def _get_token(self, sdk_resource=None):
"""
:param sdk_resource: `resource` converted from Track 2 SDK's `scopes`
"""
external_tenant_tokens = None
try:
scheme, token, full_token = self._token_retriever()
scheme, token, full_token = self._token_retriever(sdk_resource)
if self._external_tenant_token_retriever:
external_tenant_tokens = self._external_tenant_token_retriever()
external_tenant_tokens = self._external_tenant_token_retriever(sdk_resource)
except CLIError as err:
if in_cloud_console():
AdalAuthentication._log_hostname()
@ -60,7 +66,16 @@ class AdalAuthentication(Authentication): # pylint: disable=too-few-public-meth
# This method is exposed for Azure Core.
def get_token(self, *scopes, **kwargs): # pylint:disable=unused-argument
_, token, full_token, _ = self._get_token()
logger.debug("AdalAuthentication.get_token invoked by Track 2 SDK with scopes=%s", scopes)
# Deal with an old Track 2 SDK issue where the default credential_scopes is extended with
# custom credential_scopes. Instead, credential_scopes should be replaced by custom credential_scopes.
# https://github.com/Azure/azure-sdk-for-python/issues/12947
# We simply remove the first one if there are multiple scopes provided.
if len(scopes) > 1:
scopes = scopes[1:]
_, token, full_token, _ = self._get_token(scopes_to_resource(scopes))
try:
return AccessToken(token, int(full_token['expiresIn'] + time.time()))
except KeyError: # needed to deal with differing unserialized MSI token payload
@ -68,6 +83,7 @@ class AdalAuthentication(Authentication): # pylint: disable=too-few-public-meth
# This method is exposed for msrest.
def signed_session(self, session=None): # pylint: disable=arguments-differ
logger.debug("AdalAuthentication.signed_session invoked by Track 1 SDK")
session = session or super(AdalAuthentication, self).signed_session()
scheme, token, _, external_tenant_tokens = self._get_token()
@ -82,8 +98,6 @@ class AdalAuthentication(Authentication): # pylint: disable=too-few-public-meth
@staticmethod
def _log_hostname():
import socket
from knack.log import get_logger
logger = get_logger(__name__)
logger.warning("A Cloud Shell credential problem occurred. When you report the issue with the error "
"below, please mention the hostname '%s'", socket.gethostname())
@ -91,13 +105,13 @@ class AdalAuthentication(Authentication): # pylint: disable=too-few-public-meth
class MSIAuthenticationWrapper(MSIAuthentication):
# This method is exposed for Azure Core. Add *scopes, **kwargs to fit azure.core requirement
def get_token(self, *scopes, **kwargs): # pylint:disable=unused-argument
logger.debug("MSIAuthenticationWrapper.get_token invoked by Track 2 SDK with scopes=%s", scopes)
self.resource = scopes_to_resource(scopes)
self.set_token()
return AccessToken(self.token['access_token'], int(self.token['expires_on']))
def set_token(self):
import traceback
from knack.log import get_logger
logger = get_logger(__name__)
from azure.cli.core.azclierror import AzureConnectionError, AzureResponseError
try:
super(MSIAuthenticationWrapper, self).set_token()
@ -117,3 +131,7 @@ class MSIAuthenticationWrapper(MSIAuthentication):
traceback.format_exc())
raise AzureConnectionError('MSI endpoint is not responding. Please make sure MSI is configured correctly.\n'
'Error detail: {}'.format(str(err)))
def signed_session(self, session=None):
logger.debug("MSIAuthenticationWrapper.signed_session invoked by Track 1 SDK")
super().signed_session(session)

Просмотреть файл

@ -150,6 +150,7 @@ def _get_mgmt_service_client(cli_ctx,
aux_tenants=None,
**kwargs):
from azure.cli.core._profile import Profile
from azure.cli.core.util import resource_to_scopes
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)
@ -169,6 +170,7 @@ def _get_mgmt_service_client(cli_ctx,
if is_track2(client_type):
client_kwargs.update(configure_common_settings_track2(cli_ctx))
client_kwargs['credential_scopes'] = resource_to_scopes(resource)
if subscription_bound:
client = client_type(cred, subscription_id, **client_kwargs)

Просмотреть файл

@ -377,6 +377,29 @@ class TestUtils(unittest.TestCase):
request = send_mock.call_args.args[1]
self.assertEqual(request.headers['User-Agent'], get_az_rest_user_agent() + ' env-ua ARG-UA')
def test_scopes_to_resource(self):
from azure.cli.core.util import scopes_to_resource
# scopes as a list
self.assertEqual(scopes_to_resource(['https://management.core.windows.net/.default']),
'https://management.core.windows.net/')
# scopes as a tuple
self.assertEqual(scopes_to_resource(('https://storage.azure.com/.default',)),
'https://storage.azure.com/')
# Double slashes are reduced
self.assertEqual(scopes_to_resource(['https://datalake.azure.net//.default']),
'https://datalake.azure.net/')
def test_resource_to_scopes(self):
from azure.cli.core.util import resource_to_scopes
# resource converted to a scopes list
self.assertEqual(resource_to_scopes('https://management.core.windows.net/'),
['https://management.core.windows.net/.default'])
# Use double slashes for certain services
self.assertEqual(resource_to_scopes('https://datalake.azure.net/'),
['https://datalake.azure.net//.default'])
class TestBase64ToHex(unittest.TestCase):

Просмотреть файл

@ -1165,3 +1165,36 @@ def handle_version_update():
refresh_known_clouds()
except Exception as ex: # pylint: disable=broad-except
logger.warning(ex)
def resource_to_scopes(resource):
"""Convert the ADAL resource ID to MSAL scopes by appending the /.default suffix and return a list.
For example: 'https://management.core.windows.net/' -> ['https://management.core.windows.net/.default']
:param resource: The ADAL resource ID
:return: A list of scopes
"""
if 'datalake' in resource or 'batch' in resource or 'database' in resource:
# For datalake, batch and database, the slash must be doubled due to service issue, like
# https://datalake.azure.net//.default
# TODO: This should be fixed on the service side.
scope = resource + '/.default'
else:
scope = resource.rstrip('/') + '/.default'
return [scope]
def scopes_to_resource(scopes):
"""Convert MSAL scopes to ADAL resource by stripping the /.default suffix and return a str.
For example: ['https://management.core.windows.net/.default'] -> 'https://management.core.windows.net/'
:param scopes: The MSAL scopes. It can be a list or tuple of string
:return: The ADAL resource
:rtype: str
"""
scope = scopes[0]
if scope.endswith(".default"):
scope = scope[:-len(".default")]
# Trim extra ending slashes. https://datalake.azure.net// -> https://datalake.azure.net/
scope = scope.rstrip('/') + '/'
return scope

Просмотреть файл

@ -129,8 +129,7 @@ def validate_client_parameters(cmd, namespace):
if is_storagev2(prefix):
from azure.cli.core._profile import Profile
profile = Profile(cli_ctx=cmd.cli_ctx)
n.token_credential, _, _ = profile.get_login_credentials(
resource="https://storage.azure.com", subscription_id=n._subscription)
n.token_credential, _, _ = profile.get_login_credentials(subscription_id=n._subscription)
# Otherwise, we will assume it is in track1 and keep previous token updater
else:
n.token_credential = _create_token_credential(cmd.cli_ctx)

Просмотреть файл

@ -128,8 +128,7 @@ def validate_client_parameters(cmd, namespace):
if is_storagev2(prefix):
from azure.cli.core._profile import Profile
profile = Profile(cli_ctx=cmd.cli_ctx)
n.token_credential, _, _ = profile.get_login_credentials(
resource="https://storage.azure.com", subscription_id=n._subscription)
n.token_credential, _, _ = profile.get_login_credentials(subscription_id=n._subscription)
# Otherwise, we will assume it is in track1 and keep previous token updater
else:
n.token_credential = _create_token_credential(cmd.cli_ctx)