Skip to content

Commit 419943d

Browse files
authored
[Identity] Allow configurable process timeouts (#28290)
AzureCliCredential, AzureDeveloperCliCredential, and AzurePowerShellCredential now allow users to pass in a custom timeout. This addresses scenarios where these proceses can take longer than the current default timeout values. DefaultAzureCredential now also has an optional keyword argument to allow users to pass in timeout values to the underlying developer credentials. Signed-off-by: Paul Van Eck <paulvaneck@microsoft.com>
1 parent 338c5bb commit 419943d

14 files changed

Lines changed: 155 additions & 45 deletions

File tree

sdk/identity/azure-identity/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
### Features Added
66

7-
- Added proactive refreshing feature for managed iedentity
7+
- Added proactive refreshing feature for managed identity
8+
- Credentials that are implemented via launching a subprocess to acquire tokens now have configurable timeouts using the `process_timeout` keyword argument. This addresses scenarios where these proceses can take longer than the current default timeout values. The affected credentials are `AzureCliCredential`, `AzureDeveloperCliCredential`, and `AzurePowerShellCredential`. (Note: For `DefaultAzureCredential`, the `developer_credential_timeout` keyword argument allows users to propagate this option to `AzureCliCredential`, `AzureDeveloperCliCredential`, and `AzurePowerShellCredential` in the authentication chain.) ([#28290](https://github.com/Azure/azure-sdk-for-python/pull/28290))
89

910
### Breaking Changes
1011

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

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import shutil
1111
import subprocess
1212
import sys
13-
from typing import Any, List, Optional
13+
from typing import Any, Dict, List, Optional
1414
import six
1515

1616
from azure.core.credentials import AccessToken
@@ -37,12 +37,21 @@ class AzureDeveloperCliCredential:
3737
:keyword List[str] additionally_allowed_tenants: Specifies tenants in addition to the specified "tenant_id"
3838
for which the credential may acquire tokens. Add the wildcard value "*" to allow the credential to
3939
acquire tokens for any tenant the application can access.
40+
:keyword int process_timeout: Seconds to wait for the Azure Developer CLI process to respond. Defaults
41+
to 10 seconds.
4042
"""
4143

42-
def __init__(self, *, tenant_id: str = "", additionally_allowed_tenants: Optional[List[str]] = None):
44+
def __init__(
45+
self,
46+
*,
47+
tenant_id: str = "",
48+
additionally_allowed_tenants: Optional[List[str]] = None,
49+
process_timeout: int = 10
50+
) -> None:
4351

4452
self.tenant_id = tenant_id
4553
self._additionally_allowed_tenants = additionally_allowed_tenants or []
54+
self._process_timeout = process_timeout
4655

4756
def __enter__(self) -> "AzureDeveloperCliCredential":
4857
return self
@@ -83,7 +92,7 @@ def get_token(self, *scopes: str, **kwargs: Any) -> AccessToken:
8392
)
8493
if tenant:
8594
command += " --tenant-id " + tenant
86-
output = _run_command(command)
95+
output = _run_command(command, self._process_timeout)
8796

8897
token = parse_token(output)
8998
if not token:
@@ -130,7 +139,7 @@ def sanitize_output(output):
130139
return re.sub(r"\"token\": \"(.*?)(\"|$)", "****", output)
131140

132141

133-
def _run_command(command):
142+
def _run_command(command: str, timeout: int) -> str:
134143
# Ensure executable exists in PATH first. This avoids a subprocess call that would fail anyway.
135144
if shutil.which(EXECUTABLE_NAME) is None:
136145
raise CredentialUnavailableError(message=CLI_NOT_FOUND)
@@ -142,12 +151,12 @@ def _run_command(command):
142151
try:
143152
working_directory = get_safe_working_dir()
144153

145-
kwargs = {
154+
kwargs: Dict[str, Any] = {
146155
"stderr": subprocess.PIPE,
147156
"cwd": working_directory,
148157
"universal_newlines": True,
149158
"env": dict(os.environ, NO_COLOR="true"),
150-
"timeout": 10,
159+
"timeout": timeout,
151160
}
152161

153162
return subprocess.check_output(args, **kwargs)

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

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import subprocess
1111
import sys
1212
import time
13-
from typing import List, Optional, Any
13+
from typing import List, Optional, Any, Dict
1414
import six
1515

1616
from azure.core.credentials import AccessToken
@@ -36,16 +36,19 @@ class AzureCliCredential:
3636
:keyword List[str] additionally_allowed_tenants: Specifies tenants in addition to the specified "tenant_id"
3737
for which the credential may acquire tokens. Add the wildcard value "*" to allow the credential to
3838
acquire tokens for any tenant the application can access.
39+
:keyword int process_timeout: Seconds to wait for the Azure CLI process to respond. Defaults to 10 seconds.
3940
"""
4041
def __init__(
4142
self,
4243
*,
4344
tenant_id: str = "",
44-
additionally_allowed_tenants: Optional[List[str]] = None
45+
additionally_allowed_tenants: Optional[List[str]] = None,
46+
process_timeout: int = 10
4547
) -> None:
4648

4749
self.tenant_id = tenant_id
4850
self._additionally_allowed_tenants = additionally_allowed_tenants or []
51+
self._process_timeout = process_timeout
4952

5053
def __enter__(self):
5154
return self
@@ -84,7 +87,7 @@ def get_token(self, *scopes: str, **kwargs: Any) -> AccessToken:
8487
)
8588
if tenant:
8689
command += " --tenant " + tenant
87-
output = _run_command(command)
90+
output = _run_command(command, self._process_timeout)
8891

8992
token = parse_token(output)
9093
if not token:
@@ -135,7 +138,7 @@ def sanitize_output(output: str) -> str:
135138
return re.sub(r"\"accessToken\": \"(.*?)(\"|$)", "****", output)
136139

137140

138-
def _run_command(command):
141+
def _run_command(command: str, timeout: int) -> str:
139142
# Ensure executable exists in PATH first. This avoids a subprocess call that would fail anyway.
140143
if shutil.which(EXECUTABLE_NAME) is None:
141144
raise CredentialUnavailableError(message=CLI_NOT_FOUND)
@@ -147,11 +150,11 @@ def _run_command(command):
147150
try:
148151
working_directory = get_safe_working_dir()
149152

150-
kwargs = {
153+
kwargs: Dict[str, Any] = {
151154
"stderr": subprocess.PIPE,
152155
"cwd": working_directory,
153156
"universal_newlines": True,
154-
"timeout": 10,
157+
"timeout": timeout,
155158
"env": dict(os.environ, AZURE_CORE_NO_COLOR="true"),
156159
}
157160
return subprocess.check_output(args, **kwargs)

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
# ------------------------------------
55
import base64
66
import logging
7-
import platform
87
import subprocess
98
import sys
109
from typing import List, Tuple, Optional, Any
@@ -51,16 +50,19 @@ class AzurePowerShellCredential:
5150
:keyword List[str] additionally_allowed_tenants: Specifies tenants in addition to the specified "tenant_id"
5251
for which the credential may acquire tokens. Add the wildcard value "*" to allow the credential to
5352
acquire tokens for any tenant the application can access.
53+
:keyword int process_timeout: Seconds to wait for the Azure PowerShell process to respond. Defaults to 10 seconds.
5454
"""
5555
def __init__(
5656
self,
5757
*,
5858
tenant_id: str = "",
59-
additionally_allowed_tenants: Optional[List[str]] = None
59+
additionally_allowed_tenants: Optional[List[str]] = None,
60+
process_timeout: int = 10
6061
) -> None:
6162

6263
self.tenant_id = tenant_id
6364
self._additionally_allowed_tenants = additionally_allowed_tenants or []
65+
self._process_timeout = process_timeout
6466

6567
def __enter__(self):
6668
return self
@@ -96,17 +98,15 @@ def get_token(self, *scopes: str, **kwargs: Any) -> AccessToken:
9698
**kwargs
9799
)
98100
command_line = get_command_line(scopes, tenant_id)
99-
output = run_command_line(command_line)
101+
output = run_command_line(command_line, self._process_timeout)
100102
token = parse_token(output)
101103
return token
102104

103105

104-
def run_command_line(command_line: List[str]) -> str:
106+
def run_command_line(command_line: List[str], timeout: int) -> str:
105107
stdout = stderr = ""
106108
proc = None
107-
kwargs = {}
108-
if platform.python_version() >= "3.3":
109-
kwargs["timeout"] = 10
109+
kwargs = {"timeout": timeout}
110110

111111
try:
112112
proc = start_process(command_line)

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ class DefaultAzureCredential(ChainedTokenCredential):
8080
:class:`~azure.identity.VisualStudioCodeCredential`. Defaults to the "Azure: Tenant" setting in VS Code's user
8181
settings or, when that setting has no value, the "organizations" tenant, which supports only Azure Active
8282
Directory work or school accounts.
83+
:keyword int developer_credential_timeout: The timeout in seconds to use for developer credentials that run
84+
subprocesses (e.g. AzureCliCredential, AzurePowerShellCredential). Defaults to **10** seconds.
8385
"""
8486

8587
def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statements
@@ -116,6 +118,8 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement
116118
"shared_cache_tenant_id", os.environ.get(EnvironmentVariables.AZURE_TENANT_ID)
117119
)
118120

121+
developer_credential_timeout = kwargs.pop("developer_credential_timeout", 10)
122+
119123
exclude_environment_credential = kwargs.pop("exclude_environment_credential", False)
120124
exclude_managed_identity_credential = kwargs.pop("exclude_managed_identity_credential", False)
121125
exclude_shared_token_cache_credential = kwargs.pop("exclude_shared_token_cache_credential", False)
@@ -138,7 +142,7 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement
138142
if not exclude_managed_identity_credential:
139143
credentials.append(ManagedIdentityCredential(client_id=managed_identity_client_id, **kwargs))
140144
if not exclude_azd_cli_credential:
141-
credentials.append(AzureDeveloperCliCredential())
145+
credentials.append(AzureDeveloperCliCredential(process_timeout=developer_credential_timeout))
142146
if not exclude_shared_token_cache_credential and SharedTokenCacheCredential.supported():
143147
try:
144148
# username and/or tenant_id are only required when the cache contains tokens for multiple identities
@@ -151,9 +155,9 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement
151155
if not exclude_visual_studio_code_credential:
152156
credentials.append(VisualStudioCodeCredential(**vscode_args))
153157
if not exclude_cli_credential:
154-
credentials.append(AzureCliCredential())
158+
credentials.append(AzureCliCredential(process_timeout=developer_credential_timeout))
155159
if not exclude_powershell_credential:
156-
credentials.append(AzurePowerShellCredential())
160+
credentials.append(AzurePowerShellCredential(process_timeout=developer_credential_timeout))
157161
if not exclude_interactive_browser_credential:
158162
if interactive_browser_client_id:
159163
credentials.append(

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

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,21 @@ class AzureDeveloperCliCredential(AsyncContextManager):
3535
:keyword List[str] additionally_allowed_tenants: Specifies tenants in addition to the specified "tenant_id"
3636
for which the credential may acquire tokens. Add the wildcard value "*" to allow the credential to
3737
acquire tokens for any tenant the application can access.
38+
:keyword int process_timeout: Seconds to wait for the Azure Developer CLI process to respond. Defaults
39+
to 10 seconds.
3840
"""
3941

40-
def __init__(self, *, tenant_id: str = "", additionally_allowed_tenants: Optional[List[str]] = None):
42+
def __init__(
43+
self,
44+
*,
45+
tenant_id: str = "",
46+
additionally_allowed_tenants: Optional[List[str]] = None,
47+
process_timeout: int = 10
48+
) -> None:
4149

4250
self.tenant_id = tenant_id
4351
self._additionally_allowed_tenants = additionally_allowed_tenants or []
52+
self._process_timeout = process_timeout
4453

4554
@log_get_token_async
4655
async def get_token(self, *scopes: str, **kwargs: Any) -> AccessToken:
@@ -73,7 +82,7 @@ async def get_token(self, *scopes: str, **kwargs: Any) -> AccessToken:
7382

7483
if tenant:
7584
command += " --tenant-id " + tenant
76-
output = await _run_command(command)
85+
output = await _run_command(command, self._process_timeout)
7786

7887
token = parse_token(output)
7988
if not token:
@@ -88,7 +97,7 @@ async def close(self) -> None:
8897
"""Calling this method is unnecessary"""
8998

9099

91-
async def _run_command(command: str) -> str:
100+
async def _run_command(command: str, timeout: int) -> str:
92101
# Ensure executable exists in PATH first. This avoids a subprocess call that would fail anyway.
93102
if shutil.which(EXECUTABLE_NAME) is None:
94103
raise CredentialUnavailableError(message=CLI_NOT_FOUND)
@@ -108,7 +117,7 @@ async def _run_command(command: str) -> str:
108117
cwd=working_directory,
109118
env=dict(os.environ, NO_COLOR="true")
110119
)
111-
stdout_b, stderr_b = await asyncio.wait_for(proc.communicate(), 10)
120+
stdout_b, stderr_b = await asyncio.wait_for(proc.communicate(), timeout)
112121
output = stdout_b.decode()
113122
stderr = stderr_b.decode()
114123
except OSError as ex:

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,19 @@ class AzureCliCredential(AsyncContextManager):
3535
:keyword List[str] additionally_allowed_tenants: Specifies tenants in addition to the specified "tenant_id"
3636
for which the credential may acquire tokens. Add the wildcard value "*" to allow the credential to
3737
acquire tokens for any tenant the application can access.
38+
:keyword int process_timeout: Seconds to wait for the Azure CLI process to respond. Defaults to 10 seconds.
3839
"""
3940
def __init__(
4041
self,
4142
*,
4243
tenant_id: str = "",
43-
additionally_allowed_tenants: Optional[List[str]] = None
44+
additionally_allowed_tenants: Optional[List[str]] = None,
45+
process_timeout: int = 10
4446
) -> None:
4547

4648
self.tenant_id = tenant_id
4749
self._additionally_allowed_tenants = additionally_allowed_tenants or []
50+
self._process_timeout = process_timeout
4851

4952
@log_get_token_async
5053
async def get_token(self, *scopes: str, **kwargs: Any) -> AccessToken:
@@ -76,7 +79,7 @@ async def get_token(self, *scopes: str, **kwargs: Any) -> AccessToken:
7679

7780
if tenant:
7881
command += " --tenant " + tenant
79-
output = await _run_command(command)
82+
output = await _run_command(command, self._process_timeout)
8083

8184
token = parse_token(output)
8285
if not token:
@@ -92,7 +95,7 @@ async def close(self) -> None:
9295
"""Calling this method is unnecessary"""
9396

9497

95-
async def _run_command(command: str) -> str:
98+
async def _run_command(command: str, timeout: int) -> str:
9699
# Ensure executable exists in PATH first. This avoids a subprocess call that would fail anyway.
97100
if shutil.which(EXECUTABLE_NAME) is None:
98101
raise CredentialUnavailableError(message=CLI_NOT_FOUND)
@@ -112,7 +115,7 @@ async def _run_command(command: str) -> str:
112115
cwd=working_directory,
113116
env=dict(os.environ, AZURE_CORE_NO_COLOR="true")
114117
)
115-
stdout_b, stderr_b = await asyncio.wait_for(proc.communicate(), 10)
118+
stdout_b, stderr_b = await asyncio.wait_for(proc.communicate(), timeout)
116119
output = stdout_b.decode()
117120
stderr = stderr_b.decode()
118121
except OSError as ex:

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,20 @@ class AzurePowerShellCredential(AsyncContextManager):
2929
:keyword List[str] additionally_allowed_tenants: Specifies tenants in addition to the specified "tenant_id"
3030
for which the credential may acquire tokens. Add the wildcard value "*" to allow the credential to
3131
acquire tokens for any tenant the application can access.
32+
:keyword int process_timeout: Seconds to wait for the Azure PowerShell process to respond. Defaults to 10 seconds.
3233
"""
3334

34-
def __init__(self, *, tenant_id: str = "", additionally_allowed_tenants: Optional[List[str]] = None):
35+
def __init__(
36+
self,
37+
*,
38+
tenant_id: str = "",
39+
additionally_allowed_tenants: Optional[List[str]] = None,
40+
process_timeout: int = 10
41+
) -> None:
3542

3643
self.tenant_id = tenant_id
3744
self._additionally_allowed_tenants = additionally_allowed_tenants or []
45+
self._process_timeout = process_timeout
3846

3947
@log_get_token_async
4048
async def get_token(
@@ -65,23 +73,23 @@ async def get_token(
6573
**kwargs
6674
)
6775
command_line = get_command_line(scopes, tenant_id)
68-
output = await run_command_line(command_line)
76+
output = await run_command_line(command_line, self._process_timeout)
6977
token = parse_token(output)
7078
return token
7179

7280
async def close(self) -> None:
7381
"""Calling this method is unnecessary"""
7482

7583

76-
async def run_command_line(command_line: List[str]) -> str:
84+
async def run_command_line(command_line: List[str], timeout: int) -> str:
7785
try:
7886
proc = await start_process(command_line)
7987
stdout, stderr = await asyncio.wait_for(proc.communicate(), 10)
8088
if sys.platform.startswith("win") and b"' is not recognized" in stderr:
8189
# pwsh.exe isn't on the path; try powershell.exe
8290
command_line[-1] = command_line[-1].replace("pwsh", "powershell", 1)
8391
proc = await start_process(command_line)
84-
stdout, stderr = await asyncio.wait_for(proc.communicate(), 10)
92+
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout)
8593

8694
except OSError as ex:
8795
# failed to execute "cmd" or "/bin/sh"; Azure PowerShell may or may not be installed

0 commit comments

Comments
 (0)