diff --git a/scripts/release/debian/build.sh b/scripts/release/debian/build.sh index f2734764880..d7105ae10eb 100755 --- a/scripts/release/debian/build.sh +++ b/scripts/release/debian/build.sh @@ -23,6 +23,9 @@ apt-get update # uuid-dev is used to build _uuid module: https://github.com/python/cpython/pull/3796 apt-get install -y libssl-dev libffi-dev python3-dev debhelper zlib1g-dev uuid-dev apt-get install -y wget +# Git is not strictly necessary, but it would allow building an experimental package +# with dependency which is currently only available in its git repo feature branch. +apt-get install -y git # Download Python source code PYTHON_SRC_DIR=$(mktemp -d) diff --git a/src/azure-cli-core/azure/cli/core/auth/adal_authentication.py b/src/azure-cli-core/azure/cli/core/auth/adal_authentication.py index 9cc060bbcf8..d37a85ec99b 100644 --- a/src/azure-cli-core/azure/cli/core/auth/adal_authentication.py +++ b/src/azure-cli-core/azure/cli/core/auth/adal_authentication.py @@ -16,12 +16,29 @@ class MSIAuthenticationWrapper(MSIAuthentication): # This method is exposed for Azure Core. Add *scopes, **kwargs to fit azure.core requirement # pylint: disable=line-too-long def get_token(self, *scopes, **kwargs): # pylint:disable=unused-argument - logger.debug("MSIAuthenticationWrapper.get_token invoked by Track 2 SDK with scopes=%s", scopes) + logger.debug("MSIAuthenticationWrapper.get_token: scopes=%r, kwargs=%r", scopes, kwargs) if 'data' in kwargs: + from azure.cli.core.util import in_cloud_console + if in_cloud_console(): + # Use MSAL to get VM SSH certificate + import msal + from .util import check_result, build_sdk_access_token + from .identity import AZURE_CLI_CLIENT_ID + app = msal.PublicClientApplication( + AZURE_CLI_CLIENT_ID, # Use a real client_id, so that cache would work + # TODO: This PoC does not currently maintain a token cache; + # Ideally we should reuse the real MSAL app object which has cache configured. + # token_cache=..., + ) + result = app.acquire_token_interactive(list(scopes), prompt="none", data=kwargs["data"]) + check_result(result, scopes=scopes) + return build_sdk_access_token(result) + from azure.cli.core.azclierror import AuthenticationError - raise AuthenticationError("VM SSH currently doesn't support managed identity or Cloud Shell.") + raise AuthenticationError("VM SSH currently doesn't support managed identity.") + # Use msrestazure to get access token resource = scopes_to_resource(_normalize_scopes(scopes)) if resource: # If available, use resource provided by SDK @@ -55,7 +72,7 @@ def set_token(self): import traceback from azure.cli.core.azclierror import AzureConnectionError, AzureResponseError try: - super(MSIAuthenticationWrapper, self).set_token() + super().set_token() except requests.exceptions.ConnectionError as err: logger.debug('throw requests.exceptions.ConnectionError when doing MSIAuthentication: \n%s', traceback.format_exc()) diff --git a/src/azure-cli-core/azure/cli/core/auth/credential_adaptor.py b/src/azure-cli-core/azure/cli/core/auth/credential_adaptor.py index 59e6ad3a426..782c8d02fea 100644 --- a/src/azure-cli-core/azure/cli/core/auth/credential_adaptor.py +++ b/src/azure-cli-core/azure/cli/core/auth/credential_adaptor.py @@ -44,7 +44,7 @@ def _get_token(self, scopes=None, **kwargs): raise CLIError(SSLERROR_TEMPLATE.format(str(err))) def signed_session(self, session=None): - logger.debug("CredentialAdaptor.get_token") + logger.debug("CredentialAdaptor.signed_session") session = session or requests.Session() token, external_tenant_tokens = self._get_token() header = "{} {}".format('Bearer', token.token) diff --git a/src/azure-cli-core/azure/cli/core/auth/msal_authentication.py b/src/azure-cli-core/azure/cli/core/auth/msal_authentication.py index fe13ee3e4d4..330f83ac412 100644 --- a/src/azure-cli-core/azure/cli/core/auth/msal_authentication.py +++ b/src/azure-cli-core/azure/cli/core/auth/msal_authentication.py @@ -6,7 +6,7 @@ """ Credentials defined in this module are alternative implementations of credentials provided by Azure Identity. -These credentials implements azure.core.credentials.TokenCredential by exposing get_token method for Track 2 +These credentials implement azure.core.credentials.TokenCredential by exposing get_token method for Track 2 SDK invocation. """ @@ -14,7 +14,7 @@ from knack.util import CLIError from msal import PublicClientApplication, ConfidentialClientApplication -from .util import check_result, AccessToken +from .util import check_result, build_sdk_access_token # OAuth 2.0 client credentials flow parameter # https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow @@ -87,7 +87,7 @@ def get_token(self, *scopes, **kwargs): # launch browser, but show the error message and `az login` command instead. else: raise - return _build_sdk_access_token(result) + return build_sdk_access_token(result) class ServicePrincipalCredential(ConfidentialClientApplication): @@ -130,21 +130,4 @@ def get_token(self, *scopes, **kwargs): if not result: result = self.acquire_token_for_client(scopes, **kwargs) check_result(result) - return _build_sdk_access_token(result) - - -def _build_sdk_access_token(token_entry): - import time - request_time = int(time.time()) - - # MSAL token entry sample: - # { - # 'access_token': 'eyJ0eXAiOiJKV...', - # 'token_type': 'Bearer', - # 'expires_in': 1618 - # } - - # Importing azure.core.credentials.AccessToken is expensive. - # This can slow down commands that doesn't need azure.core, like `az account get-access-token`. - # So We define our own AccessToken. - return AccessToken(token_entry["access_token"], request_time + token_entry["expires_in"]) + return build_sdk_access_token(result) diff --git a/src/azure-cli-core/azure/cli/core/auth/util.py b/src/azure-cli-core/azure/cli/core/auth/util.py index 0186cdc3ad2..53030e7bee8 100644 --- a/src/azure-cli-core/azure/cli/core/auth/util.py +++ b/src/azure-cli-core/azure/cli/core/auth/util.py @@ -20,17 +20,27 @@ def aad_error_handler(error, **kwargs): # https://docs.microsoft.com/en-us/azure/active-directory/develop/reference-aadsts-error-codes # Search for an error code at https://login.microsoftonline.com/error + # To trigger this function for testing, simply provide an invalid scope: + # az account get-access-token --scope https://my-invalid-scope + from azure.cli.core.util import in_cloud_console if in_cloud_console(): import socket logger.warning("A Cloud Shell credential problem occurred. When you report the issue with the error " "below, please mention the hostname '%s'", socket.gethostname()) - msg = error.get('error_description') - login_message = _generate_login_message(**kwargs) + error_description = error.get('error_description') + + # Build recommendation message + login_command = _generate_login_command(**kwargs) + login_message = ( + # Cloud Shell uses IMDS-like interface for implicit login. If getting token/cert failed, + # we let the user explicitly log in to AAD with MSAL. + "Please explicitly log in with:\n{}" if error.get('error') == 'broker_error' + else "To re-authenticate, please run:\n{}").format(login_command) from azure.cli.core.azclierror import AuthenticationError - raise AuthenticationError(msg, recommendation=login_message) + raise AuthenticationError(error_description, recommendation=login_message) def _generate_login_command(scopes=None): @@ -43,16 +53,6 @@ def _generate_login_command(scopes=None): return ' '.join(login_command) -def _generate_login_message(**kwargs): - from azure.cli.core.util import in_cloud_console - login_command = _generate_login_command(**kwargs) - - msg = "To re-authenticate, please {}" .format( - "refresh Azure Portal." if in_cloud_console() else "run:\n{}".format(login_command)) - - return msg - - def resource_to_scopes(resource): """Convert the ADAL resource ID to MSAL scopes by appending the /.default suffix and return a list. For example: @@ -139,6 +139,23 @@ def check_result(result, **kwargs): return None +def build_sdk_access_token(token_entry): + import time + request_time = int(time.time()) + + # MSAL token entry sample: + # { + # 'access_token': 'eyJ0eXAiOiJKV...', + # 'token_type': 'Bearer', + # 'expires_in': 1618 + # } + + # Importing azure.core.credentials.AccessToken is expensive. + # This can slow down commands that doesn't need azure.core, like `az account get-access-token`. + # So We define our own AccessToken. + return AccessToken(token_entry["access_token"], request_time + token_entry["expires_in"]) + + def decode_access_token(access_token): # Decode the access token. We can do the same with https://jwt.ms from msal.oauth2cli.oidc import decode_part diff --git a/src/azure-cli-core/setup.py b/src/azure-cli-core/setup.py index 354d39b92ea..e54f09f91f3 100644 --- a/src/azure-cli-core/setup.py +++ b/src/azure-cli-core/setup.py @@ -52,7 +52,7 @@ 'jmespath', 'knack~=0.9.0', 'msal-extensions~=1.0.0', - 'msal>=1.17.0,<2.0.0', + 'msal==1.18.0b1', 'msrestazure~=0.6.4', 'packaging>=20.9,<22.0', 'paramiko>=2.0.8,<3.0.0', diff --git a/src/azure-cli/requirements.py3.Darwin.txt b/src/azure-cli/requirements.py3.Darwin.txt index ab6dddacfce..b9a9f252998 100644 --- a/src/azure-cli/requirements.py3.Darwin.txt +++ b/src/azure-cli/requirements.py3.Darwin.txt @@ -110,7 +110,7 @@ jsondiff==2.0.0 knack==0.9.0 MarkupSafe==1.1.1 msal-extensions==1.0.0 -msal==1.17.0 +msal==1.18.0b1 msrest==0.6.21 msrestazure==0.6.4 oauthlib==3.0.1 diff --git a/src/azure-cli/requirements.py3.Linux.txt b/src/azure-cli/requirements.py3.Linux.txt index e03ed08059a..bba19c7f394 100644 --- a/src/azure-cli/requirements.py3.Linux.txt +++ b/src/azure-cli/requirements.py3.Linux.txt @@ -111,7 +111,7 @@ jsondiff==2.0.0 knack==0.9.0 MarkupSafe==1.1.1 msal-extensions==1.0.0 -msal==1.17.0 +msal==1.18.0b1 msrest==0.6.21 msrestazure==0.6.4 oauthlib==3.0.1 diff --git a/src/azure-cli/requirements.py3.windows.txt b/src/azure-cli/requirements.py3.windows.txt index 4dd1aa4509a..3caaea3eaad 100644 --- a/src/azure-cli/requirements.py3.windows.txt +++ b/src/azure-cli/requirements.py3.windows.txt @@ -110,7 +110,7 @@ jsondiff==2.0.0 knack==0.9.0 MarkupSafe==1.1.1 msal-extensions==1.0.0 -msal==1.17.0 +msal==1.18.0b1 msrest==0.6.21 msrestazure==0.6.4 oauthlib==3.0.1