Skip to content

Commit 1a9968d

Browse files
authored
[Identity] Use full path for CLI executables (#38606)
Also, adjustments were made to pass in subprocess arguments as a list. Signed-off-by: Paul Van Eck <[email protected]>
1 parent 1f1727f commit 1a9968d

File tree

9 files changed

+86
-65
lines changed

9 files changed

+86
-65
lines changed

sdk/identity/azure-identity/CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
### Other Changes
1616

17+
- `AzureCliCredential` and `AzureDeveloperCliCredential` will now call their corresponding executables directly instead of going through the shell. ([#38606](https://github.com/Azure/azure-sdk-for-python/pull/38606))
18+
1719
## 1.19.0 (2024-10-08)
1820

1921
### Bugs Fixed

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

+15-11
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from datetime import datetime
77
import json
8+
import logging
89
import os
910
import re
1011
import shutil
@@ -19,12 +20,15 @@
1920
from .._internal import resolve_tenant, within_dac, validate_tenant_id, validate_scope
2021
from .._internal.decorators import log_get_token
2122

23+
24+
_LOGGER = logging.getLogger(__name__)
25+
2226
CLI_NOT_FOUND = (
2327
"Azure Developer CLI could not be found. "
2428
"Please visit https://aka.ms/azure-dev for installation instructions and then,"
2529
"once installed, authenticate to your Azure account using 'azd auth login'."
2630
)
27-
COMMAND_LINE = "azd auth token --output json --scope {}"
31+
COMMAND_LINE = ["auth", "token", "--output", "json"]
2832
EXECUTABLE_NAME = "azd"
2933
NOT_LOGGED_IN = "Please run 'azd auth login' from a command prompt to authenticate before using this credential."
3034

@@ -160,17 +164,18 @@ def _get_token_base(
160164
for scope in scopes:
161165
validate_scope(scope)
162166

163-
commandString = " --scope ".join(scopes)
164-
command = COMMAND_LINE.format(commandString)
167+
command_args = COMMAND_LINE.copy()
168+
for scope in scopes:
169+
command_args += ["--scope", scope]
165170
tenant = resolve_tenant(
166171
default_tenant=self.tenant_id,
167172
tenant_id=tenant_id,
168173
additionally_allowed_tenants=self._additionally_allowed_tenants,
169174
**kwargs,
170175
)
171176
if tenant:
172-
command += " --tenant-id " + tenant
173-
output = _run_command(command, self._process_timeout)
177+
command_args += ["--tenant-id", tenant]
178+
output = _run_command(command_args, self._process_timeout)
174179

175180
token = parse_token(output)
176181
if not token:
@@ -236,15 +241,13 @@ def sanitize_output(output: str) -> str:
236241
return re.sub(r"\"token\": \"(.*?)(\"|$)", "****", output)
237242

238243

239-
def _run_command(command: str, timeout: int) -> str:
244+
def _run_command(command_args: List[str], timeout: int) -> str:
240245
# Ensure executable exists in PATH first. This avoids a subprocess call that would fail anyway.
241-
if shutil.which(EXECUTABLE_NAME) is None:
246+
azd_path = shutil.which(EXECUTABLE_NAME)
247+
if not azd_path:
242248
raise CredentialUnavailableError(message=CLI_NOT_FOUND)
243249

244-
if sys.platform.startswith("win"):
245-
args = ["cmd", "/c", command]
246-
else:
247-
args = ["/bin/sh", "-c", command]
250+
args = [azd_path] + command_args
248251
try:
249252
working_directory = get_safe_working_dir()
250253

@@ -257,6 +260,7 @@ def _run_command(command: str, timeout: int) -> str:
257260
"timeout": timeout,
258261
}
259262

263+
_LOGGER.debug("Executing subprocess with the following arguments %s", args)
260264
return subprocess.check_output(args, **kwargs)
261265
except subprocess.CalledProcessError as ex:
262266
# non-zero return from shell

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

+14-11
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import json
77
import os
88
import re
9+
import logging
910
import shutil
1011
import subprocess
1112
import sys
@@ -26,8 +27,11 @@
2627
from .._internal.decorators import log_get_token
2728

2829

30+
_LOGGER = logging.getLogger(__name__)
31+
2932
CLI_NOT_FOUND = "Azure CLI not found on path"
30-
COMMAND_LINE = "az account get-access-token --output json --resource {}"
33+
# COMMAND_LINE = "account get-access-token --output json --resource {}"
34+
COMMAND_LINE = ["account", "get-access-token", "--output", "json"]
3135
EXECUTABLE_NAME = "az"
3236
NOT_LOGGED_IN = "Please run 'az login' to set up an account"
3337

@@ -149,19 +153,19 @@ def _get_token_base(
149153
validate_scope(scope)
150154

151155
resource = _scopes_to_resource(*scopes)
152-
command = COMMAND_LINE.format(resource)
156+
command_args = COMMAND_LINE + ["--resource", resource]
153157
tenant = resolve_tenant(
154158
default_tenant=self.tenant_id,
155159
tenant_id=tenant_id,
156160
additionally_allowed_tenants=self._additionally_allowed_tenants,
157161
**kwargs,
158162
)
159163
if tenant:
160-
command += " --tenant " + tenant
164+
command_args += ["--tenant", tenant]
161165

162166
if self.subscription:
163-
command += f' --subscription "{self.subscription}"'
164-
output = _run_command(command, self._process_timeout)
167+
command_args += ["--subscription", self.subscription]
168+
output = _run_command(command_args, self._process_timeout)
165169

166170
token = parse_token(output)
167171
if not token:
@@ -228,15 +232,13 @@ def sanitize_output(output: str) -> str:
228232
return re.sub(r"\"accessToken\": \"(.*?)(\"|$)", "****", output)
229233

230234

231-
def _run_command(command: str, timeout: int) -> str:
235+
def _run_command(command_args: List[str], timeout: int) -> str:
232236
# Ensure executable exists in PATH first. This avoids a subprocess call that would fail anyway.
233-
if shutil.which(EXECUTABLE_NAME) is None:
237+
az_path = shutil.which(EXECUTABLE_NAME)
238+
if not az_path:
234239
raise CredentialUnavailableError(message=CLI_NOT_FOUND)
235240

236-
if sys.platform.startswith("win"):
237-
args = ["cmd", "/c", command]
238-
else:
239-
args = ["/bin/sh", "-c", command]
241+
args = [az_path] + command_args
240242
try:
241243
working_directory = get_safe_working_dir()
242244

@@ -248,6 +250,7 @@ def _run_command(command: str, timeout: int) -> str:
248250
"timeout": timeout,
249251
"env": dict(os.environ, AZURE_CORE_NO_COLOR="true"),
250252
}
253+
_LOGGER.debug("Executing subprocess with the following arguments %s", args)
251254
return subprocess.check_output(args, **kwargs)
252255
except subprocess.CalledProcessError as ex:
253256
# non-zero return from shell

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

+14-11
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# Licensed under the MIT License.
44
# ------------------------------------
55
import asyncio
6+
import logging
67
import os
78
import shutil
89
import sys
@@ -26,6 +27,9 @@
2627
from ..._internal import resolve_tenant, within_dac, validate_tenant_id, validate_scope
2728

2829

30+
_LOGGER = logging.getLogger(__name__)
31+
32+
2933
class AzureDeveloperCliCredential(AsyncContextManager):
3034
"""Authenticates by requesting a token from the Azure Developer CLI.
3135
@@ -153,8 +157,9 @@ async def _get_token_base(
153157
for scope in scopes:
154158
validate_scope(scope)
155159

156-
commandString = " --scope ".join(scopes)
157-
command = COMMAND_LINE.format(commandString)
160+
command_args = COMMAND_LINE.copy()
161+
for scope in scopes:
162+
command_args += ["--scope", scope]
158163
tenant = resolve_tenant(
159164
default_tenant=self.tenant_id,
160165
tenant_id=tenant_id,
@@ -163,8 +168,8 @@ async def _get_token_base(
163168
)
164169

165170
if tenant:
166-
command += " --tenant-id " + tenant
167-
output = await _run_command(command, self._process_timeout)
171+
command_args += ["--tenant-id", tenant]
172+
output = await _run_command(command_args, self._process_timeout)
168173

169174
token = parse_token(output)
170175
if not token:
@@ -184,19 +189,17 @@ async def close(self) -> None:
184189
"""Calling this method is unnecessary"""
185190

186191

187-
async def _run_command(command: str, timeout: int) -> str:
192+
async def _run_command(command_args: List[str], timeout: int) -> str:
188193
# Ensure executable exists in PATH first. This avoids a subprocess call that would fail anyway.
189-
if shutil.which(EXECUTABLE_NAME) is None:
194+
azd_path = shutil.which(EXECUTABLE_NAME)
195+
if not azd_path:
190196
raise CredentialUnavailableError(message=CLI_NOT_FOUND)
191197

192-
if sys.platform.startswith("win"):
193-
args = ("cmd", "/c " + command)
194-
else:
195-
args = ("/bin/sh", "-c", command)
196-
198+
args = [azd_path] + command_args
197199
working_directory = get_safe_working_dir()
198200

199201
try:
202+
_LOGGER.debug("Executing subprocess with the following arguments %s", args)
200203
proc = await asyncio.create_subprocess_exec(
201204
*args,
202205
stdout=asyncio.subprocess.PIPE,

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

+14-12
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# Licensed under the MIT License.
44
# ------------------------------------
55
import asyncio
6+
import logging
67
import os
78
import shutil
89
import sys
@@ -33,6 +34,9 @@
3334
)
3435

3536

37+
_LOGGER = logging.getLogger(__name__)
38+
39+
3640
class AzureCliCredential(AsyncContextManager):
3741
"""Authenticates by requesting a token from the Azure CLI.
3842
@@ -145,7 +149,7 @@ async def _get_token_base(
145149
validate_scope(scope)
146150

147151
resource = _scopes_to_resource(*scopes)
148-
command = COMMAND_LINE.format(resource)
152+
command_args = COMMAND_LINE + ["--resource", resource]
149153
tenant = resolve_tenant(
150154
default_tenant=self.tenant_id,
151155
tenant_id=tenant_id,
@@ -154,10 +158,11 @@ async def _get_token_base(
154158
)
155159

156160
if tenant:
157-
command += " --tenant " + tenant
161+
command_args += ["--tenant", tenant]
162+
158163
if self.subscription:
159-
command += f' --subscription "{self.subscription}"'
160-
output = await _run_command(command, self._process_timeout)
164+
command_args += ["--subscription", self.subscription]
165+
output = await _run_command(command_args, self._process_timeout)
161166

162167
token = parse_token(output)
163168
if not token:
@@ -177,19 +182,16 @@ async def close(self) -> None:
177182
"""Calling this method is unnecessary"""
178183

179184

180-
async def _run_command(command: str, timeout: int) -> str:
185+
async def _run_command(command_args: List[str], timeout: int) -> str:
181186
# Ensure executable exists in PATH first. This avoids a subprocess call that would fail anyway.
182-
if shutil.which(EXECUTABLE_NAME) is None:
187+
az_path = shutil.which(EXECUTABLE_NAME)
188+
if not az_path:
183189
raise CredentialUnavailableError(message=CLI_NOT_FOUND)
184190

185-
if sys.platform.startswith("win"):
186-
args = ("cmd", "/c " + command)
187-
else:
188-
args = ("/bin/sh", "-c", command)
189-
191+
args = [az_path] + command_args
190192
working_directory = get_safe_working_dir()
191-
192193
try:
194+
_LOGGER.debug("Executing subprocess with the following arguments %s", args)
193195
proc = await asyncio.create_subprocess_exec(
194196
*args,
195197
stdout=asyncio.subprocess.PIPE,

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -211,8 +211,8 @@ def test_multitenant_authentication_class(get_token_method):
211211
second_token = first_token * 2
212212

213213
def fake_check_output(command_line, **_):
214-
match = re.search("--tenant-id (.*)", command_line[-1])
215-
tenant = match.groups()[0] if match else default_tenant
214+
tenant_id_index = command_line.index("--tenant-id") if "--tenant-id" in command_line else None
215+
tenant = command_line[tenant_id_index + 1] if tenant_id_index is not None else default_tenant
216216
assert tenant in (default_tenant, second_tenant), 'unexpected tenant "{}"'.format(tenant)
217217
return json.dumps(
218218
{
@@ -244,8 +244,8 @@ def test_multitenant_authentication(get_token_method):
244244
second_token = first_token * 2
245245

246246
def fake_check_output(command_line, **_):
247-
match = re.search("--tenant-id (.*)", command_line[-1])
248-
tenant = match.groups()[0] if match else default_tenant
247+
tenant_id_index = command_line.index("--tenant-id") if "--tenant-id" in command_line else None
248+
tenant = command_line[tenant_id_index + 1] if tenant_id_index is not None else default_tenant
249249
assert tenant in (default_tenant, second_tenant), 'unexpected tenant "{}"'.format(tenant)
250250
return json.dumps(
251251
{

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

+5-4
Original file line numberDiff line numberDiff line change
@@ -234,8 +234,8 @@ async def test_multitenant_authentication(get_token_method):
234234
second_token = first_token * 2
235235

236236
async def fake_exec(*args, **_):
237-
match = re.search("--tenant-id (.*)", args[-1])
238-
tenant = match[1] if match else default_tenant
237+
tenant_id_index = args.index("--tenant-id") if "--tenant-id" in args else None
238+
tenant = args[tenant_id_index + 1] if tenant_id_index is not None else default_tenant
239239
assert tenant in (default_tenant, second_tenant), 'unexpected tenant "{}"'.format(tenant)
240240
output = json.dumps(
241241
{
@@ -277,8 +277,9 @@ async def test_multitenant_authentication_not_allowed(get_token_method):
277277
expected_token = "***"
278278

279279
async def fake_exec(*args, **_):
280-
match = re.search("--tenant-id (.*)", args[-1])
281-
assert match is None or match[1] == expected_tenant
280+
tenant_id_index = args.index("--tenant-id") if "--tenant-id" in args else None
281+
tenant = args[tenant_id_index + 1] if tenant_id_index is not None else None
282+
assert tenant is None or tenant == expected_tenant
282283
output = json.dumps(
283284
{
284285
"expiresOn": datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ"),

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

+10-7
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,9 @@ def test_subscription(get_token_method):
8585
assert credential.subscription == subscription
8686

8787
def fake_check_output(command_line, **_):
88-
assert f'--subscription "{subscription}"' in command_line[-1]
88+
assert "--subscription" in command_line
89+
subscription_id_index = command_line.index("--subscription")
90+
assert command_line[subscription_id_index + 1]
8991
return json.dumps(
9092
{
9193
"expiresOn": datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f"),
@@ -293,8 +295,8 @@ def test_multitenant_authentication_class(get_token_method):
293295
second_token = first_token * 2
294296

295297
def fake_check_output(command_line, **_):
296-
match = re.search("--tenant (.*)", command_line[-1])
297-
tenant = match.groups()[0] if match else default_tenant
298+
tenant_index = command_line.index("--tenant") if "--tenant" in command_line else None
299+
tenant = command_line[tenant_index + 1] if tenant_index is not None else default_tenant
298300
assert tenant in (default_tenant, second_tenant), 'unexpected tenant "{}"'.format(tenant)
299301
return json.dumps(
300302
{
@@ -326,8 +328,8 @@ def test_multitenant_authentication(get_token_method):
326328
second_token = first_token * 2
327329

328330
def fake_check_output(command_line, **_):
329-
match = re.search("--tenant (.*)", command_line[-1])
330-
tenant = match.groups()[0] if match else default_tenant
331+
tenant_index = command_line.index("--tenant") if "--tenant" in command_line else None
332+
tenant = command_line[tenant_index + 1] if tenant_index is not None else default_tenant
331333
assert tenant in (default_tenant, second_tenant), 'unexpected tenant "{}"'.format(tenant)
332334
return json.dumps(
333335
{
@@ -368,8 +370,9 @@ def test_multitenant_authentication_not_allowed(get_token_method):
368370
expected_token = "***"
369371

370372
def fake_check_output(command_line, **_):
371-
match = re.search("--tenant (.*)", command_line[-1])
372-
assert match is None or match[1] == expected_tenant
373+
tenant_index = command_line.index("--tenant") if "--tenant" in command_line else None
374+
tenant = command_line[tenant_index + 1] if tenant_index is not None else None
375+
assert tenant is None or tenant == expected_tenant
373376
return json.dumps(
374377
{
375378
"expiresOn": datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f"),

0 commit comments

Comments
 (0)