Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 29 additions & 12 deletions crates/google-workspace-cli/src/auth_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -810,16 +810,18 @@ async fn resolve_scopes(
/// short name equals the service or starts with `service.` (e.g. service
/// `drive` matches `drive`, `drive.readonly`, `drive.metadata.readonly`).
///
/// The `cloud-platform` scope always passes through since it's a
/// cross-service platform scope.
/// The `cloud-platform` scope is a cross-service platform scope, but it's only
/// included if explicitly selected or if no services filter is active.
fn scope_matches_service(scope_url: &str, services: &HashSet<String>) -> bool {
let short = scope_url
.strip_prefix("https://www.googleapis.com/auth/")
.unwrap_or(scope_url);

// cloud-platform is a cross-service scope, always include
// cloud-platform is cross-service but should only be included if:
// 1. No services filter is active (all services -> include cloud-platform)
// 2. Cloud-platform is explicitly selected by the user
if short == "cloud-platform" {
return true;
return services.is_empty() || services.contains("cloud-platform");
}

let prefix = short.split('.').next().unwrap_or(short);
Expand Down Expand Up @@ -930,7 +932,7 @@ fn run_discovery_scope_picker(
relevant_scopes: &[crate::setup::DiscoveredScope],
services_filter: Option<&HashSet<String>>,
) -> Option<Vec<String>> {
use crate::setup::{ScopeClassification, PLATFORM_SCOPE};
use crate::setup::ScopeClassification;
use crate::setup_tui::{PickerResult, SelectItem};

let mut recommended_scopes = vec![];
Expand Down Expand Up @@ -1102,11 +1104,6 @@ fn run_discovery_scope_picker(
}
}

// Always include cloud-platform scope
if !selected.contains(&PLATFORM_SCOPE.to_string()) {
selected.push(PLATFORM_SCOPE.to_string());
}

// Hierarchical dedup: if we have both a broad scope (e.g. `.../auth/drive`)
// and a narrower scope (e.g. `.../auth/drive.metadata`, `.../auth/drive.readonly`),
// drop the narrower one since the broad scope subsumes it.
Expand Down Expand Up @@ -2184,11 +2181,31 @@ mod tests {
}

#[test]
fn scope_matches_service_cloud_platform_always_matches() {
fn scope_matches_service_cloud_platform_only_matches_when_explicitly_selected() {
// #918 regression: cloud-platform must NOT match when the services
// filter is narrowed (e.g. `--services gmail`), because it grants
// read/write/delete across all GCP resources.
let services: HashSet<String> = ["drive"].iter().map(|s| s.to_string()).collect();
assert!(
!scope_matches_service("https://www.googleapis.com/auth/cloud-platform", &services),
"cloud-platform must not pass through a narrowed services filter"
);

// It still matches when the filter explicitly includes it.
let with_platform: HashSet<String> = ["drive", "cloud-platform"]
.iter()
.map(|s| s.to_string())
.collect();
assert!(scope_matches_service(
"https://www.googleapis.com/auth/cloud-platform",
&services
&with_platform
));

// Empty services filter (no narrowing) keeps legacy pass-through.
let empty: HashSet<String> = HashSet::new();
assert!(scope_matches_service(
"https://www.googleapis.com/auth/cloud-platform",
&empty
));
}

Expand Down
1 change: 0 additions & 1 deletion crates/google-workspace-cli/src/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,6 @@ pub enum ScopeClassification {
Restricted,
}

pub const PLATFORM_SCOPE: &str = "https://www.googleapis.com/auth/cloud-platform";

/// A scope discovered from a Discovery Document.
#[derive(Clone)]
Expand Down
Loading