Skip to content

feat(auth): redesign scope picker to show services instead of raw scopes#241

Closed
jpoehnelt wants to merge 3 commits intomainfrom
feat/service-first-scope-picker
Closed

feat(auth): redesign scope picker to show services instead of raw scopes#241
jpoehnelt wants to merge 3 commits intomainfrom
feat/service-first-scope-picker

Conversation

@jpoehnelt
Copy link
Member

Summary

Fixes #234

Redesigns the scope picker from showing raw scope names (drive.metadata.readonly, gmail.compose) to showing services with human-readable labels:

Select services to authorize
──────────────────────────────────────
✨ Recommended       Core consumer APIs: Drive, Gmail, Calendar, Docs, Sheets, Slides, Tasks
🔒 Read Only         Read-only access to all enabled APIs
⚠️ Full Access       Full read/write access to ALL enabled APIs
──────────────────────────────────────
[x] Drive            Manage files, folders, and shared drives · 8 scopes · ⛔ 7 restricted
[x] Gmail            Send, read, and manage email · 5 scopes · ⛔ 6 restricted  · ⚠️ 3 sensitive
[x] Calendar         Manage calendars and events · 2 scopes
[x] Sheets           Read and write spreadsheets · 2 scopes
[ ] Admin            Manage users, groups, and devices · 4 scopes
...

What changed

  1. src/auth_commands.rs: run_discovery_scope_picker() now:

    • Groups scopes by service prefix (first segment of scope short name)
    • Looks up service names/descriptions from SERVICES registry
    • Shows scope count and classification badges (restricted/sensitive) per service
    • Templates (Recommended/Read Only/Full) now select/deselect services
    • Scope→URL resolution happens automatically based on selected services + template
  2. src/setup.rs: Added #[allow(dead_code)] on DiscoveredScope.description (now used at service level rather than per-scope).

Why service-first?

  • Users think in terms of APIs they want to use ("I need Drive and Gmail"), not individual scope permissions
  • Reduces cognitive load from 30+ scope names to ~10 service names
  • Each service shows its description, scope count, and security classification at a glance
  • The template system naturally maps to service selection

Test plan

  • All 462 tests pass
  • cargo clippy -- -D warnings clean
  • CI

Fixes #234

- Group scopes by service prefix (drive, gmail, calendar, etc.)
- Show capitalized service names with descriptions from registry
- Display scope count and classification badges per service
- Templates now select/deselect services, not individual scopes
- Title changed to 'Select services to authorize'
- Scope resolution still produces correct URLs for selected services
@changeset-bot
Copy link

changeset-bot bot commented Mar 6, 2026

🦋 Changeset detected

Latest commit: 6e833ed

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

This PR includes changesets to release 1 package
Name Type
@googleworkspace/cli Minor

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

@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 introduces a significant user experience improvement to the OAuth scope selection process. By shifting from a technical, raw scope list to a service-first interface, users can now intuitively choose the Google APIs they wish to authorize. This change reduces cognitive load and provides clearer context for permissions, making the authorization flow more user-friendly and transparent.

Highlights

  • Redesigned Scope Picker: The OAuth scope picker has been redesigned to present services (e.g., Drive, Gmail) with human-readable labels instead of raw scope names, significantly improving user experience.
  • Enhanced Service Information: Each service now displays its description, total scope count, and security classification badges (restricted/sensitive) directly in the picker.
  • Template Integration: Existing templates (Recommended, Read Only, Full Access) now select or deselect entire services, with automatic resolution to the appropriate underlying OAuth scope URLs.
  • Refactored Scope Logic: The internal logic for run_discovery_scope_picker was refactored to group scopes by service prefix and handle selection and hierarchical deduplication based on service choices.
Changelog
  • .changeset/service-first-scope-picker.md
    • Added a new changeset entry detailing the scope picker redesign.
  • src/auth_commands.rs
    • Refactored run_discovery_scope_picker to group scopes by service prefix.
    • Integrated service descriptions, scope counts, and classification badges into the picker UI.
    • Modified template selection logic to operate on services rather than individual scopes.
    • Updated scope resolution to automatically map selected services and templates to the correct OAuth scope URLs.
  • src/setup.rs
    • Applied #[allow(dead_code)] to the description field of DiscoveredScope as its usage moved to service-level descriptions.
Activity
  • All 462 tests passed.
  • cargo clippy -- -D warnings ran clean.
  • CI is pending.
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.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

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.

@googleworkspace-bot googleworkspace-bot added the cla: yes This human has signed the Contributor License Agreement. label Mar 6, 2026
@codecov
Copy link

codecov bot commented Mar 6, 2026

Codecov Report

❌ Patch coverage is 0% with 119 lines in your changes missing coverage. Please review.
✅ Project coverage is 57.52%. Comparing base (f07dd2b) to head (6e833ed).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
src/auth_commands.rs 0.00% 119 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #241      +/-   ##
==========================================
- Coverage   57.69%   57.52%   -0.18%     
==========================================
  Files          38       38              
  Lines       14328    14370      +42     
==========================================
- Hits         8267     8266       -1     
- Misses       6061     6104      +43     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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 is a great improvement to the user experience, redesigning the scope picker to be service-oriented rather than showing a raw list of scopes. The implementation is well-structured, grouping scopes by service and providing clear information to the user. I've found one critical issue that could lead to a panic under certain data conditions, which should be addressed.

Comment on lines +827 to +837
let label = svc_entry
.map(|e| {
// Capitalize the first alias for display
let name = e.aliases[0];
let mut chars = name.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
}
})
.unwrap_or_else(|| svc_key.clone());
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

There's a potential for a panic here if a ServiceEntry has an empty aliases slice. The code at line 830 accesses e.aliases[0] without checking if the slice is empty. While all current ServiceEntry definitions in src/services.rs have at least one alias, this is fragile and could lead to a crash if a new entry is added incorrectly in the future.

To make the code more robust, you should safely access the first element of the aliases slice.

Suggested change
let label = svc_entry
.map(|e| {
// Capitalize the first alias for display
let name = e.aliases[0];
let mut chars = name.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
}
})
.unwrap_or_else(|| svc_key.clone());
let label = svc_entry
.and_then(|e| {
// Capitalize the first alias for display
e.aliases.first().map(|name| {
let mut chars = name.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
}
})
})
.unwrap_or_else(|| svc_key.clone());

@googleworkspace-bot googleworkspace-bot added the gemini: reviewed Gemini Code Assist has reviewed the latest changes label Mar 6, 2026
Addresses review comments:
- Use aliases.first() instead of aliases[0] to prevent potential
  panic on empty aliases slice.
- Add '🔧 Custom' template option that falls through to the existing
  individual scope picker (run_simple_scope_picker), giving users
  full control over individual scope selection when needed.
@googleworkspace-bot googleworkspace-bot removed the gemini: reviewed Gemini Code Assist has reviewed the latest changes label Mar 6, 2026
@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 user experience of the scope picker by grouping scopes into services. The code is well-structured and the new service-centric approach is much more intuitive. I have one suggestion regarding the default behavior for custom service selections to better align with the principle of least privilege.

Comment on lines 918 to 932
} else if recommended && !full {
// In Recommended mode, use recommended scope selection logic
for s in scopes {
if is_recommended_scope(s, &all_shorts, services_filter.is_some()) {
selected.push(s.url.clone());
}
}
} else {
// Full or custom: include top-level (non-subsumed) scopes
for s in scopes {
if !is_subsumed_scope(&s.short, &all_shorts) {
selected.push(s.url.clone());
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

high

When a user makes a custom selection of services (i.e., not using one of the 'Recommended', 'Read Only', or 'Full Access' templates exclusively), the current logic defaults to selecting the broadest, non-subsumed scopes for each service. This 'full access' default for custom selections might grant more permissions than the user intends, violating the principle of least privilege.

It would be safer to default to 'recommended' scopes for custom selections. This ensures that even when customizing, the user starts with a more secure, curated set of permissions. The 'Full Access' template can still be used explicitly when broad permissions are required.

Suggested change
} else if recommended && !full {
// In Recommended mode, use recommended scope selection logic
for s in scopes {
if is_recommended_scope(s, &all_shorts, services_filter.is_some()) {
selected.push(s.url.clone());
}
}
} else {
// Full or custom: include top-level (non-subsumed) scopes
for s in scopes {
if !is_subsumed_scope(&s.short, &all_shorts) {
selected.push(s.url.clone());
}
}
}
} else if full && !recommended && !readonly {
// Full Access template: include top-level (non-subsumed) scopes
for s in scopes {
if !is_subsumed_scope(&s.short, &all_shorts) {
selected.push(s.url.clone());
}
}
} else {
// Recommended or custom: use recommended scope selection logic
for s in scopes {
if is_recommended_scope(s, &all_shorts, services_filter.is_some()) {
selected.push(s.url.clone());
}
}
}

@googleworkspace-bot googleworkspace-bot added the gemini: reviewed Gemini Code Assist has reviewed the latest changes label Mar 6, 2026
Addresses review: custom service selection now uses recommended
scope logic (principle of least privilege) instead of full access.
Only the explicit Full Access template gets broad scopes.
@googleworkspace-bot googleworkspace-bot removed the gemini: reviewed Gemini Code Assist has reviewed the latest changes label Mar 6, 2026
@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 is a great user experience improvement, redesigning the scope picker to be service-oriented instead of showing a flat list of raw scope names. The implementation is well-structured, grouping scopes by service and providing clear descriptions and badges. However, I found a critical logic issue in how combinations of templates (like 'Read Only' and 'Full Access') are handled, which can lead to an incorrect set of scopes being selected for authorization. My review includes a detailed comment and a suggested fix for this issue.

Comment on lines +883 to 932
// Determine which services are selected
let selected_service_keys: Vec<&str> = if full && !recommended && !readonly {
all_services.iter().map(|s| s.as_str()).collect()
} else if recommended && !full && !readonly {
// Recommended: consumer scopes only (or top-level scopes if filtered).
for entry in &filtered_scopes {
if is_app_only_scope(&entry.url) {
continue;
}
if is_recommended_scope(entry, &all_shorts, services_filter.is_some()) {
selected.push(entry.url.to_string());
}
}
recommended_services.iter().map(|s| s.as_str()).collect()
} else if readonly && !full && !recommended {
for entry in &filtered_scopes {
if is_app_only_scope(&entry.url) {
continue;
}
if entry.is_readonly {
selected.push(entry.url.to_string());
}
}
readonly_services.iter().map(|s| s.as_str()).collect()
} else {
for (i, item) in items.iter().enumerate().skip(template_count) {
if item.selected {
let picker_idx = i - template_count;
if let Some(&scope_idx) = valid_scope_indices.get(picker_idx) {
if let Some(entry) = filtered_scopes.get(scope_idx) {
selected.push(entry.url.to_string());
// Individual service selection
items
.iter()
.enumerate()
.skip(template_count)
.filter(|(_, item)| item.selected)
.filter_map(|(i, _)| {
service_keys_in_order
.get(i - template_count)
.map(|s| s.as_str())
})
.collect()
};

// Map selected services to scope URLs
let mut selected: Vec<String> = Vec::new();
let is_readonly_mode = readonly && !full && !recommended;

for svc_key in &selected_service_keys {
if let Some(scopes) = service_groups.get(*svc_key) {
if is_readonly_mode {
// In Read Only mode, prefer readonly scopes
for s in scopes {
if s.is_readonly {
selected.push(s.url.clone());
}
}
} else if full && !recommended && !readonly {
// Full Access template: include top-level (non-subsumed) scopes
for s in scopes {
if !is_subsumed_scope(&s.short, &all_shorts) {
selected.push(s.url.clone());
}
}
} else {
// Recommended or custom: use recommended scope selection logic
for s in scopes {
if is_recommended_scope(s, &all_shorts, services_filter.is_some()) {
selected.push(s.url.clone());
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

There's a logic issue in how selected services and their corresponding scopes are determined when multiple templates are selected. The current implementation uses mutually exclusive conditions (e.g., full && !recommended && !readonly) for both determining the list of selected services and for choosing which scopes to grant. This approach fails for combinations of templates.

For example, if a user selects both 'Full Access' and 'Read Only', the logic falls through to the final else block, which applies 'Recommended' scope logic. This is likely not the intended behavior. A user would probably expect 'Read Only' to take precedence.

This can be fixed by simplifying the logic:

  1. Always determine the selected services by inspecting the UI state, since the TUI updates the service selections when a template is toggled.
  2. Establish a clear priority for which scope-selection logic to use (e.g., Read Only > Full Access > Recommended).

This ensures that template combinations behave predictably and correctly.

            // Determine which services are selected by looking at the UI state,
            // which is updated by the TUI when templates are toggled.
            let selected_service_keys: Vec<&str> = items
                .iter()
                .enumerate()
                .skip(template_count)
                .filter(|(_, item)| item.selected)
                .filter_map(|(i, _)| {
                    service_keys_in_order
                        .get(i - template_count)
                        .map(|s| s.as_str())
                })
                .collect();

            // Map selected services to scope URLs
            let mut selected: Vec<String> = Vec::new();

            // Establish priority for templates: Read Only > Full Access > Recommended/Custom
            let use_readonly = readonly;
            let use_full = full && !use_readonly;

            for svc_key in &selected_service_keys {
                if let Some(scopes) = service_groups.get(*svc_key) {
                    if use_readonly {
                        // In Read Only mode, prefer readonly scopes
                        for s in scopes {
                            if s.is_readonly {
                                selected.push(s.url.clone());
                            }
                        }
                    } else if use_full {
                        // Full Access mode: include top-level (non-subsumed) scopes
                        for s in scopes {
                            if !is_subsumed_scope(&s.short, &all_shorts) {
                                selected.push(s.url.clone());
                            }
                        }
                    } else {
                        // Recommended or individual selection: use recommended scope selection logic
                        for s in scopes {
                            if is_recommended_scope(s, &all_shorts, services_filter.is_some()) {
                                selected.push(s.url.clone());
                            }
                        }
                    }
                }
            }

@googleworkspace-bot googleworkspace-bot added the gemini: reviewed Gemini Code Assist has reviewed the latest changes label Mar 6, 2026
@jpoehnelt-bot
Copy link
Contributor

Consolidating this thread into the central discussion for the auth UX/user journey work: #245

@jpoehnelt jpoehnelt closed this Mar 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: auth cla: yes This human has signed the Contributor License Agreement. gemini: reviewed Gemini Code Assist has reviewed the latest changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Redesign scope picker: service-first UX with API-enabled badges

3 participants