-
Notifications
You must be signed in to change notification settings - Fork 1k
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
base: main
Are you sure you want to change the base?
Changes from all 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,34 @@ 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()) | ||
| scopes | ||
| .iter() | ||
| .map(|s| { | ||
| let priority = if s.ends_with(".readonly") { | ||
| 1 // Most compatible with typical user logins | ||
| } 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 { | ||
| 20 // Broad alias scopes (e.g., https://mail.google.com/) | ||
| }; | ||
| (priority, s.as_str()) | ||
| }) | ||
| .min_by_key(|(p, _)| *p) | ||
| .map(|(_, s)| s) | ||
| } | ||
|
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 +729,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 function can be simplified and made more robust.
or_elsecall at the end is redundant. If thescopesslice is not empty,best_scopewill always beSomebecausebest_priorityis initialized to 100 and all possible priorities are lower. This makes theor_elseclosure unreachable.iter().min_by_key().ends_withfor.readonlyand.metadatais more robust thancontains, as it prevents accidental matches on substrings that are not at the end of the scope string.Here is a suggested implementation that addresses these points: