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+ } ;
79use azure_core:: {
810 credentials:: { AccessToken , Secret , TokenCredential } ,
911 error:: { Error , ErrorKind } ,
@@ -12,12 +14,10 @@ use azure_core::{
1214} ;
1315use serde:: de:: { self , Deserializer } ;
1416use serde:: Deserialize ;
15- use std:: { ffi:: OsStr , fmt :: Debug , str , sync:: Arc } ;
17+ use std:: { ffi:: OsString , sync:: Arc } ;
1618use time:: format_description:: well_known:: Rfc3339 ;
1719use time:: OffsetDateTime ;
1820
19- const AZURE_DEVELOPER_CLI_CREDENTIAL : & str = "AzureDeveloperCliCredential" ;
20-
2121#[ derive( Clone , Debug , Deserialize ) ]
2222struct AzdTokenResponse {
2323 #[ serde( rename = "token" ) ]
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 ) ]
3963pub 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 {
159139mod 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