fix(client): For ADC, send x-goog-user-project header (#215)

* 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.

* Update src/auth.rs

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
Adeel Khan
2026-03-05 18:29:22 -05:00
committed by GitHub
parent 5fa5f60db5
commit 2173a929d3
3 changed files with 42 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@googleworkspace/cli": patch
---
Send x-goog-user-project header when using ADC with a quota_project_id
+30
View File
@@ -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<String> {
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`.
///
@@ -710,4 +724,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()));
}
}
+7
View File
@@ -11,6 +11,13 @@ pub fn build_client() -> Result<reqwest::Client, crate::error::GwsError> {
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(&quota_project) {
headers.insert("x-goog-user-project", header_value);
}
}
reqwest::Client::builder()
.default_headers(headers)
.build()