Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-quota-project-priority.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)).
Expand Down
74 changes: 72 additions & 2 deletions src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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() {
Expand All @@ -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()));
}
}
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:");
Expand Down
Loading