1
1
// Copyright (c) Microsoft Corporation. All rights reserved.
2
2
// Licensed under the MIT License.
3
3
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
+ } ;
7
9
use azure_core:: {
8
10
credentials:: { AccessToken , Secret , TokenCredential } ,
9
11
error:: { Error , ErrorKind } ,
@@ -12,12 +14,10 @@ use azure_core::{
12
14
} ;
13
15
use serde:: de:: { self , Deserializer } ;
14
16
use serde:: Deserialize ;
15
- use std:: { ffi:: OsStr , fmt :: Debug , str , sync:: Arc } ;
17
+ use std:: { ffi:: OsString , sync:: Arc } ;
16
18
use time:: format_description:: well_known:: Rfc3339 ;
17
19
use time:: OffsetDateTime ;
18
20
19
- const AZURE_DEVELOPER_CLI_CREDENTIAL : & str = "AzureDeveloperCliCredential" ;
20
-
21
21
#[ derive( Clone , Debug , Deserialize ) ]
22
22
struct AzdTokenResponse {
23
23
#[ serde( rename = "token" ) ]
34
34
OffsetDateTime :: parse ( s, & Rfc3339 ) . map_err ( de:: Error :: custom)
35
35
}
36
36
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
+
37
61
/// Authenticates the identity logged in to the [Azure Developer CLI](https://learn.microsoft.com/azure/developer/azure-developer-cli/overview).
38
62
#[ derive( Debug ) ]
39
63
pub struct AzureDeveloperCliCredential {
@@ -88,61 +112,17 @@ impl TokenCredential for AzureDeveloperCliCredential {
88
112
"at least one scope required" ,
89
113
) ) ;
90
114
}
91
- let mut command = "azd auth token -o json" . to_string ( ) ;
115
+ let mut command = OsString :: from ( "azd auth token -o json" ) ;
92
116
for scope in scopes {
93
117
validate_scope ( scope) ?;
94
- command. push_str ( " --scope " ) ;
95
- command. push_str ( scope) ;
118
+ command. push ( " --scope " ) ;
119
+ command. push ( scope) ;
96
120
}
97
121
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) ;
145
124
}
125
+ shell_exec :: < AzdTokenResponse > ( self . executor . clone ( ) , & self . env , & command) . await
146
126
}
147
127
}
148
128
@@ -159,6 +139,7 @@ impl From<TokenCredentialOptions> for AzureDeveloperCliCredentialOptions {
159
139
mod tests {
160
140
use super :: * ;
161
141
use crate :: tests:: { MockExecutor , FAKE_TENANT_ID , FAKE_TOKEN , LIVE_TEST_SCOPES } ;
142
+ use std:: ffi:: OsStr ;
162
143
use time:: UtcOffset ;
163
144
164
145
async fn run_test (
0 commit comments