Skip to content

Commit 6f836fa

Browse files
authored
AzureCliCredential consistently invokes az within a shell (Azure#2560)
1 parent 71fe449 commit 6f836fa

File tree

7 files changed

+370
-144
lines changed

7 files changed

+370
-144
lines changed

sdk/identity/.dict.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ imds
99
managedidentity
1010
msal
1111
replacen
12+
SYSTEMROOT
1213
workloadidentity

sdk/identity/azure_identity/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
### Bugs Fixed
1010

11+
- `AzureCliCredential` didn't invoke `az` within a shell on all platforms
12+
1113
### Other Changes
1214

1315
## 0.24.0 (2025-05-06)

sdk/identity/azure_identity/src/azure_developer_cli_credential.rs

Lines changed: 37 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
// cspell:ignore SYSTEMROOT workdir
5-
6-
use crate::{env::Env, validate_scope, validate_tenant_id, TokenCredentialOptions};
4+
use crate::{
5+
env::Env,
6+
process::{shell_exec, OutputProcessor},
7+
validate_scope, validate_tenant_id, TokenCredentialOptions,
8+
};
79
use azure_core::{
810
credentials::{AccessToken, Secret, TokenCredential},
911
error::{Error, ErrorKind},
@@ -12,12 +14,10 @@ use azure_core::{
1214
};
1315
use serde::de::{self, Deserializer};
1416
use serde::Deserialize;
15-
use std::{ffi::OsStr, fmt::Debug, str, sync::Arc};
17+
use std::{ffi::OsString, sync::Arc};
1618
use time::format_description::well_known::Rfc3339;
1719
use time::OffsetDateTime;
1820

19-
const AZURE_DEVELOPER_CLI_CREDENTIAL: &str = "AzureDeveloperCliCredential";
20-
2121
#[derive(Clone, Debug, Deserialize)]
2222
struct AzdTokenResponse {
2323
#[serde(rename = "token")]
@@ -34,6 +34,30 @@ where
3434
OffsetDateTime::parse(s, &Rfc3339).map_err(de::Error::custom)
3535
}
3636

37+
impl OutputProcessor for AzdTokenResponse {
38+
fn credential_name() -> &'static str {
39+
"AzureDeveloperCliCredential"
40+
}
41+
42+
fn deserialize_token(stdout: &str) -> azure_core::Result<AccessToken> {
43+
let response: Self = from_json(stdout)?;
44+
Ok(AccessToken::new(response.access_token, response.expires_on))
45+
}
46+
47+
fn get_error_message(stderr: &str) -> Option<&str> {
48+
// azd embeds its "you need to log in" error message in JSON, so in that case we can provide a clearer one
49+
if stderr.contains("azd auth login") {
50+
Some("please run `azd auth login` from a command prompt before using this credential")
51+
} else {
52+
None
53+
}
54+
}
55+
56+
fn tool_name() -> &'static str {
57+
"azd"
58+
}
59+
}
60+
3761
/// Authenticates the identity logged in to the [Azure Developer CLI](https://learn.microsoft.com/azure/developer/azure-developer-cli/overview).
3862
#[derive(Debug)]
3963
pub struct AzureDeveloperCliCredential {
@@ -88,61 +112,17 @@ impl TokenCredential for AzureDeveloperCliCredential {
88112
"at least one scope required",
89113
));
90114
}
91-
let mut command = "azd auth token -o json".to_string();
115+
let mut command = OsString::from("azd auth token -o json");
92116
for scope in scopes {
93117
validate_scope(scope)?;
94-
command.push_str(" --scope ");
95-
command.push_str(scope);
118+
command.push(" --scope ");
119+
command.push(scope);
96120
}
97121
if let Some(ref tenant_id) = self.tenant_id {
98-
command.push_str(" --tenant-id ");
99-
command.push_str(tenant_id);
100-
}
101-
let (workdir, program, c_switch) = if cfg!(target_os = "windows") {
102-
let system_root = self.env.var("SYSTEMROOT").map_err(|_| {
103-
Error::message(
104-
ErrorKind::Credential,
105-
"SYSTEMROOT environment variable not set",
106-
)
107-
})?;
108-
(system_root, "cmd", "/C")
109-
} else {
110-
("/bin".to_string(), "/bin/sh", "-c")
111-
};
112-
let command_string = format!("cd {workdir} && {command}");
113-
let args = vec![OsStr::new(c_switch), OsStr::new(command_string.as_str())];
114-
115-
let status = self.executor.run(OsStr::new(program), &args).await;
116-
117-
match status {
118-
Ok(azd_output) if azd_output.status.success() => {
119-
let output = str::from_utf8(&azd_output.stdout)?;
120-
let response: AzdTokenResponse = from_json(output)?;
121-
Ok(AccessToken::new(response.access_token, response.expires_on))
122-
}
123-
Ok(azd_output) => {
124-
let stderr = String::from_utf8_lossy(&azd_output.stderr);
125-
let message = if stderr.contains("azd auth login") {
126-
"please run 'azd auth login' from a command prompt before using this credential"
127-
} else if azd_output.status.code() == Some(127)
128-
|| stderr.contains("'azd' is not recognized")
129-
{
130-
"Azure Developer CLI not found on path"
131-
} else {
132-
&stderr
133-
};
134-
Err(Error::with_message(ErrorKind::Credential, || {
135-
format!("{AZURE_DEVELOPER_CLI_CREDENTIAL} authentication failed: {message}")
136-
}))
137-
}
138-
Err(e) => {
139-
let message = format!(
140-
"{AZURE_DEVELOPER_CLI_CREDENTIAL} authentication failed due to {} error: {e}",
141-
e.kind()
142-
);
143-
Err(Error::with_message(ErrorKind::Credential, || message))
144-
}
122+
command.push(" --tenant-id ");
123+
command.push(tenant_id);
145124
}
125+
shell_exec::<AzdTokenResponse>(self.executor.clone(), &self.env, &command).await
146126
}
147127
}
148128

@@ -159,6 +139,7 @@ impl From<TokenCredentialOptions> for AzureDeveloperCliCredentialOptions {
159139
mod tests {
160140
use super::*;
161141
use crate::tests::{MockExecutor, FAKE_TENANT_ID, FAKE_TOKEN, LIVE_TEST_SCOPES};
142+
use std::ffi::OsStr;
162143
use time::UtcOffset;
163144

164145
async fn run_test(

0 commit comments

Comments
 (0)