Skip to content
Draft
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
25 changes: 24 additions & 1 deletion src/azure-cli-core/azure/cli/core/_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -826,7 +826,30 @@ def find_using_common_tenant(self, username, credential=None):

client = self._create_subscription_client(credential)
# https://learn.microsoft.com/en-us/rest/api/resources/tenants/list
tenants = client.tenants.list()
from azure.core.exceptions import DecodeError, HttpResponseError
from azure.cli.core.azclierror import AzureResponseError
try:
# Eagerly materialise tenants here so any HTTP errors (e.g. firewall blocking the
# request with a non-JSON response) are caught below before iteration begins.
# Tenant counts are typically small so materialising up-front is not a concern.
tenants = list(client.tenants.list())
except (DecodeError, HttpResponseError) as ex:
if isinstance(ex, DecodeError):
raise AzureResponseError(
"Failed to retrieve tenants. The response from the server could not be parsed. "
"This may be caused by a network firewall or proxy returning an unexpected response. "
"Please check your network settings and try again, or use "
"'az login --tenant TENANT_ID' to log in to a specific tenant."
) from ex
# HttpResponseError (but not DecodeError)
if ex.status_code == 403:
raise AzureResponseError(
"Failed to retrieve tenants. The request was blocked (HTTP 403 Forbidden). "
"This may be caused by a network firewall, proxy, or Conditional Access policy. "
"Please check your network settings and try again, or use "
"'az login --tenant TENANT_ID' to log in to a specific tenant."
) from ex
raise

for t in tenants:
tenant_id = t.tenant_id
Expand Down
56 changes: 56 additions & 0 deletions src/azure-cli-core/azure/cli/core/tests/test_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,62 @@ def test_login_no_subscription_raises_error(self, can_launch_browser_mock,
profile.login(True, None, None, False, None, use_device_code=False,
allow_no_subscriptions=False)

@mock.patch('azure.cli.core._profile.SubscriptionFinder._create_subscription_client', autospec=True)
@mock.patch('azure.cli.core.auth.identity.Identity.get_user_credential', autospec=True)
@mock.patch('azure.cli.core.auth.identity.Identity.login_with_auth_code', autospec=True)
@mock.patch('azure.cli.core._profile.can_launch_browser', autospec=True, return_value=True)
def test_login_tenant_list_403_raises_friendly_error(self, can_launch_browser_mock,
login_with_auth_code_mock, get_user_credential_mock,
create_subscription_client_mock):
"""When listing tenants returns 403 (e.g. network block), a friendly AzureResponseError is raised."""
from azure.core.exceptions import HttpResponseError
from azure.cli.core.azclierror import AzureResponseError
login_with_auth_code_mock.return_value = self.user_identity_mock

cli = DummyCli()
mock_subscription_client = mock.MagicMock()
http_response_mock = mock.MagicMock()
http_response_mock.status_code = 403
http_error = HttpResponseError(response=http_response_mock)
mock_subscription_client.tenants.list.side_effect = http_error
create_subscription_client_mock.return_value = mock_subscription_client

storage_mock = {'subscriptions': None}
profile = Profile(cli_ctx=cli, storage=storage_mock)

with self.assertRaises(AzureResponseError) as cm:
profile.login(True, None, None, False, None, use_device_code=False, allow_no_subscriptions=False)
self.assertIn("403", str(cm.exception))

@mock.patch('azure.cli.core._profile.SubscriptionFinder._create_subscription_client', autospec=True)
@mock.patch('azure.cli.core.auth.identity.Identity.get_user_credential', autospec=True)
@mock.patch('azure.cli.core.auth.identity.Identity.login_with_auth_code', autospec=True)
@mock.patch('azure.cli.core._profile.can_launch_browser', autospec=True, return_value=True)
def test_login_tenant_list_decode_error_raises_friendly_error(self, can_launch_browser_mock,
login_with_auth_code_mock,
get_user_credential_mock,
create_subscription_client_mock):
"""When listing tenants returns a non-JSON response (e.g. HTML 403 block page), a friendly error is raised."""
from azure.core.exceptions import DecodeError
from azure.cli.core.azclierror import AzureResponseError
login_with_auth_code_mock.return_value = self.user_identity_mock

cli = DummyCli()
mock_subscription_client = mock.MagicMock()
mock_subscription_client.tenants.list.side_effect = DecodeError(
message="JSON is invalid: Expecting value: line 1 column 1 (char 0)",
response=mock.MagicMock(),
error=None
)
create_subscription_client_mock.return_value = mock_subscription_client

storage_mock = {'subscriptions': None}
profile = Profile(cli_ctx=cli, storage=storage_mock)

with self.assertRaises(AzureResponseError) as cm:
profile.login(True, None, None, False, None, use_device_code=False, allow_no_subscriptions=False)
self.assertIn("could not be parsed", str(cm.exception))

@mock.patch('azure.cli.core._profile.SubscriptionFinder._create_subscription_client', autospec=True)
@mock.patch('azure.cli.core.auth.identity.Identity.get_user_credential', autospec=True)
@mock.patch('azure.cli.core.auth.identity.Identity.login_with_auth_code', autospec=True)
Expand Down
Loading