-
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 4 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,32 @@ 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.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) | ||
| } | ||
|
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 +727,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.
The current heuristic for "Standard service scopes" gives the same priority (5) to both specific scopes like
.../gmail.modifyand very broad alias scopes likehttps://mail.google.com/. Becausemin_by_keyis stable, if a broad alias appears before a more specific scope with the same priority, the broader one will be chosen. This undermines the principle of least privilege that this PR aims to improve.Consider differentiating these broad alias scopes, which often end in a
/, by giving them a lower preference (a higher priority number) than standard specific scopes. This will make the scope selection more robust and ensure the most appropriate, least-privileged scope is chosen.