@@ -21,7 +21,7 @@ use crate::util::hmac_sha256;
21
21
use crate :: RetryConfig ;
22
22
use base64:: prelude:: BASE64_STANDARD ;
23
23
use base64:: Engine ;
24
- use chrono:: Utc ;
24
+ use chrono:: { DateTime , Utc } ;
25
25
use reqwest:: header:: ACCEPT ;
26
26
use reqwest:: {
27
27
header:: {
@@ -34,6 +34,7 @@ use reqwest::{
34
34
use serde:: Deserialize ;
35
35
use snafu:: { ResultExt , Snafu } ;
36
36
use std:: borrow:: Cow ;
37
+ use std:: process:: Command ;
37
38
use std:: str;
38
39
use std:: time:: { Duration , Instant } ;
39
40
use url:: Url ;
@@ -61,6 +62,12 @@ pub enum Error {
61
62
62
63
#[ snafu( display( "Error reading federated token file " ) ) ]
63
64
FederatedTokenFile ,
65
+
66
+ #[ snafu( display( "'az account get-access-token' command failed: {message}" ) ) ]
67
+ AzureCli { message : String } ,
68
+
69
+ #[ snafu( display( "Failed to parse azure cli response: {source}" ) ) ]
70
+ AzureCliResponse { source : serde_json:: Error } ,
64
71
}
65
72
66
73
pub type Result < T , E = Error > = std:: result:: Result < T , E > ;
@@ -69,6 +76,7 @@ pub type Result<T, E = Error> = std::result::Result<T, E>;
69
76
#[ derive( Debug ) ]
70
77
pub enum CredentialProvider {
71
78
AccessKey ( String ) ,
79
+ BearerToken ( String ) ,
72
80
SASToken ( Vec < ( String , String ) > ) ,
73
81
TokenCredential ( TokenCache < String > , Box < dyn TokenCredential > ) ,
74
82
}
@@ -540,6 +548,122 @@ impl TokenCredential for WorkloadIdentityOAuthProvider {
540
548
}
541
549
}
542
550
551
+ mod az_cli_date_format {
552
+ use chrono:: { DateTime , TimeZone } ;
553
+ use serde:: { self , Deserialize , Deserializer } ;
554
+
555
+ pub fn deserialize < ' de , D > (
556
+ deserializer : D ,
557
+ ) -> Result < DateTime < chrono:: Local > , D :: Error >
558
+ where
559
+ D : Deserializer < ' de > ,
560
+ {
561
+ let s = String :: deserialize ( deserializer) ?;
562
+ // expiresOn from azure cli uses the local timezone
563
+ let date = chrono:: NaiveDateTime :: parse_from_str ( & s, "%Y-%m-%d %H:%M:%S.%6f" )
564
+ . map_err ( serde:: de:: Error :: custom) ?;
565
+ chrono:: Local
566
+ . from_local_datetime ( & date)
567
+ . single ( )
568
+ . ok_or ( serde:: de:: Error :: custom (
569
+ "azure cli returned ambiguous expiry date" ,
570
+ ) )
571
+ }
572
+ }
573
+
574
+ #[ derive( Debug , Clone , Deserialize ) ]
575
+ #[ serde( rename_all = "camelCase" ) ]
576
+ struct AzureCliTokenResponse {
577
+ pub access_token : String ,
578
+ #[ serde( with = "az_cli_date_format" ) ]
579
+ pub expires_on : DateTime < chrono:: Local > ,
580
+ pub token_type : String ,
581
+ }
582
+
583
+ #[ derive( Default , Debug ) ]
584
+ pub struct AzureCliCredential {
585
+ _private : ( ) ,
586
+ }
587
+
588
+ impl AzureCliCredential {
589
+ pub fn new ( ) -> Self {
590
+ Self :: default ( )
591
+ }
592
+ }
593
+
594
+ #[ async_trait:: async_trait]
595
+ impl TokenCredential for AzureCliCredential {
596
+ /// Fetch a token
597
+ async fn fetch_token (
598
+ & self ,
599
+ _client : & Client ,
600
+ _retry : & RetryConfig ,
601
+ ) -> Result < TemporaryToken < String > > {
602
+ // on window az is a cmd and it should be called like this
603
+ // see https://doc.rust-lang.org/nightly/std/process/struct.Command.html
604
+ let program = if cfg ! ( target_os = "windows" ) {
605
+ "cmd"
606
+ } else {
607
+ "az"
608
+ } ;
609
+ let mut args = Vec :: new ( ) ;
610
+ if cfg ! ( target_os = "windows" ) {
611
+ args. push ( "/C" ) ;
612
+ args. push ( "az" ) ;
613
+ }
614
+ args. push ( "account" ) ;
615
+ args. push ( "get-access-token" ) ;
616
+ args. push ( "--output" ) ;
617
+ args. push ( "json" ) ;
618
+ args. push ( "--scope" ) ;
619
+ args. push ( AZURE_STORAGE_SCOPE ) ;
620
+
621
+ match Command :: new ( program) . args ( args) . output ( ) {
622
+ Ok ( az_output) if az_output. status . success ( ) => {
623
+ let output =
624
+ str:: from_utf8 ( & az_output. stdout ) . map_err ( |_| Error :: AzureCli {
625
+ message : "az response is not a valid utf-8 string" . to_string ( ) ,
626
+ } ) ?;
627
+
628
+ let token_response =
629
+ serde_json:: from_str :: < AzureCliTokenResponse > ( output)
630
+ . context ( AzureCliResponseSnafu ) ?;
631
+ if !token_response. token_type . eq_ignore_ascii_case ( "bearer" ) {
632
+ return Err ( Error :: AzureCli {
633
+ message : format ! (
634
+ "got unexpected token type from azure cli: {0}" ,
635
+ token_response. token_type
636
+ ) ,
637
+ } ) ;
638
+ }
639
+ let duration = token_response. expires_on . naive_local ( )
640
+ - chrono:: Local :: now ( ) . naive_local ( ) ;
641
+ Ok ( TemporaryToken {
642
+ token : token_response. access_token ,
643
+ expiry : Instant :: now ( )
644
+ + duration. to_std ( ) . map_err ( |_| Error :: AzureCli {
645
+ message : "az returned invalid lifetime" . to_string ( ) ,
646
+ } ) ?,
647
+ } )
648
+ }
649
+ Ok ( az_output) => {
650
+ let message = String :: from_utf8_lossy ( & az_output. stderr ) ;
651
+ Err ( Error :: AzureCli {
652
+ message : message. into ( ) ,
653
+ } )
654
+ }
655
+ Err ( e) => match e. kind ( ) {
656
+ std:: io:: ErrorKind :: NotFound => Err ( Error :: AzureCli {
657
+ message : "Azure Cli not installed" . into ( ) ,
658
+ } ) ,
659
+ error_kind => Err ( Error :: AzureCli {
660
+ message : format ! ( "io error: {error_kind:?}" ) ,
661
+ } ) ,
662
+ } ,
663
+ }
664
+ }
665
+ }
666
+
543
667
#[ cfg( test) ]
544
668
mod tests {
545
669
use super :: * ;
0 commit comments