diff --git a/.changeset/fix-adc-quota-project.md b/.changeset/fix-adc-quota-project.md new file mode 100644 index 0000000..4c42c53 --- /dev/null +++ b/.changeset/fix-adc-quota-project.md @@ -0,0 +1,5 @@ +--- +"@googleworkspace/cli": patch +--- + +Send x-goog-user-project header when using ADC with a quota_project_id diff --git a/src/auth.rs b/src/auth.rs index a14d48f..c368c5b 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -24,6 +24,20 @@ use anyhow::Context; use crate::credential_store; +/// Returns the `quota_project_id` from Application Default Credentials, if present. +/// This is used to set the `x-goog-user-project` header on API requests. +pub fn get_quota_project() -> Option { + let path = std::env::var("GOOGLE_APPLICATION_CREDENTIALS") + .ok() + .map(PathBuf::from) + .or_else(adc_well_known_path)?; + let content = std::fs::read_to_string(path).ok()?; + let json: serde_json::Value = serde_json::from_str(&content).ok()?; + json.get("quota_project_id") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) +} + /// Returns the well-known Application Default Credentials path: /// `~/.config/gcloud/application_default_credentials.json`. /// @@ -637,4 +651,20 @@ mod tests { .to_string() .contains("No credentials found")); } + + #[test] + #[serial_test::serial] + fn test_get_quota_project_reads_adc() { + let tmp = tempfile::tempdir().unwrap(); + let adc_dir = tmp.path().join(".config").join("gcloud"); + std::fs::create_dir_all(&adc_dir).unwrap(); + std::fs::write( + adc_dir.join("application_default_credentials.json"), + r#"{"quota_project_id": "my-project-123"}"#, + ) + .unwrap(); + + let _home_guard = EnvVarGuard::set("HOME", tmp.path()); + assert_eq!(get_quota_project(), Some("my-project-123".to_string())); + } } diff --git a/src/client.rs b/src/client.rs index eb83885..421ffbb 100644 --- a/src/client.rs +++ b/src/client.rs @@ -11,6 +11,13 @@ pub fn build_client() -> Result { headers.insert("x-goog-api-client", header_value); } + // Set quota project from ADC for billing/quota attribution + if let Some(quota_project) = crate::auth::get_quota_project() { + if let Ok(header_value) = HeaderValue::from_str("a_project) { + headers.insert("x-goog-user-project", header_value); + } + } + reqwest::Client::builder() .default_headers(headers) .build()