fix(auth): prioritize local project configuration over global ADC for quota attribution (#295)

* fix(auth): prioritize local project configuration over global ADC for quota attribution

* chore: add changeset for project ID priority fix
This commit is contained in:
Shane Huntley
2026-03-09 11:20:56 +11:00
committed by GitHub
parent 95bb24e9c2
commit 4d41e52198
5 changed files with 80 additions and 5 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@googleworkspace/cli": patch
---
Prioritize local project configuration and `GOOGLE_WORKSPACE_PROJECT_ID` over global Application Default Credentials (ADC) for quota attribution. This fixes 403 errors when the Drive API is disabled in a global gcloud project but enabled in the project configured for gws.
+1 -1
View File
@@ -199,6 +199,6 @@ Use these labels to categorize pull requests and issues:
| Variable | Description |
|---|---|
| `GOOGLE_WORKSPACE_PROJECT_ID` | GCP project ID fallback for `gmail watch` and `events subscribe` helpers (overridden by `--project` flag) |
| `GOOGLE_WORKSPACE_PROJECT_ID` | GCP project ID override for quota/billing and fallback for helper commands (overridden by `--project` flag) |
All variables can also live in a `.env` file (loaded via `dotenvy`).
+1 -1
View File
@@ -316,7 +316,7 @@ All variables are optional. See [`.env.example`](.env.example) for a copy-paste
| `GOOGLE_WORKSPACE_CLI_CONFIG_DIR` | Override config directory (default: `~/.config/gws`) |
| `GOOGLE_WORKSPACE_CLI_SANITIZE_TEMPLATE` | Default Model Armor template |
| `GOOGLE_WORKSPACE_CLI_SANITIZE_MODE` | `warn` (default) or `block` |
| `GOOGLE_WORKSPACE_PROJECT_ID` | GCP project ID fallback for helper commands |
| `GOOGLE_WORKSPACE_PROJECT_ID` | GCP project ID override for quota/billing and fallback for helper commands |
Environment variables can also be set in a `.env` file (loaded via [dotenvy](https://crates.io/crates/dotenvy)).
+72 -2
View File
@@ -24,9 +24,28 @@ 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.
/// Returns the project ID to be used for quota and billing (sets the `x-goog-user-project` header).
///
/// Priority:
/// 1. `GOOGLE_WORKSPACE_PROJECT_ID` environment variable.
/// 2. `project_id` from the OAuth client configuration (`client_secret.json`).
/// 3. `quota_project_id` from Application Default Credentials (ADC).
pub fn get_quota_project() -> Option<String> {
// 1. Explicit environment variable (highest priority)
if let Ok(project_id) = std::env::var("GOOGLE_WORKSPACE_PROJECT_ID") {
if !project_id.is_empty() {
return Some(project_id);
}
}
// 2. Project ID from the OAuth client configuration (set via `gws auth setup`)
if let Ok(config) = crate::oauth_config::load_client_config() {
if !config.project_id.is_empty() {
return Some(config.project_id);
}
}
// 3. Fallback to Application Default Credentials (ADC)
let path = std::env::var("GOOGLE_APPLICATION_CREDENTIALS")
.ok()
.map(PathBuf::from)
@@ -622,6 +641,53 @@ mod tests {
.contains("No credentials found"));
}
#[test]
#[serial_test::serial]
fn test_get_quota_project_priority_env_var() {
let _env_guard = EnvVarGuard::set("GOOGLE_WORKSPACE_PROJECT_ID", "priority-env");
let _adc_guard = EnvVarGuard::remove("GOOGLE_APPLICATION_CREDENTIALS");
let _config_guard = EnvVarGuard::remove("GOOGLE_WORKSPACE_CLI_CONFIG_DIR");
let _home_guard = EnvVarGuard::set("HOME", "/missing/home");
assert_eq!(get_quota_project(), Some("priority-env".to_string()));
}
#[test]
#[serial_test::serial]
fn test_get_quota_project_priority_config() {
let tmp = tempfile::tempdir().unwrap();
let _config_guard =
EnvVarGuard::set("GOOGLE_WORKSPACE_CLI_CONFIG_DIR", tmp.path().to_str().unwrap());
let _env_guard = EnvVarGuard::remove("GOOGLE_WORKSPACE_PROJECT_ID");
let _adc_guard = EnvVarGuard::remove("GOOGLE_APPLICATION_CREDENTIALS");
let _home_guard = EnvVarGuard::set("HOME", "/missing/home");
// Save a client config with a project ID
crate::oauth_config::save_client_config("id", "secret", "config-project").unwrap();
assert_eq!(get_quota_project(), Some("config-project".to_string()));
}
#[test]
#[serial_test::serial]
fn test_get_quota_project_priority_adc_fallback() {
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": "adc-project"}"#,
)
.unwrap();
let _home_guard = EnvVarGuard::set("HOME", tmp.path());
let _env_guard = EnvVarGuard::remove("GOOGLE_WORKSPACE_PROJECT_ID");
let _config_guard = EnvVarGuard::remove("GOOGLE_WORKSPACE_CLI_CONFIG_DIR");
let _adc_guard = EnvVarGuard::remove("GOOGLE_APPLICATION_CREDENTIALS");
assert_eq!(get_quota_project(), Some("adc-project".to_string()));
}
#[test]
#[serial_test::serial]
fn test_get_quota_project_reads_adc() {
@@ -636,6 +702,10 @@ mod tests {
let _home_guard = EnvVarGuard::set("HOME", tmp.path());
let _adc_guard = EnvVarGuard::remove("GOOGLE_APPLICATION_CREDENTIALS");
// Isolate from local environment
let _env_guard = EnvVarGuard::remove("GOOGLE_WORKSPACE_PROJECT_ID");
let _config_guard = EnvVarGuard::remove("GOOGLE_WORKSPACE_CLI_CONFIG_DIR");
assert_eq!(get_quota_project(), Some("my-project-123".to_string()));
}
}
+1 -1
View File
@@ -454,7 +454,7 @@ fn print_usage() {
" GOOGLE_WORKSPACE_CLI_SANITIZE_MODE Sanitization mode: warn (default) or block"
);
println!(
" GOOGLE_WORKSPACE_PROJECT_ID GCP project ID fallback for helper commands"
" GOOGLE_WORKSPACE_PROJECT_ID Override the GCP project ID for quota and billing"
);
println!();
println!("COMMUNITY:");