From de46128e22c3e1b345c952b0dae3a566a44d5165 Mon Sep 17 00:00:00 2001 From: congyi <15605187270@163.com> Date: Mon, 18 Mar 2024 15:35:32 +0800 Subject: [PATCH 1/8] support workload identity auth for Azure --- src/azure/storage/config.rs | 18 ++++ src/azure/storage/loader.rs | 18 +++- src/azure/storage/mod.rs | 2 + .../storage/workload_identity_credential.rs | 85 +++++++++++++++++++ 4 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 src/azure/storage/workload_identity_credential.rs diff --git a/src/azure/storage/config.rs b/src/azure/storage/config.rs index fac6d7a1..667fc184 100644 --- a/src/azure/storage/config.rs +++ b/src/azure/storage/config.rs @@ -44,4 +44,22 @@ pub struct Config { /// /// This is part of use AAD(Azure Active Directory) authenticate on Azure VM pub endpoint: Option, + /// `azure_federated_token` value will be loaded from: + /// + /// - this field if it's `is_some` + /// - env value: [`AZURE_FEDERATED_TOKEN`] + /// - profile config: `azure_federated_token_file` + pub azure_federated_token: Option, + /// `azure_federated_token_file` value will be loaded from: + /// + /// - this field if it's `is_some` + /// - env value: [`AZURE_FEDERATED_TOKEN_FILE`] + /// - profile config: `azure_federated_token_file` + pub azure_federated_token_file: Option, + /// `azure_tenant_id_env_key` value will be loaded from: + /// + /// - this field if it's `is_some` + /// - env value: [`AZURE_TENANT_ID_ENV_KEY`] + /// - profile config: `azure_tenant_id_env_key` + pub azure_tenant_id_env_key: Option, } diff --git a/src/azure/storage/loader.rs b/src/azure/storage/loader.rs index 5a445bcc..b0b29d36 100644 --- a/src/azure/storage/loader.rs +++ b/src/azure/storage/loader.rs @@ -3,9 +3,9 @@ use std::sync::Mutex; use anyhow::Result; -use super::config::Config; use super::credential::Credential; use super::imds_credential; +use super::{config::Config, workload_identity_credential}; /// Loader will load credential from different methods. #[cfg_attr(test, derive(Debug))] @@ -45,6 +45,10 @@ impl Loader { return Ok(Some(cred)); } + if let Some(cred) = self.load_via_workload_identity().await? { + return Ok(Some(cred)); + } + // try to load credential using AAD(Azure Active Directory) authenticate on Azure VM // we may get an error if not running on Azure VM // see https://learn.microsoft.com/en-us/azure/app-service/overview-managed-identity?tabs=portal,http#using-the-rest-protocol @@ -72,4 +76,16 @@ impl Loader { Ok(cred) } + + async fn load_via_workload_identity(&self) -> Result> { + let workload_identity_token = workload_identity_credential::get_workload_identity_token( + "https://storage.azure.com/", + &self.config, + ) + .await?; + match workload_identity_token { + Some(token) => Ok(Some(Credential::BearerToken(token.access_token))), + None => Ok(None), + } + } } diff --git a/src/azure/storage/mod.rs b/src/azure/storage/mod.rs index a2549958..3fb2afa2 100644 --- a/src/azure/storage/mod.rs +++ b/src/azure/storage/mod.rs @@ -14,6 +14,8 @@ pub use credential::Credential as AzureStorageCredential; mod imds_credential; +mod workload_identity_credential; + mod loader; pub use loader::Loader as AzureStorageLoader; diff --git a/src/azure/storage/workload_identity_credential.rs b/src/azure/storage/workload_identity_credential.rs new file mode 100644 index 00000000..afa3467d --- /dev/null +++ b/src/azure/storage/workload_identity_credential.rs @@ -0,0 +1,85 @@ +use std::str; + +use http::HeaderValue; +use http::Method; +use http::Request; +use reqwest::Client; +use reqwest::Url; +use serde::Deserialize; +use std::fs; + +use super::config::Config; + +const MSI_API_VERSION: &str = "2019-08-01"; +const MSI_ENDPOINT: &str = "http://169.254.169.254/metadata/identity/oauth2/token"; + +/// Gets an access token for the specified resource and configuration. +/// +/// See +pub async fn get_workload_identity_token( + resource: &str, + config: &Config, +) -> anyhow::Result> { + let token = match ( + &config.azure_federated_token, + &config.azure_federated_token_file, + ) { + (Some(token), Some(_)) | (Some(token), None) => token.clone(), + (None, Some(token_file)) => { + let token = fs::read_to_string(token_file)?; + token + } + _ => return Ok(None), + }; + let tenant_id = if let Some(tenant_id) = &config.azure_tenant_id_env_key { + tenant_id + } else { + return Ok(None); + }; + let client_id = if let Some(client_id) = &config.client_id { + client_id + } else { + return Ok(None); + }; + + let endpoint = config.endpoint.as_deref().unwrap_or(MSI_ENDPOINT); + + let mut query_items = vec![("api-version", MSI_API_VERSION), ("resource", resource)]; + query_items.push(("token", &token)); + query_items.push(("tenant_id", &tenant_id)); + query_items.push(("client_id", &client_id)); + + let url = Url::parse_with_params(endpoint, &query_items)?; + let mut req = Request::builder() + .method(Method::GET) + .uri(url.to_string()) + .body("")?; + + req.headers_mut() + .insert("metadata", HeaderValue::from_static("true")); + + if let Some(secret) = &config.msi_secret { + req.headers_mut() + .insert("x-identity-header", HeaderValue::from_str(secret)?); + }; + + let res = Client::new().execute(req.try_into()?).await?; + let rsp_status = res.status(); + let rsp_body = res.text().await?; +; + + if !rsp_status.is_success() { + return Err(anyhow::anyhow!("Failed to get token from working identity credential")); + } + + let token: AccessToken = serde_json::from_str(&rsp_body)?; + Ok(Some(token)) +} + +#[derive(Debug, Clone, Deserialize)] +#[allow(unused)] +pub struct AccessToken { + pub access_token: String, + pub expires_on: String, + +} From 9e8911be6fda0a21c4aeca6ba31553d5c395cfcb Mon Sep 17 00:00:00 2001 From: congyi <15605187270@163.com> Date: Fri, 12 Apr 2024 12:27:38 +0800 Subject: [PATCH 2/8] check is expire for azure storage federated token --- src/azure/storage/credential.rs | 37 ++++++++++++++++++- src/azure/storage/loader.rs | 15 ++++++-- src/azure/storage/signer.rs | 5 ++- .../storage/workload_identity_credential.rs | 11 ++---- 4 files changed, 54 insertions(+), 14 deletions(-) diff --git a/src/azure/storage/credential.rs b/src/azure/storage/credential.rs index 96243f2e..3639de1e 100644 --- a/src/azure/storage/credential.rs +++ b/src/azure/storage/credential.rs @@ -16,5 +16,40 @@ pub enum Credential { /// associated with the subscription that contains the storage account. /// /// ref: - BearerToken(String), + BearerToken(String, String), +} + +impl Credential { + /// is current cred is valid? + pub fn is_valid(&self) -> bool { + if self.is_empty() { + return false; + } + + match self { + Credential::BearerToken(_, expires_on) => { + if let Ok(expires) = chrono::DateTime::parse_from_rfc3339(expires_on) { + let buffer = chrono::Duration::seconds(120); + if expires > (chrono::Utc::now() + buffer) { + return false; + } + } + } + _ => {} + }; + + true + } + + fn is_empty(&self) -> bool { + match self { + Credential::SharedKey(account_name, account_key) => { + account_name.is_empty() || account_key.is_empty() + } + Credential::SharedAccessSignature(sas_token) => sas_token.is_empty(), + Credential::BearerToken(bearer_token, expire_on) => { + bearer_token.is_empty() || expire_on.is_empty() + } + } + } } diff --git a/src/azure/storage/loader.rs b/src/azure/storage/loader.rs index b0b29d36..3c4336cb 100644 --- a/src/azure/storage/loader.rs +++ b/src/azure/storage/loader.rs @@ -28,8 +28,9 @@ impl Loader { /// Load credential. pub async fn load(&self) -> Result> { // Return cached credential if it's valid. - if let Some(cred) = self.credential.lock().expect("lock poisoned").clone() { - return Ok(Some(cred)); + match self.credential.lock().expect("lock poisoned").clone() { + Some(cred) if cred.is_valid() => return Ok(Some(cred)), + _ => (), } let cred = self.load_inner().await?; @@ -72,7 +73,10 @@ impl Loader { async fn load_via_imds(&self) -> Result> { let token = imds_credential::get_access_token("https://storage.azure.com/", &self.config).await?; - let cred = Some(Credential::BearerToken(token.access_token)); + let cred = Some(Credential::BearerToken( + token.access_token, + token.expires_on, + )); Ok(cred) } @@ -84,7 +88,10 @@ impl Loader { ) .await?; match workload_identity_token { - Some(token) => Ok(Some(Credential::BearerToken(token.access_token))), + Some(token) => Ok(Some(Credential::BearerToken( + token.access_token, + token.expires_on, + ))), None => Ok(None), } } diff --git a/src/azure/storage/signer.rs b/src/azure/storage/signer.rs index e8d2c1be..89be9902 100644 --- a/src/azure/storage/signer.rs +++ b/src/azure/storage/signer.rs @@ -61,7 +61,7 @@ impl Signer { ctx.query_append(token); return Ok(ctx); } - Credential::BearerToken(token) => match method { + Credential::BearerToken(token, _) => match method { SigningMethod::Query(_) => { return Err(anyhow!("BearerToken can't be used in query string")); } @@ -307,7 +307,8 @@ mod tests { .uri("https://test.blob.core.windows.net/testbucket/testblob") .body(()) .unwrap(); - let cred = AzureStorageCredential::BearerToken("token".to_string()); + let cred = + AzureStorageCredential::BearerToken("token".to_string(), "expires_on".to_string()); // Can effectively sign request with SigningMethod::Header assert!(signer.sign(&mut req, &cred).is_ok()); diff --git a/src/azure/storage/workload_identity_credential.rs b/src/azure/storage/workload_identity_credential.rs index afa3467d..425e968b 100644 --- a/src/azure/storage/workload_identity_credential.rs +++ b/src/azure/storage/workload_identity_credential.rs @@ -25,10 +25,7 @@ pub async fn get_workload_identity_token( &config.azure_federated_token_file, ) { (Some(token), Some(_)) | (Some(token), None) => token.clone(), - (None, Some(token_file)) => { - let token = fs::read_to_string(token_file)?; - token - } + (None, Some(token_file)) => fs::read_to_string(token_file)?, _ => return Ok(None), }; let tenant_id = if let Some(tenant_id) = &config.azure_tenant_id_env_key { @@ -66,10 +63,11 @@ pub async fn get_workload_identity_token( let res = Client::new().execute(req.try_into()?).await?; let rsp_status = res.status(); let rsp_body = res.text().await?; -; if !rsp_status.is_success() { - return Err(anyhow::anyhow!("Failed to get token from working identity credential")); + return Err(anyhow::anyhow!( + "Failed to get token from working identity credential" + )); } let token: AccessToken = serde_json::from_str(&rsp_body)?; @@ -81,5 +79,4 @@ pub async fn get_workload_identity_token( pub struct AccessToken { pub access_token: String, pub expires_on: String, - } From ddf9d01a311e7a93ebfa85fbb7fec10e90d3a7e1 Mon Sep 17 00:00:00 2001 From: congyi <15605187270@163.com> Date: Fri, 12 Apr 2024 13:06:45 +0800 Subject: [PATCH 3/8] minor --- src/azure/storage/credential.rs | 14 +++++--------- src/azure/storage/loader.rs | 2 +- .../storage/workload_identity_credential.rs | 16 ++++++++++------ 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/azure/storage/credential.rs b/src/azure/storage/credential.rs index 6e06e159..d610e585 100644 --- a/src/azure/storage/credential.rs +++ b/src/azure/storage/credential.rs @@ -25,17 +25,13 @@ impl Credential { if self.is_empty() { return false; } - - match self { - Credential::BearerToken(_, expires_on) => { - if let Ok(expires) = chrono::DateTime::parse_from_rfc3339(expires_on) { - let buffer = chrono::Duration::try_minutes(2).expect("in bounds"); - if expires > (chrono::Utc::now() + buffer) { - return false; - } + if let Credential::BearerToken(_, expires_on) = self { + if let Ok(expires) = chrono::DateTime::parse_from_rfc3339(expires_on) { + let buffer = chrono::Duration::try_minutes(2).expect("in bounds"); + if expires > (chrono::Utc::now() + buffer) { + return false; } } - _ => {} }; true diff --git a/src/azure/storage/loader.rs b/src/azure/storage/loader.rs index 30525b68..337f9822 100644 --- a/src/azure/storage/loader.rs +++ b/src/azure/storage/loader.rs @@ -86,7 +86,7 @@ impl Loader { match workload_identity_token { Some(token) => Ok(Some(Credential::BearerToken( token.access_token, - token.expires_on, + token.expires_on.unwrap_or("".to_string()), ))), None => Ok(None), } diff --git a/src/azure/storage/workload_identity_credential.rs b/src/azure/storage/workload_identity_credential.rs index 2c9a7b18..490d57b7 100644 --- a/src/azure/storage/workload_identity_credential.rs +++ b/src/azure/storage/workload_identity_credential.rs @@ -14,7 +14,7 @@ const STORAGE_TOKEN_SCOPE: &str = "https://storage.azure.com/.default"; /// Gets an access token for the specified resource and configuration. /// /// See -pub async fn get_workload_identity_token(config: &Config) -> anyhow::Result> { +pub async fn get_workload_identity_token(config: &Config) -> anyhow::Result> { let (token, tenant_id, client_id, authority_host) = match ( &config.federated_token, &config.tenant_id, @@ -61,13 +61,17 @@ pub async fn get_workload_identity_token(config: &Config) -> anyhow::Result, + pub not_before: Option, + pub resource: Option, pub access_token: String, - pub expires_on: String, -} +} \ No newline at end of file From 6d2023f0d466557aa5782104b0d13dc365d09b92 Mon Sep 17 00:00:00 2001 From: congyi <15605187270@163.com> Date: Fri, 12 Apr 2024 13:08:53 +0800 Subject: [PATCH 4/8] fmt --- src/azure/storage/workload_identity_credential.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/azure/storage/workload_identity_credential.rs b/src/azure/storage/workload_identity_credential.rs index 490d57b7..844690a6 100644 --- a/src/azure/storage/workload_identity_credential.rs +++ b/src/azure/storage/workload_identity_credential.rs @@ -57,12 +57,14 @@ pub async fn get_workload_identity_token(config: &Config) -> anyhow::Result, pub resource: Option, pub access_token: String, -} \ No newline at end of file +} From a8034259290b7cf5f35916fd964fa3d51fa5ecdf Mon Sep 17 00:00:00 2001 From: congyi <15605187270@163.com> Date: Mon, 15 Apr 2024 17:05:37 +0800 Subject: [PATCH 5/8] resolve comments --- src/azure/storage/credential.rs | 16 +++++++--------- src/azure/storage/loader.rs | 25 +++++++++++++++++-------- src/azure/storage/signer.rs | 5 ++--- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/azure/storage/credential.rs b/src/azure/storage/credential.rs index d610e585..8768f0f1 100644 --- a/src/azure/storage/credential.rs +++ b/src/azure/storage/credential.rs @@ -1,3 +1,5 @@ +use crate::time::DateTime; + /// Credential that holds the access_key and secret_key. #[derive(Clone)] #[cfg_attr(test, derive(Debug))] @@ -16,7 +18,7 @@ pub enum Credential { /// associated with the subscription that contains the storage account. /// /// ref: - BearerToken(String, String), + BearerToken(String, DateTime), } impl Credential { @@ -26,11 +28,9 @@ impl Credential { return false; } if let Credential::BearerToken(_, expires_on) = self { - if let Ok(expires) = chrono::DateTime::parse_from_rfc3339(expires_on) { - let buffer = chrono::Duration::try_minutes(2).expect("in bounds"); - if expires > (chrono::Utc::now() + buffer) { - return false; - } + let buffer = chrono::TimeDelta::try_minutes(2).expect("in bounds"); + if expires_on > &(chrono::Utc::now() + buffer) { + return false; } }; @@ -43,9 +43,7 @@ impl Credential { account_name.is_empty() || account_key.is_empty() } Credential::SharedAccessSignature(sas_token) => sas_token.is_empty(), - Credential::BearerToken(bearer_token, expire_on) => { - bearer_token.is_empty() || expire_on.is_empty() - } + Credential::BearerToken(bearer_token, _) => bearer_token.is_empty(), } } } diff --git a/src/azure/storage/loader.rs b/src/azure/storage/loader.rs index 337f9822..067cd666 100644 --- a/src/azure/storage/loader.rs +++ b/src/azure/storage/loader.rs @@ -3,6 +3,8 @@ use std::sync::Mutex; use anyhow::Result; +use crate::time::{now, parse_rfc3339}; + use super::credential::Credential; use super::imds_credential; use super::{config::Config, workload_identity_credential}; @@ -72,10 +74,11 @@ impl Loader { async fn load_via_imds(&self) -> Result> { let token = imds_credential::get_access_token("https://storage.azure.com/", &self.config).await?; - let cred = Some(Credential::BearerToken( - token.access_token, - token.expires_on, - )); + let expires_on = match token.expires_on.is_empty() { + true => now() + chrono::TimeDelta::try_minutes(10).expect("in bounds"), + false => parse_rfc3339(&token.expires_on)?, + }; + let cred = Some(Credential::BearerToken(token.access_token, expires_on)); Ok(cred) } @@ -84,10 +87,16 @@ impl Loader { let workload_identity_token = workload_identity_credential::get_workload_identity_token(&self.config).await?; match workload_identity_token { - Some(token) => Ok(Some(Credential::BearerToken( - token.access_token, - token.expires_on.unwrap_or("".to_string()), - ))), + Some(token) => { + let expires_on_duration = match token.expires_on { + None => now() + chrono::TimeDelta::try_minutes(10).expect("in bounds"), + Some(expires_on) => parse_rfc3339(&expires_on)?, + }; + Ok(Some(Credential::BearerToken( + token.access_token, + expires_on_duration, + ))) + } None => Ok(None), } } diff --git a/src/azure/storage/signer.rs b/src/azure/storage/signer.rs index 805decdb..2019ae59 100644 --- a/src/azure/storage/signer.rs +++ b/src/azure/storage/signer.rs @@ -269,9 +269,9 @@ mod tests { use http::Request; use super::super::config::Config; - use crate::azure::storage::loader::Loader; use crate::AzureStorageCredential; use crate::AzureStorageSigner; + use crate::{azure::storage::loader::Loader, time::now}; #[tokio::test] async fn test_sas_url() { @@ -307,8 +307,7 @@ mod tests { .uri("https://test.blob.core.windows.net/testbucket/testblob") .body(()) .unwrap(); - let cred = - AzureStorageCredential::BearerToken("token".to_string(), "expires_on".to_string()); + let cred = AzureStorageCredential::BearerToken("token".to_string(), now()); // Can effectively sign request with SigningMethod::Header assert!(signer.sign(&mut req, &cred).is_ok()); From 26e1ebf0adf918f973b767da04a15a4ff0006e76 Mon Sep 17 00:00:00 2001 From: congyi <15605187270@163.com> Date: Mon, 15 Apr 2024 17:21:06 +0800 Subject: [PATCH 6/8] minor --- src/azure/storage/loader.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/azure/storage/loader.rs b/src/azure/storage/loader.rs index 067cd666..dda61dc7 100644 --- a/src/azure/storage/loader.rs +++ b/src/azure/storage/loader.rs @@ -74,9 +74,10 @@ impl Loader { async fn load_via_imds(&self) -> Result> { let token = imds_credential::get_access_token("https://storage.azure.com/", &self.config).await?; - let expires_on = match token.expires_on.is_empty() { - true => now() + chrono::TimeDelta::try_minutes(10).expect("in bounds"), - false => parse_rfc3339(&token.expires_on)?, + let expires_on = if token.expires_on.is_empty() { + now() + chrono::TimeDelta::try_minutes(10).expect("in bounds") + } else { + parse_rfc3339(&token.expires_on)? }; let cred = Some(Credential::BearerToken(token.access_token, expires_on)); From 1deb304627cbd6de40e4e6cbf2d55c48ea0f41f0 Mon Sep 17 00:00:00 2001 From: congyi <15605187270@163.com> Date: Mon, 15 Apr 2024 18:39:12 +0800 Subject: [PATCH 7/8] fix expires on --- src/azure/storage/credential.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/azure/storage/credential.rs b/src/azure/storage/credential.rs index 8768f0f1..980fee1a 100644 --- a/src/azure/storage/credential.rs +++ b/src/azure/storage/credential.rs @@ -29,7 +29,7 @@ impl Credential { } if let Credential::BearerToken(_, expires_on) = self { let buffer = chrono::TimeDelta::try_minutes(2).expect("in bounds"); - if expires_on > &(chrono::Utc::now() + buffer) { + if expires_on < &(chrono::Utc::now() - buffer) { return false; } }; From dfb59be73f1bd536236e9efa083656c09f3c6282 Mon Sep 17 00:00:00 2001 From: congyi <15605187270@163.com> Date: Tue, 16 Apr 2024 20:04:50 +0800 Subject: [PATCH 8/8] expires on keep the same with sdk --- src/azure/storage/credential.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/azure/storage/credential.rs b/src/azure/storage/credential.rs index 980fee1a..b10363e2 100644 --- a/src/azure/storage/credential.rs +++ b/src/azure/storage/credential.rs @@ -28,8 +28,8 @@ impl Credential { return false; } if let Credential::BearerToken(_, expires_on) = self { - let buffer = chrono::TimeDelta::try_minutes(2).expect("in bounds"); - if expires_on < &(chrono::Utc::now() - buffer) { + let buffer = chrono::TimeDelta::try_seconds(20).expect("in bounds"); + if expires_on < &(chrono::Utc::now() + buffer) { return false; } };