-
Notifications
You must be signed in to change notification settings - Fork 1.2k
fix(auth): pass all valid method scopes to authenticator #539
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
a66de00
743fe92
5f2ed39
1ea7a09
43ffb1c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| ---\n"@googleworkspace/cli": patch\n---\n\nfix(auth): improve scope selection heuristic to prefer standard/readonly scopes over restrictive ones |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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())) | ||
| } | ||
|
Comment on lines
310
to
329
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function can be simplified and made more robust.
Here is a suggested implementation that addresses these points: pub(crate) fn select_scope(scopes: &[String]) -> Option<&str> {
scopes
.iter()
.min_by_key(|scope| {
// Using `ends_with` is more robust for suffixes like `.readonly` and `.metadata`
// to avoid accidentally matching them in the middle of a scope segment.
match scope.as_str() {
s if s.ends_with(".readonly") => 1, // Most compatible with typical user logins
s if s.ends_with(".metadata") => 10, // Restrictive, avoid if broader is available
s if s.contains("cloud-platform") => 50, // Extremely broad, avoid if possible
_ => 5, // Standard service scopes
}
})
.map(|s| s.as_str())
}
Comment on lines
310
to
329
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current implementation for selecting a scope has a potential robustness issue and can be simplified.
Here is a suggested refactoring that addresses these points: pub(crate) fn select_scope(scopes: &[String]) -> Option<&str> {
scopes
.iter()
.min_by_key(|scope| {
if scope.ends_with(".readonly") {
1 // Most compatible with typical user logins
} else if scope.ends_with(".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)
}
})
.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] | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This implementation can be simplified using
Iterator::min_by_keyto make it more idiomatic and concise. This approach also naturally handles an emptyscopesslice by returningNone, removing the need for an explicit check.Additionally, the
or_elsecall in the original implementation is redundant. Ifscopesis not empty,best_scopeis guaranteed to beSome, so the closure inor_elsewould never be executed.For improved readability and maintainability, consider defining the priority values as constants.
scopes .iter() .min_by_key(|scope| { // Priority mapping (lower is better) 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) } }) .map(|s| s.as_str())