From 5aab65f7acb6c04cd8d029a47ae333c40c64595e Mon Sep 17 00:00:00 2001 From: Vijeth-Rai Date: Mon, 20 Jul 2026 10:43:51 +0530 Subject: [PATCH 1/2] fix: don't use oauth client project_id as quota project --- .changeset/no-quota-project-for-oauth.md | 14 +++++++++ crates/google-workspace-cli/src/auth.rs | 36 ++++++++++++++++-------- 2 files changed, 38 insertions(+), 12 deletions(-) create mode 100644 .changeset/no-quota-project-for-oauth.md diff --git a/.changeset/no-quota-project-for-oauth.md b/.changeset/no-quota-project-for-oauth.md new file mode 100644 index 00000000..b2a74e7f --- /dev/null +++ b/.changeset/no-quota-project-for-oauth.md @@ -0,0 +1,14 @@ +--- +"@googleworkspace/cli": patch +--- + +Stop sending `x-goog-user-project` derived from the OAuth client's `project_id` + +The `project_id` in `client_secret.json` was used as the quota project on every request. +The API only honors that header when the authenticated end user holds +`serviceusage.services.use` on the project, so users who are not IAM members of it +received a 403 on every call. Quota for end-user OAuth credentials is already attributed +via the OAuth client ID. + +The header is still sent for ADC credentials with a `quota_project_id`, and can be set +explicitly via `GOOGLE_WORKSPACE_PROJECT_ID`. diff --git a/crates/google-workspace-cli/src/auth.rs b/crates/google-workspace-cli/src/auth.rs index 9d8847e4..3609d57f 100644 --- a/crates/google-workspace-cli/src/auth.rs +++ b/crates/google-workspace-cli/src/auth.rs @@ -84,8 +84,10 @@ async fn refresh_token_with_reqwest( /// /// 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). +/// 2. `quota_project_id` from Application Default Credentials (ADC). +/// +/// The `project_id` from the OAuth client configuration (`client_secret.json`) is not +/// used: quota for end-user OAuth credentials is attributed via the OAuth client ID. pub fn get_quota_project() -> Option { // 1. Explicit environment variable (highest priority) if let Ok(project_id) = std::env::var("GOOGLE_WORKSPACE_PROJECT_ID") { @@ -94,14 +96,7 @@ pub fn get_quota_project() -> Option { } } - // 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) + // 2. Fallback to Application Default Credentials (ADC) let path = std::env::var("GOOGLE_APPLICATION_CREDENTIALS") .ok() .map(PathBuf::from) @@ -936,7 +931,7 @@ mod tests { #[test] #[serial_test::serial] - fn test_get_quota_project_priority_config() { + fn test_get_quota_project_ignores_client_config() { let tmp = tempfile::tempdir().unwrap(); let _config_guard = EnvVarGuard::set( "GOOGLE_WORKSPACE_CLI_CONFIG_DIR", @@ -949,7 +944,24 @@ mod tests { // 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())); + assert_eq!(get_quota_project(), None); + } + + #[test] + #[serial_test::serial] + fn test_get_quota_project_env_var_overrides_client_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::set("GOOGLE_WORKSPACE_PROJECT_ID", "explicit-project"); + let _adc_guard = EnvVarGuard::remove("GOOGLE_APPLICATION_CREDENTIALS"); + let _home_guard = EnvVarGuard::set("HOME", "/missing/home"); + + crate::oauth_config::save_client_config("id", "secret", "config-project").unwrap(); + + assert_eq!(get_quota_project(), Some("explicit-project".to_string())); } #[test] From 7fbd29a5a5881cc7f71bc5d57a95855b1e1da1b7 Mon Sep 17 00:00:00 2001 From: Vijeth-Rai Date: Mon, 20 Jul 2026 11:00:32 +0530 Subject: [PATCH 2/2] test: point adc env at a missing path instead of unsetting it --- crates/google-workspace-cli/src/auth.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/google-workspace-cli/src/auth.rs b/crates/google-workspace-cli/src/auth.rs index 3609d57f..0a6a39f3 100644 --- a/crates/google-workspace-cli/src/auth.rs +++ b/crates/google-workspace-cli/src/auth.rs @@ -938,8 +938,10 @@ mod tests { 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"); + let _adc_guard = EnvVarGuard::set( + "GOOGLE_APPLICATION_CREDENTIALS", + tmp.path().join("missing-adc.json").to_str().unwrap(), + ); // Save a client config with a project ID crate::oauth_config::save_client_config("id", "secret", "config-project").unwrap(); @@ -956,8 +958,10 @@ mod tests { tmp.path().to_str().unwrap(), ); let _env_guard = EnvVarGuard::set("GOOGLE_WORKSPACE_PROJECT_ID", "explicit-project"); - let _adc_guard = EnvVarGuard::remove("GOOGLE_APPLICATION_CREDENTIALS"); - let _home_guard = EnvVarGuard::set("HOME", "/missing/home"); + let _adc_guard = EnvVarGuard::set( + "GOOGLE_APPLICATION_CREDENTIALS", + tmp.path().join("missing-adc.json").to_str().unwrap(), + ); crate::oauth_config::save_client_config("id", "secret", "config-project").unwrap();