use crate::credential::Credential;
use reqsign_core::time::Timestamp;
use reqsign_core::{Context, ProvideCredential};
use serde::Deserialize;
#[derive(Clone, Debug, Default)]
pub struct AzureCliCredentialProvider {}
impl AzureCliCredentialProvider {
pub fn new() -> Self {
Self::default()
}
async fn get_access_token_from_cli(
&self,
ctx: &Context,
resource: &str,
) -> Result<AzureCliToken, reqsign_core::Error> {
let args = [
"account",
"get-access-token",
"--resource",
resource,
"--output",
"json",
];
let output = ctx.command_execute("az", &args).await?;
if !output.success() {
return Err(reqsign_core::Error::credential_invalid(format!(
"Azure CLI command failed: {}",
String::from_utf8_lossy(&output.stderr)
)));
}
let token: AzureCliToken = serde_json::from_slice(&output.stdout).map_err(|e| {
reqsign_core::Error::unexpected(format!("Failed to parse Azure CLI output: {e}"))
})?;
Ok(token)
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct AzureCliToken {
access_token: String,
expires_on: Option<String>,
#[serde(rename = "expires_on")]
expires_on_timestamp: Option<i64>,
#[allow(dead_code)]
subscription: Option<String>,
#[allow(dead_code)]
tenant: Option<String>,
#[allow(dead_code)]
token_type: String,
}
impl ProvideCredential for AzureCliCredentialProvider {
type Credential = Credential;
async fn provide_credential(
&self,
ctx: &Context,
) -> Result<Option<Self::Credential>, reqsign_core::Error> {
let resource = "https://storage.azure.com/";
let token = self.get_access_token_from_cli(ctx, resource).await?;
let expires_on = if let Some(timestamp) = token.expires_on_timestamp {
Timestamp::from_second(timestamp).ok()
} else if let Some(expires_str) = token.expires_on {
Timestamp::parse_datetime_utc(&expires_str).ok()
} else {
None
};
Ok(Some(Credential::with_bearer_token(
&token.access_token,
expires_on,
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_redacted_real_azure_cli_response() {
let response = include_bytes!("../../tests/fixtures/azure_cli_token_response.json");
let token: AzureCliToken = serde_json::from_slice(response).unwrap();
assert_eq!(token.access_token, "REDACTED");
assert_eq!(token.expires_on_timestamp, Some(1788423131));
assert_eq!(token.token_type, "Bearer");
}
}