From 63b66adbe255ba069fd4d8c9aa7f593ca0390613 Mon Sep 17 00:00:00 2001 From: Adeel Khan Date: Thu, 5 Mar 2026 17:32:17 -0500 Subject: [PATCH 1/2] fix(client): send x-goog-user-project header from ADC quota project When using Application Default Credentials with a quota_project_id set, API requests failed with 403 because the quota project header was never sent. Read quota_project_id from ADC and set it as a default header. --- .changeset/fix-adc-quota-project.md | 5 +++++ src/auth.rs | 27 +++++++++++++++++++++++++++ src/client.rs | 7 +++++++ 3 files changed, 39 insertions(+) create mode 100644 .changeset/fix-adc-quota-project.md 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..364c72f 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -24,6 +24,17 @@ 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 = 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 +648,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() From 39fcea5af7ea3969ded35907d5e3c523de52d237 Mon Sep 17 00:00:00 2001 From: Adeel Khan Date: Thu, 5 Mar 2026 17:41:27 -0500 Subject: [PATCH 2/2] Update src/auth.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/auth.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/auth.rs b/src/auth.rs index 364c72f..c368c5b 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -27,7 +27,10 @@ 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 = adc_well_known_path()?; + 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")