From a66de009e1749a288f2a699186ff3641da75b925 Mon Sep 17 00:00:00 2001 From: dumko2001 Date: Wed, 18 Mar 2026 14:35:44 +0530 Subject: [PATCH 1/5] fix(auth): improve scope selection heuristic (fixes #519) --- src/main.rs | 60 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/src/main.rs b/src/main.rs index 2fe5efc7..9f4feacc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -298,15 +298,42 @@ async fn run() -> Result<(), GwsError> { .map(|_| ()) } -/// Select the best scope from a method's scope list. +/// Select the best scope for the method from its list of alternatives. /// /// Discovery Documents list method scopes as alternatives — any single scope -/// grants access. The first scope is typically the broadest. Using all scopes -/// causes issues when restrictive scopes (e.g., `gmail.metadata`) are included, -/// as the API enforces that scope's restrictions even when broader scopes are -/// also present. +/// grants access. We pick the most appropriate one based on a heuristic: +/// 1. Prefer narrower scopes (e.g., `.readonly`) as they are more likely to +/// be present in the token cache (fixes #519). +/// 2. Avoid restrictive scopes (e.g., `.metadata`) if broader alternatives +/// are available, as the API may enforce the most restrictive scope's +/// limitations even when broader ones are present. pub(crate) fn select_scope(scopes: &[String]) -> Option<&str> { - scopes.first().map(|s| s.as_str()) + if scopes.is_empty() { + return None; + } + + let mut best_scope: Option<&str> = None; + let mut best_priority = 100; + + for scope in scopes { + // Priority mapping (lower is better) + let priority = if scope.contains(".readonly") { + 1 // Most compatible with typical user logins + } else if scope.contains(".metadata") { + 10 // Restrictive, avoid if broader is available + } else if scope.contains("cloud-platform") { + 50 // Extremely broad, avoid if possible + } else { + 5 // Standard service scopes (e.g., drive, gmail.modify) + }; + + if priority < best_priority { + best_priority = priority; + best_scope = Some(scope.as_str()); + } + } + + best_scope.or_else(|| scopes.first().map(|s| s.as_str())) } fn parse_pagination_config(matches: &clap::ArgMatches) -> executor::PaginationConfig { @@ -710,14 +737,31 @@ mod tests { } #[test] - fn test_select_scope_picks_first() { + fn test_select_scope_prefers_readonly() { let scopes = vec![ "https://mail.google.com/".to_string(), "https://www.googleapis.com/auth/gmail.metadata".to_string(), "https://www.googleapis.com/auth/gmail.modify".to_string(), "https://www.googleapis.com/auth/gmail.readonly".to_string(), ]; - assert_eq!(select_scope(&scopes), Some("https://mail.google.com/")); + // .readonly should be preferred over the first one (mail.google.com) + assert_eq!( + select_scope(&scopes), + Some("https://www.googleapis.com/auth/gmail.readonly") + ); + } + + #[test] + fn test_select_scope_avoids_metadata() { + let scopes = vec![ + "https://www.googleapis.com/auth/gmail.metadata".to_string(), + "https://www.googleapis.com/auth/gmail.modify".to_string(), + ]; + // .modify should be preferred over .metadata + assert_eq!( + select_scope(&scopes), + Some("https://www.googleapis.com/auth/gmail.modify") + ); } #[test] From 743fe92219cbef95b5eb29347d746250aaefbfe4 Mon Sep 17 00:00:00 2001 From: dumko2001 Date: Wed, 18 Mar 2026 14:53:58 +0530 Subject: [PATCH 2/5] chore: add changeset for scope negotiation fix --- .changeset/fix-issue-519-multi-scope.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changeset/fix-issue-519-multi-scope.md diff --git a/.changeset/fix-issue-519-multi-scope.md b/.changeset/fix-issue-519-multi-scope.md new file mode 100644 index 00000000..cfc278e9 --- /dev/null +++ b/.changeset/fix-issue-519-multi-scope.md @@ -0,0 +1 @@ +---\n"gws": patch\n---\n\nfix(auth): improve scope selection heuristic to prefer standard/readonly scopes over restrictive ones From 5f2ed398826c8d27235ad48f31232efb60dc65a2 Mon Sep 17 00:00:00 2001 From: dumko2001 Date: Wed, 18 Mar 2026 15:10:45 +0530 Subject: [PATCH 3/5] chore: correct changeset package name --- .changeset/fix-issue-519-multi-scope.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fix-issue-519-multi-scope.md b/.changeset/fix-issue-519-multi-scope.md index cfc278e9..4bb41392 100644 --- a/.changeset/fix-issue-519-multi-scope.md +++ b/.changeset/fix-issue-519-multi-scope.md @@ -1 +1 @@ ----\n"gws": patch\n---\n\nfix(auth): improve scope selection heuristic to prefer standard/readonly scopes over restrictive ones +---\n"@googleworkspace/cli": patch\n---\n\nfix(auth): improve scope selection heuristic to prefer standard/readonly scopes over restrictive ones From 1ea7a0929c9a62e1e8c87985887f45d12527a6b7 Mon Sep 17 00:00:00 2001 From: dumko2001 Date: Wed, 18 Mar 2026 15:18:47 +0530 Subject: [PATCH 4/5] refactor(auth): simplify scope selection heuristic --- src/main.rs | 42 ++++++++++++++++-------------------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/src/main.rs b/src/main.rs index 9f4feacc..43b20e75 100644 --- a/src/main.rs +++ b/src/main.rs @@ -308,32 +308,22 @@ async fn run() -> Result<(), GwsError> { /// are available, as the API may enforce the most restrictive scope's /// limitations even when broader ones are present. pub(crate) fn select_scope(scopes: &[String]) -> Option<&str> { - if scopes.is_empty() { - return None; - } - - let mut best_scope: Option<&str> = None; - let mut best_priority = 100; - - for scope in scopes { - // Priority mapping (lower is better) - let priority = if scope.contains(".readonly") { - 1 // Most compatible with typical user logins - } else if scope.contains(".metadata") { - 10 // Restrictive, avoid if broader is available - } else if scope.contains("cloud-platform") { - 50 // Extremely broad, avoid if possible - } else { - 5 // Standard service scopes (e.g., drive, gmail.modify) - }; - - if priority < best_priority { - best_priority = priority; - best_scope = Some(scope.as_str()); - } - } - - best_scope.or_else(|| scopes.first().map(|s| s.as_str())) + scopes + .iter() + .map(|s| { + let priority = if s.contains(".readonly") { + 1 // Most compatible with typical user logins + } else if s.contains(".metadata") { + 10 // Restrictive, avoid if broader is available + } else if s.contains("cloud-platform") { + 50 // Extremely broad, avoid if possible + } else { + 5 // Standard service scopes (e.g., drive, gmail.modify) + }; + (priority, s.as_str()) + }) + .min_by_key(|(p, _)| *p) + .map(|(_, s)| s) } fn parse_pagination_config(matches: &clap::ArgMatches) -> executor::PaginationConfig { From 43ffb1c60f12d1e67a7be23640b24c23ade18fa7 Mon Sep 17 00:00:00 2001 From: dumko2001 Date: Wed, 18 Mar 2026 15:26:46 +0530 Subject: [PATCH 5/5] refactor(auth): refine scope selection heuristic using ends_with --- src/main.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 43b20e75..24da0f19 100644 --- a/src/main.rs +++ b/src/main.rs @@ -311,14 +311,16 @@ pub(crate) fn select_scope(scopes: &[String]) -> Option<&str> { scopes .iter() .map(|s| { - let priority = if s.contains(".readonly") { + let priority = if s.ends_with(".readonly") { 1 // Most compatible with typical user logins - } else if s.contains(".metadata") { + } else if s.ends_with(".metadata") { 10 // Restrictive, avoid if broader is available } else if s.contains("cloud-platform") { 50 // Extremely broad, avoid if possible + } else if s.contains("googleapis.com/auth/") { + 5 // Specific service scopes (e.g., drive, gmail.modify) } else { - 5 // Standard service scopes (e.g., drive, gmail.modify) + 20 // Broad alias scopes (e.g., https://mail.google.com/) }; (priority, s.as_str()) })