Skip to content

fix(auth): pass all valid method scopes to authenticator#539

Open
dumko2001 wants to merge 5 commits intogoogleworkspace:mainfrom
dumko2001:fix/issue-519-multi-scope-v2
Open

fix(auth): pass all valid method scopes to authenticator#539
dumko2001 wants to merge 5 commits intogoogleworkspace:mainfrom
dumko2001:fix/issue-519-multi-scope-v2

Conversation

@dumko2001
Copy link
Contributor

@dumko2001 dumko2001 commented Mar 18, 2026

Description

Improve scope selection heuristic by implementing a priority-based choice among method alternatives. This prefers narrower, more common scopes (like .readonly) which are more likely to be present in the user's token cache (fixing #519), while still avoiding restrictive .metadata scopes when broader alternatives are available.

This approach is more robust than passing all scopes to the authenticator, as it avoids requesting unnecessary permissions and stays compatible with yup-oauth2's exact-set matching for cached tokens.

Fixes #519

Checklist:

  • My code follows the AGENTS.md guidelines (no generated google-* crates).
  • I have run cargo fmt --all to format the code perfectly.
  • I have run cargo clippy -- -D warnings and resolved all warnings.
  • I have added tests that prove my fix is effective or that my feature works.
  • I have provided a Changeset file (e.g. via pnpx changeset) to document my changes.

@dumko2001 dumko2001 requested a review from jpoehnelt as a code owner March 18, 2026 08:52
@changeset-bot
Copy link

changeset-bot bot commented Mar 18, 2026

🦋 Changeset detected

Latest commit: 43ffb1c

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@googleworkspace-bot googleworkspace-bot added the area: core Core CLI parsing, commands, error handling, utilities label Mar 18, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request enhances the application's authentication mechanism by refining how API scopes are handled. Instead of relying on a single, potentially broad scope, the system now provides all valid scopes for a given method to the authenticator. This change significantly improves the flexibility of token reuse, allowing the application to successfully authenticate with tokens that possess any of the acceptable scopes, thereby resolving issues where authentication previously failed due to scope mismatches.

Highlights

  • Improved Scope Negotiation: The authentication process now passes all valid method scopes to the authenticator, rather than just the first one. This allows the system to reuse existing tokens that might have alternative, more specific scopes, preventing 403 errors.
  • Removed select_scope function: The select_scope helper function, which was responsible for picking only the first scope, has been removed as it is no longer needed with the new approach of passing all scopes.
  • Removed associated tests: Unit tests specifically for the select_scope function have been removed as the function itself was deprecated.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Generative AI Prohibited Use Policy, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request changes the authentication scope handling to pass all available scopes for a method to the authenticator, instead of just the first one. The goal is to improve token reuse. While the code change is straightforward, it appears to re-introduce a previously addressed issue where including restrictive scopes alongside broader ones can lead to unexpected API behavior. I've added a critical comment requesting clarification on why this is no longer a concern, as it could represent a significant regression.

src/main.rs Outdated
Comment on lines +258 to +261
// Select the scopes for the method. Discovery Documents list scopes as
// alternatives (any one grants access). We pass all of them so the authenticator
// can find a cached token that satisfies any of these alternatives.
let scopes: Vec<&str> = method.scopes.iter().map(|s| s.as_str()).collect();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This change passes all available scopes to the authenticator, which differs from the previous implementation that deliberately selected only the first (usually broadest) scope.

The removed select_scope function and associated comments highlighted a key reason for this:

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.

This change appears to re-introduce this potential issue. For example, if an API call is made with a token that has both a broad scope (e.g., gmail.readonly) and a restrictive scope (gmail.metadata), the API might enforce the limitations of the more restrictive scope, causing unexpected failures for operations that should be allowed by the broader scope.

Could you clarify why this is no longer a concern? Has the underlying Google API behavior changed, or is there another mechanism now in place to mitigate this problem? Without addressing this, the change risks introducing a significant regression for some APIs.

@dumko2001 dumko2001 force-pushed the fix/issue-519-multi-scope-v2 branch from c10b7e4 to a66de00 Compare March 18, 2026 09:05
@googleworkspace-bot
Copy link
Collaborator

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request improves the scope selection logic by introducing a priority-based heuristic. The implementation has a small logical issue with a redundant fallback and can be made more robust and idiomatic. I've suggested a refactoring of the select_scope function to improve its clarity, correctness, and robustness.

Comment on lines 310 to 337
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()))
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This function can be simplified and made more robust.

  1. The or_else call at the end is redundant. If the scopes slice is not empty, best_scope will always be Some because best_priority is initialized to 100 and all possible priorities are lower. This makes the or_else closure unreachable.
  2. The logic can be expressed more idiomatically and concisely using iter().min_by_key().
  3. Using ends_with for .readonly and .metadata is more robust than contains, 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:

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())
}

@dumko2001
Copy link
Contributor Author

Reverted the approach of passing all scopes simultaneously, which could trigger unwanted permission requests and exact-set matching issues in yup-oauth2. Instead, implemented a priority-based heuristic in select_scope that prefers narrower, standard scopes (like .readonly) to improve compatibility with existing token caches (fixing #519) while avoiding restrictive metadata scopes when broader alternatives are available.

@googleworkspace-bot
Copy link
Collaborator

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the scope selection logic to be more intelligent, preferring narrower and more common scopes over broad or restrictive ones. This is a good improvement that should fix the issue with token cache misses. The implementation in select_scope is sound, but it can be made more concise and idiomatic using Rust's iterator methods. I've suggested a refactoring that simplifies the code and removes a redundant or_else call. The new tests correctly validate the intended behavior.

src/main.rs Outdated
Comment on lines +311 to +336
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()))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This implementation can be simplified using Iterator::min_by_key to make it more idiomatic and concise. This approach also naturally handles an empty scopes slice by returning None, removing the need for an explicit check.

Additionally, the or_else call in the original implementation is redundant. If scopes is not empty, best_scope is guaranteed to be Some, so the closure in or_else would 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())

@googleworkspace-bot
Copy link
Collaborator

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a more sophisticated heuristic for selecting authentication scopes, which is a great improvement over picking the first one available. My review focuses on a potential robustness issue in the new logic. The use of contains for checking scope suffixes like .readonly could lead to incorrect prioritization. I've suggested a refactoring to use ends_with for more precise matching, which also simplifies the function to be more idiomatic Rust and removes some redundant code.

Comment on lines 310 to 337
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()))
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation for selecting a scope has a potential robustness issue and can be simplified.

  1. Potential Bug: Using scope.contains(".readonly") and scope.contains(".metadata") is not robust. It could incorrectly match scopes that contain these substrings but don't end with them, leading to incorrect priority assignment. For example, a scope like example.readonly.but.not.really would be miscategorized. Using ends_with would be more precise for these cases.

  2. Redundancy: The or_else call on line 336 is redundant. Given the initial best_priority of 100 and the priority values assigned, best_scope will always be Some if the input scopes slice is not empty, making the fallback to scopes.first() unnecessary.

  3. Idiomatic Rust: The function can be written more idiomatically using iterators, which would make it more concise, remove the need for mutable state, and address the other points.

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())
}

@googleworkspace-bot
Copy link
Collaborator

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request improves the OAuth scope selection by introducing a priority-based heuristic. This is a good improvement over the previous implementation which simply picked the first available scope. The new logic prefers more specific and common scopes like .readonly, which is beneficial for security and user experience. I've suggested one improvement to the heuristic to better differentiate between specific scopes and broad alias scopes, further strengthening the principle of least privilege.

src/main.rs Outdated
Comment on lines +314 to +322
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)
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current heuristic for "Standard service scopes" gives the same priority (5) to both specific scopes like .../gmail.modify and very broad alias scopes like https://mail.google.com/. Because min_by_key is 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.

            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.ends_with('/') {
                20 // Broad alias scopes (e.g., https://mail.google.com/), prefer specific scopes
            } else if s.contains("cloud-platform") {
                50 // Extremely broad, avoid if possible
            } else {
                5 // Standard service scopes (e.g., drive, gmail.modify)
            };

@dumko2001
Copy link
Contributor Author

Refined the scope selection heuristic to use ends_with for suffix matching and added differentiation between specific service scopes and broad aliases, as suggested in review.

@googleworkspace-bot
Copy link
Collaborator

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request significantly improves the scope selection heuristic by implementing a priority-based choice among method alternatives. The new select_scope function correctly prioritizes narrower, more common scopes like .readonly while avoiding overly restrictive or broad scopes when better alternatives are available. The updated tests adequately cover the new logic, ensuring its correctness. This change directly addresses the issue of scope selection and enhances compatibility with token caching, which is a valuable improvement.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: core Core CLI parsing, commands, error handling, utilities

Projects

None yet

Development

Successfully merging this pull request may close these issues.

403 insufficientPermissions: Request had insufficient authentication scopes.

2 participants