Skip to content

Commit 68e38c6

Browse files
authored
Raise CredentialUnavailableError when CLI subprocess times out (Azure#18509)
1 parent 83acba7 commit 68e38c6

File tree

4 files changed

+47
-21
lines changed

4 files changed

+47
-21
lines changed

sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77
import os
88
import platform
99
import re
10+
import subprocess
1011
import sys
1112
import time
1213
from typing import TYPE_CHECKING
1314

14-
import subprocess
15+
import six
1516

1617
from azure.core.credentials import AccessToken
1718
from azure.core.exceptions import ClientAuthenticationError
@@ -53,9 +54,7 @@ def get_token(self, *scopes, **kwargs): # pylint:disable=no-self-use,unused-arg
5354
"""
5455

5556
resource = _scopes_to_resource(*scopes)
56-
output, error = _run_command(COMMAND_LINE.format(resource))
57-
if error:
58-
raise error
57+
output = _run_command(COMMAND_LINE.format(resource))
5958

6059
token = parse_token(output)
6160
if not token:
@@ -120,25 +119,25 @@ def _run_command(command):
120119
if platform.python_version() >= "3.3":
121120
kwargs["timeout"] = 10
122121

123-
output = subprocess.check_output(args, **kwargs)
124-
return output, None
122+
return subprocess.check_output(args, **kwargs)
125123
except subprocess.CalledProcessError as ex:
126124
# non-zero return from shell
127125
if ex.returncode == 127 or ex.output.startswith("'az' is not recognized"):
128-
error = CredentialUnavailableError(message=CLI_NOT_FOUND)
129-
elif "az login" in ex.output or "az account set" in ex.output:
130-
error = CredentialUnavailableError(message=NOT_LOGGED_IN)
126+
raise CredentialUnavailableError(message=CLI_NOT_FOUND)
127+
if "az login" in ex.output or "az account set" in ex.output:
128+
raise CredentialUnavailableError(message=NOT_LOGGED_IN)
129+
130+
# return code is from the CLI -> propagate its output
131+
if ex.output:
132+
message = sanitize_output(ex.output)
131133
else:
132-
# return code is from the CLI -> propagate its output
133-
if ex.output:
134-
message = sanitize_output(ex.output)
135-
else:
136-
message = "Failed to invoke Azure CLI"
137-
error = ClientAuthenticationError(message=message)
134+
message = "Failed to invoke Azure CLI"
135+
raise ClientAuthenticationError(message=message)
138136
except OSError as ex:
139137
# failed to execute 'cmd' or '/bin/sh'; CLI may or may not be installed
140138
error = CredentialUnavailableError(message="Failed to execute '{}'".format(args[0]))
139+
six.raise_from(error, ex)
141140
except Exception as ex: # pylint:disable=broad-except
142-
error = ex
143-
144-
return None, error
141+
# could be a timeout, for example
142+
error = CredentialUnavailableError(message="Failed to invoke the Azure CLI")
143+
six.raise_from(error, ex)

sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,15 @@ async def _run_command(command: str) -> str:
8181
cwd=working_directory,
8282
env=dict(os.environ, AZURE_CORE_NO_COLOR="true")
8383
)
84+
stdout, _ = await asyncio.wait_for(proc.communicate(), 10)
85+
output = stdout.decode()
8486
except OSError as ex:
8587
# failed to execute 'cmd' or '/bin/sh'; CLI may or may not be installed
8688
error = CredentialUnavailableError(message="Failed to execute '{}'".format(args[0]))
8789
raise error from ex
88-
89-
stdout, _ = await asyncio.wait_for(proc.communicate(), 10)
90-
output = stdout.decode()
90+
except asyncio.TimeoutError as ex:
91+
proc.kill()
92+
raise CredentialUnavailableError(message="Timed out waiting for Azure CLI") from ex
9193

9294
if proc.returncode == 0:
9395
return output

sdk/identity/azure-identity/tests/test_cli_credential.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# ------------------------------------
55
from datetime import datetime
66
import json
7+
import sys
78

89
from azure.identity import AzureCliCredential, CredentialUnavailableError
910
from azure.identity._credentials.azure_cli import CLI_NOT_FOUND, NOT_LOGGED_IN
@@ -136,3 +137,14 @@ def test_subprocess_error_does_not_expose_token(output):
136137

137138
assert "secret value" not in str(ex.value)
138139
assert "secret value" not in repr(ex.value)
140+
141+
142+
@pytest.mark.skipif(sys.version_info < (3, 3), reason="Python 3.3 added timeout support")
143+
def test_timeout():
144+
"""The credential should raise CredentialUnavailableError when the subprocess times out"""
145+
146+
from subprocess import TimeoutExpired
147+
148+
with mock.patch(CHECK_OUTPUT, mock.Mock(side_effect=TimeoutExpired("", 42))):
149+
with pytest.raises(CredentialUnavailableError):
150+
AzureCliCredential().get_token("scope")

sdk/identity/azure-identity/tests/test_cli_credential_async.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Copyright (c) Microsoft Corporation.
33
# Licensed under the MIT License.
44
# ------------------------------------
5+
import asyncio
56
from datetime import datetime
67
import json
78
import sys
@@ -168,3 +169,15 @@ async def test_subprocess_error_does_not_expose_token(output):
168169

169170
assert "secret value" not in str(ex.value)
170171
assert "secret value" not in repr(ex.value)
172+
173+
174+
async def test_timeout():
175+
"""The credential should kill the subprocess after a timeout"""
176+
177+
proc = mock.Mock(communicate=mock.Mock(side_effect=asyncio.TimeoutError), returncode=None)
178+
with mock.patch(SUBPROCESS_EXEC, mock.Mock(return_value=get_completed_future(proc))):
179+
with pytest.raises(CredentialUnavailableError):
180+
await AzureCliCredential().get_token("scope")
181+
182+
assert proc.communicate.call_count == 1
183+
assert proc.kill.call_count == 1

0 commit comments

Comments
 (0)