-
Notifications
You must be signed in to change notification settings - Fork 252
/
Copy pathazure_cli_credentials.rs
146 lines (131 loc) · 4.84 KB
/
azure_cli_credentials.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
use azure_core::auth::{AccessToken, TokenCredential, TokenResponse};
use azure_core::error::{Error, ErrorKind, ResultExt};
use serde::Deserialize;
use std::process::Command;
use std::str;
use time::OffsetDateTime;
mod az_cli_date_format {
use azure_core::date;
use azure_core::error::{ErrorKind, ResultExt};
use serde::{self, Deserialize, Deserializer};
use time::format_description::FormatItem;
use time::macros::format_description;
use time::{OffsetDateTime, PrimitiveDateTime};
const FORMAT: &[FormatItem] =
format_description!("[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:6]");
pub fn parse(s: &str) -> azure_core::Result<OffsetDateTime> {
// expiresOn from azure cli uses the local timezone and needs to be converted to UTC
let dt = PrimitiveDateTime::parse(s, FORMAT)
.with_context(ErrorKind::DataConversion, || {
format!("unable to parse expiresOn '{s}")
})?;
Ok(date::assume_local(&dt))
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<OffsetDateTime, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
parse(&s).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CliTokenResponse {
pub access_token: AccessToken,
#[serde(with = "az_cli_date_format")]
pub expires_on: OffsetDateTime,
pub subscription: String,
pub tenant: String,
#[allow(unused)]
pub token_type: String,
}
/// Enables authentication to Azure Active Directory using Azure CLI to obtain an access token.
#[derive(Default)]
pub struct AzureCliCredential {
_private: (),
}
impl AzureCliCredential {
/// Create a new `AzureCliCredential`
pub fn new() -> Self {
Self::default()
}
/// Get an access token for an optional resource
fn get_access_token(resource: Option<&str>) -> azure_core::Result<CliTokenResponse> {
// on window az is a cmd and it should be called like this
// see https://doc.rust-lang.org/nightly/std/process/struct.Command.html
let program = if cfg!(target_os = "windows") {
"cmd"
} else {
"az"
};
let mut args = Vec::new();
if cfg!(target_os = "windows") {
args.push("/C");
args.push("az");
}
args.push("account");
args.push("get-access-token");
args.push("--output");
args.push("json");
if let Some(resource) = resource {
args.push("--resource");
args.push(resource);
}
match Command::new(program).args(args).output() {
Ok(az_output) if az_output.status.success() => {
let output = str::from_utf8(&az_output.stdout)?;
let token_response = serde_json::from_str::<CliTokenResponse>(output)
.map_kind(ErrorKind::DataConversion)?;
Ok(token_response)
}
Ok(az_output) => {
let output = String::from_utf8_lossy(&az_output.stderr);
Err(Error::with_message(ErrorKind::Credential, || {
format!("'az account get-access-token' command failed: {output}")
}))
}
Err(e) => match e.kind() {
std::io::ErrorKind::NotFound => {
Err(Error::message(ErrorKind::Other, "Azure CLI not installed"))
}
error_kind => Err(Error::with_message(ErrorKind::Other, || {
format!("Unknown error of kind: {error_kind:?}")
})),
},
}
}
/// Returns the current subscription ID from the Azure CLI.
pub fn get_subscription() -> azure_core::Result<String> {
let tr = Self::get_access_token(None)?;
Ok(tr.subscription)
}
/// Returns the current tenant ID from the Azure CLI.
pub fn get_tenant() -> azure_core::Result<String> {
let tr = Self::get_access_token(None)?;
Ok(tr.tenant)
}
}
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl TokenCredential for AzureCliCredential {
async fn get_token(&self, resource: &str) -> azure_core::Result<TokenResponse> {
let tr = Self::get_access_token(Some(resource))?;
Ok(TokenResponse::new(tr.access_token, tr.expires_on))
}
}
#[cfg(test)]
mod tests {
use super::*;
use azure_core::date;
use time::macros::datetime;
#[test]
fn can_parse_expires_on() -> azure_core::Result<()> {
let expires_on = "2022-07-30 12:12:53.919110";
assert_eq!(
az_cli_date_format::parse(expires_on)?,
date::assume_local(&datetime!(2022-07-30 12:12:53.919110))
);
Ok(())
}
}