diff --git a/.changeset/fix-auth-login-services.md b/.changeset/fix-auth-login-services.md new file mode 100644 index 00000000..006244dd --- /dev/null +++ b/.changeset/fix-auth-login-services.md @@ -0,0 +1,5 @@ +--- +"@googleworkspace/cli": patch +--- + +Skip the auth scope picker when `gws auth login --services` is provided and accept exact scope short names in the filter. diff --git a/crates/google-workspace-cli/src/auth_commands.rs b/crates/google-workspace-cli/src/auth_commands.rs index d7571e74..6af3a8c5 100644 --- a/crates/google-workspace-cli/src/auth_commands.rs +++ b/crates/google-workspace-cli/src/auth_commands.rs @@ -776,7 +776,10 @@ async fn resolve_scopes( } // Interactive scope picker when running in a TTY - if !cfg!(test) && std::io::IsTerminal::is_terminal(&std::io::stdin()) { + if should_run_interactive_scope_picker(scope_mode, services_filter) + && !cfg!(test) + && std::io::IsTerminal::is_terminal(&std::io::stdin()) + { // If we have a project_id, use discovery-based scope picker (rich templates) if let Some(pid) = project_id { let enabled_apis = crate::setup::get_enabled_apis(pid); @@ -803,6 +806,14 @@ async fn resolve_scopes( result } +fn should_run_interactive_scope_picker( + scope_mode: ScopeMode, + services_filter: Option<&HashSet>, +) -> bool { + matches!(scope_mode, ScopeMode::Default) + && services_filter.is_none_or(|services| services.is_empty()) +} + /// Check if a scope URL belongs to one of the specified services. /// /// Matching is done on the scope's short name (the part after @@ -817,6 +828,10 @@ fn scope_matches_service(scope_url: &str, services: &HashSet) -> bool { .strip_prefix("https://www.googleapis.com/auth/") .unwrap_or(scope_url); + if services.contains(short) { + return true; + } + // cloud-platform is a cross-service scope, always include if short == "cloud-platform" { return true; @@ -2350,6 +2365,32 @@ mod tests { assert_eq!(result, vec!["https://mail.google.com/"]); } + #[test] + fn scope_matches_service_accepts_exact_scope_short_name() { + let services = HashSet::from([String::from("documents.readonly")]); + assert!(scope_matches_service( + "https://www.googleapis.com/auth/documents.readonly", + &services + )); + } + + #[test] + fn interactive_picker_skipped_when_services_filter_is_provided() { + let services = HashSet::from([String::from("gmail")]); + assert!(!should_run_interactive_scope_picker( + ScopeMode::Default, + Some(&services) + )); + } + + #[test] + fn interactive_picker_allowed_for_plain_default_login() { + assert!(should_run_interactive_scope_picker( + ScopeMode::Default, + None + )); + } + #[test] fn filter_restrictive_keeps_metadata_when_only_scope() { let scopes = vec![