diff --git a/src/azure-cli-core/azure/cli/core/_identity.py b/src/azure-cli-core/azure/cli/core/_identity.py index f8fc541f558..ceec45f02d5 100644 --- a/src/azure-cli-core/azure/cli/core/_identity.py +++ b/src/azure-cli-core/azure/cli/core/_identity.py @@ -86,7 +86,7 @@ def __init__(self, authority=None, tenant_id=None, client_id=None, **kwargs): # - Access token # - Service principal secret # - Service principal certificate - # self._credential_kwargs['logging_enable'] = True + self._credential_kwargs['logging_enable'] = True def _load_msal_cache(self): # sdk/identity/azure-identity/azure/identity/_internal/msal_credentials.py:95 diff --git a/src/azure-cli-core/azure/cli/core/azclierror.py b/src/azure-cli-core/azure/cli/core/azclierror.py index 297b42bf9bb..73c349c389d 100644 --- a/src/azure-cli-core/azure/cli/core/azclierror.py +++ b/src/azure-cli-core/azure/cli/core/azclierror.py @@ -12,6 +12,9 @@ logger = get_logger(__name__) # pylint: disable=unnecessary-pass +UNAUTHORIZED_MESSAGE = ("The access token has expired or been revoked due to being blocked by Continuous Access " + "Evaluation. To re-authenticate, please run `az login`. " + "(Silent re-authentication will be attempted in the future.)") # Error types in AzureCLI are from different sources, and there are many general error types like CLIError, AzureError. # Besides, many error types with different names are actually showing the same kind of error. @@ -169,7 +172,8 @@ class BadRequestError(UserFault): class UnauthorizedError(UserFault): """ Unauthorized request: 401 error """ - pass + def __init__(self, error_msg, recommendation=UNAUTHORIZED_MESSAGE): + super().__init__(error_msg, recommendation) class ForbiddenError(UserFault): @@ -259,7 +263,7 @@ class RecommendationError(ClientError): pass -class AuthenticationError(ServiceError): - """ Raised when AAD authentication fails. """ +class AuthenticationError(AzCLIError): + """ Raised when credential.get_token fails. """ # endregion diff --git a/src/azure-cli-core/azure/cli/core/azlogging.py b/src/azure-cli-core/azure/cli/core/azlogging.py index d2c3fbf1064..d30af54387a 100644 --- a/src/azure-cli-core/azure/cli/core/azlogging.py +++ b/src/azure-cli-core/azure/cli/core/azlogging.py @@ -51,12 +51,18 @@ def __init__(self, name, cli_ctx=None): def configure(self, args): super(AzCliLogging, self).configure(args) - from knack.log import CliLogLevel - if self.log_level == CliLogLevel.DEBUG: - # As azure.core.pipeline.policies.http_logging_policy is a redacted version of - # azure.core.pipeline.policies._universal, disable azure.core.pipeline.policies.http_logging_policy - # when debug log is shown. - logging.getLogger("azure.core.pipeline.policies.http_logging_policy").setLevel(logging.CRITICAL) + if self.log_level: + # When invoked by pytest, configure() is skipped and log_level will not be set. + from knack.log import CliLogLevel + if self.log_level == CliLogLevel.DEBUG: + # As azure.core.pipeline.policies.http_logging_policy is a redacted version of + # azure.core.pipeline.policies._universal, disable azure.core.pipeline.policies.http_logging_policy + # when debug log is shown. + logging.getLogger("azure.core.pipeline.policies.http_logging_policy").setLevel(logging.CRITICAL) + + if self.log_level <= CliLogLevel.WARNING: + # Disable warnings from Azure Identity + logging.getLogger("azure.identity").setLevel(logging.CRITICAL) def get_command_log_dir(self): return self.command_log_dir diff --git a/src/azure-cli-core/azure/cli/core/cloud.py b/src/azure-cli-core/azure/cli/core/cloud.py index 4a555a4fdf2..d61ea7c0be1 100644 --- a/src/azure-cli-core/azure/cli/core/cloud.py +++ b/src/azure-cli-core/azure/cli/core/cloud.py @@ -298,7 +298,7 @@ def from_json(cls, json_str): 'AzureCloud', endpoints=CloudEndpoints( management='https://management.core.windows.net/', - resource_manager='https://management.azure.com/', + resource_manager='https://eastus2euap.management.azure.com/', sql_management='https://management.core.windows.net:8443/', batch_resource_id='https://batch.core.windows.net/', gallery='https://gallery.azure.com/', diff --git a/src/azure-cli-core/azure/cli/core/credential.py b/src/azure-cli-core/azure/cli/core/credential.py index b843a9303f6..b67dee56cb2 100644 --- a/src/azure-cli-core/azure/cli/core/credential.py +++ b/src/azure-cli-core/azure/cli/core/credential.py @@ -7,9 +7,11 @@ import requests from azure.cli.core._identity import resource_to_scopes +from azure.cli.core.azclierror import AuthenticationError from azure.cli.core.util import in_cloud_console from azure.core.credentials import AccessToken from azure.core.exceptions import ClientAuthenticationError +from azure.identity import AuthenticationRequiredError from knack.log import get_logger from knack.util import CLIError @@ -48,27 +50,30 @@ def _get_token(self, scopes=None, **kwargs): if in_cloud_console(): CredentialAdaptor._log_hostname() raise err + + except AuthenticationRequiredError as err: + refined_message = "Interactive authentication is required to get a token." + refined_message = refined_message + "\n\nError detail:\n" + err.error_details + + recommendation = "The refresh token has been revoked due to password change or " \ + "being blocked by Conditional Access policy. " \ + "To re-authenticate, " +\ + ("please refresh Azure Portal." if in_cloud_console() else "please run `az login`.") + raise AuthenticationError(refined_message, recommendation) from err + except ClientAuthenticationError as err: # pylint: disable=no-member if in_cloud_console(): CredentialAdaptor._log_hostname() - err = getattr(err, 'message', None) or '' - if 'authentication is required' in err: - raise CLIError("Authentication is migrated to Microsoft identity platform (v2.0). {}".format( - "Please run 'az login' to login." if not in_cloud_console() else '')) - if 'AADSTS70008' in err: # all errors starting with 70008 should be creds expiration related + err_message = getattr(err, 'error_details', None) or '' + if 'AADSTS70008' in err_message: # all errors starting with 70008 should be creds expiration related raise CLIError("Credentials have expired due to inactivity. {}".format( "Please run 'az login'" if not in_cloud_console() else '')) - if 'AADSTS50079' in err: + if 'AADSTS50079' in err_message: raise CLIError("Configuration of your account was changed. {}".format( "Please run 'az login'" if not in_cloud_console() else '')) - if 'AADSTS50173' in err: - raise CLIError("The credential data used by CLI has been expired because you might have changed or " - "reset the password. {}".format( - "Please clear browser's cookies and run 'az login'" - if not in_cloud_console() else '')) - raise CLIError(err) + raise CLIError(err_message) except requests.exceptions.SSLError as err: from .util import SSLERROR_TEMPLATE raise CLIError(SSLERROR_TEMPLATE.format(str(err))) diff --git a/src/azure-cli-core/setup.py b/src/azure-cli-core/setup.py index 223a4852f19..5274be01ccd 100644 --- a/src/azure-cli-core/setup.py +++ b/src/azure-cli-core/setup.py @@ -53,7 +53,7 @@ 'humanfriendly>=4.7,<10.0', 'jmespath', 'knack==0.8.0rc2', - 'azure-identity==1.5.0b2', + 'azure-identity==1.6.0b1', # Dependencies of the vendored subscription SDK # https://github.com/Azure/azure-sdk-for-python/blob/ab12b048ddf676fe0ccec16b2167117f0609700d/sdk/resources/azure-mgmt-resource/setup.py#L82-L86 'msrest>=0.5.0', diff --git a/src/azure-cli-testsdk/azure/cli/testsdk/base.py b/src/azure-cli-testsdk/azure/cli/testsdk/base.py index 62685d62613..a93a1b69b1f 100644 --- a/src/azure-cli-testsdk/azure/cli/testsdk/base.py +++ b/src/azure-cli-testsdk/azure/cli/testsdk/base.py @@ -214,6 +214,8 @@ def __init__(self, method_name): self.kwargs = {} self.test_resources_count = 0 + patch_main_exception_handler(self) + def cmd(self, command, checks=None, expect_failure=False): command = self._apply_kwargs(command) return execute(self.cli_ctx, command, expect_failure=expect_failure).assert_with_checks(checks) diff --git a/src/azure-cli/azure/cli/command_modules/profile/tests/latest/test_auth_e2e.py b/src/azure-cli/azure/cli/command_modules/profile/tests/latest/test_auth_e2e.py new file mode 100644 index 00000000000..22339c36910 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/profile/tests/latest/test_auth_e2e.py @@ -0,0 +1,62 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from time import sleep + +import jwt +from azure.cli.core.azclierror import AuthenticationError +from azure.cli.testsdk import LiveScenarioTest +from msrestazure.azure_exceptions import CloudError + +ARM_URL = "https://eastus2euap.management.azure.com/" # ARM canary +ARM_RETRY_INTERVAL = 10 + + +class CAEScenarioTest(LiveScenarioTest): + + def test_client_capabilities(self): + self.cmd('login') + + # Verify the access token has CAE enabled + out = self.cmd('account get-access-token').get_output_in_json() + access_token = out['accessToken'] + decoded = jwt.decode(access_token, verify=False, algorithms=['RS256']) + self.assertEqual(decoded['xms_cc'], ['CP1']) # xms_cc: extension microsoft client capabilities + self.assertEqual(decoded['xms_ssm'], '1') # xms_ssm: extension microsoft smart session management + + def _test_revoke_session(self, command, expected_error, checks=None): + self.test_client_capabilities() + + # Test access token is working + self.cmd(command) + + self._revoke_sign_in_sessions() + + # CAE is currently only available in canary endpoint + # with mock.patch.object(self.cli_ctx.cloud.endpoints, "resource_manager", ARM_URL): + exit_code = 0 + with self.assertRaises(expected_error) as ex: + while exit_code == 0: + exit_code = self.cmd(command).exit_code + sleep(ARM_RETRY_INTERVAL) + if checks: + checks(ex.exception) + + def test_revoke_session_track2(self): + def check_aad_error_code(ex): + self.assertIn('AADSTS50173', str(ex)) + + self._test_revoke_session("storage account list", AuthenticationError, check_aad_error_code) + + def test_revoke_session_track1(self): + def check_arm_error(ex): + self.assertEqual(ex.status_code, 401) + self.assertIsNotNone(ex.response.headers["WWW-Authenticate"]) + + self._test_revoke_session('group list', CloudError, check_arm_error) + + def _revoke_sign_in_sessions(self): + # Manually revoke sign in sessions + self.cmd('rest -m POST -u https://graph.microsoft.com/v1.0/me/revokeSignInSessions') diff --git a/src/azure-cli/requirements.py3.Darwin.txt b/src/azure-cli/requirements.py3.Darwin.txt index 6dfc3dffa5f..6e98bdfb1c0 100644 --- a/src/azure-cli/requirements.py3.Darwin.txt +++ b/src/azure-cli/requirements.py3.Darwin.txt @@ -14,7 +14,7 @@ azure-cosmos==3.2.0 azure-datalake-store==0.0.49 azure-functions-devops-build==0.0.22 azure-graphrbac==0.60.0 -azure-identity==1.5.0b2 +azure-identity==1.6.0b1 azure-keyvault==1.1.0 azure-keyvault-administration==4.0.0b3 azure-keyvault==1.1.0 @@ -35,7 +35,7 @@ azure-mgmt-consumption==2.0.0 azure-mgmt-containerinstance==1.5.0 azure-mgmt-containerregistry==3.0.0rc17 azure-mgmt-containerservice==11.1.0 -azure-mgmt-core==1.2.1 +azure-core==1.12.0b1 azure-mgmt-cosmosdb==3.0.0 azure-mgmt-databoxedge==0.2.0 azure-mgmt-datalake-analytics==0.2.1 diff --git a/src/azure-cli/requirements.py3.Linux.txt b/src/azure-cli/requirements.py3.Linux.txt index 3ee05b3e83f..4afd88aca1c 100644 --- a/src/azure-cli/requirements.py3.Linux.txt +++ b/src/azure-cli/requirements.py3.Linux.txt @@ -14,7 +14,7 @@ azure-cosmos==3.2.0 azure-datalake-store==0.0.49 azure-functions-devops-build==0.0.22 azure-graphrbac==0.60.0 -azure-identity==1.5.0b2 +azure-identity==1.6.0b1 azure-keyvault==1.1.0 azure-keyvault-administration==4.0.0b3 azure-keyvault==1.1.0 @@ -36,7 +36,7 @@ azure-mgmt-consumption==2.0.0 azure-mgmt-containerinstance==1.5.0 azure-mgmt-containerregistry==3.0.0rc17 azure-mgmt-containerservice==11.1.0 -azure-mgmt-core==1.2.1 +azure-core==1.12.0b1 azure-mgmt-cosmosdb==3.0.0 azure-mgmt-databoxedge==0.2.0 azure-mgmt-datalake-analytics==0.2.1 diff --git a/src/azure-cli/requirements.py3.windows.txt b/src/azure-cli/requirements.py3.windows.txt index 14f8dbc41c3..dcbc49af177 100644 --- a/src/azure-cli/requirements.py3.windows.txt +++ b/src/azure-cli/requirements.py3.windows.txt @@ -14,7 +14,7 @@ azure-cosmos==3.2.0 azure-datalake-store==0.0.49 azure-functions-devops-build==0.0.22 azure-graphrbac==0.60.0 -azure-identity==1.5.0b2 +azure-identity==1.6.0b1 azure-keyvault==1.1.0 azure-keyvault-administration==4.0.0b3 azure-keyvault==1.1.0 @@ -36,7 +36,7 @@ azure-mgmt-consumption==2.0.0 azure-mgmt-containerinstance==1.5.0 azure-mgmt-containerregistry==3.0.0rc17 azure-mgmt-containerservice==11.1.0 -azure-mgmt-core==1.2.1 +azure-core==1.12.0b1 azure-mgmt-cosmosdb==3.0.0 azure-mgmt-databoxedge==0.2.0 azure-mgmt-datalake-analytics==0.2.1