Skip to content

Commit fc6bc95

Browse files
zerone0xclaude
andauthored
fix: exclude Workspace-admin-only scopes from Recommended preset (npm#119) (npm#127)
Admin-only scopes (apps.*, cloud-identity.*, ediscovery, directory.readonly, groups) require Workspace domain-admin access and cannot be granted to personal @gmail.com accounts — Google returns 400 invalid_scope when they're included. Changes: - Add is_workspace_admin_scope() helper (mirrors is_app_only_scope()) to identify scopes that fail for personal Google accounts - Exclude these scopes from the template_selects of the 'Recommended' preset in run_discovery_scope_picker() - Exclude them from the resolved scope list when the Recommended template is confirmed - Add 8 unit tests covering the new helper Workspace admins can still access these scopes via 'Full Access' template or by selecting them individually in the picker. Note: this is complementary to PR npm#108 which filters alertcenter scopes at the API-discovery level. This PR handles the broader set at the recommendation layer. Addresses npm#119 (Bug 1: admin scopes in Recommended preset) Co-authored-by: Claude <noreply@anthropic.com>
1 parent 29a029a commit fc6bc95

2 files changed

Lines changed: 127 additions & 2 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@googleworkspace/cli": patch
3+
---
4+
5+
Exclude Workspace-admin-only scopes from the "Recommended" scope preset.
6+
7+
Scopes that require Google Workspace domain-admin access (`apps.*`,
8+
`cloud-identity.*`, `ediscovery`, `directory.readonly`, `groups`) now return
9+
`400 invalid_scope` when used by personal `@gmail.com` accounts. These scopes
10+
are no longer included in the "Recommended" template, preventing login failures
11+
for non-Workspace users.
12+
13+
Workspace admins can still select these scopes manually via the "Full Access"
14+
template or by picking them individually in the scope picker.
15+
16+
Adds a new `is_workspace_admin_scope()` helper (mirroring the existing
17+
`is_app_only_scope()`) that centralises this detection logic.

src/auth_commands.rs

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,10 @@ fn run_discovery_scope_picker(
569569
entry.classification != ScopeClassification::Restricted
570570
};
571571

572-
if is_recommended && !entry.short.starts_with("admin.") {
572+
if is_recommended
573+
&& !entry.short.starts_with("admin.")
574+
&& !is_workspace_admin_scope(&entry.url)
575+
{
573576
recommended_scopes.push(entry.short.to_string());
574577
}
575578
if entry.is_readonly {
@@ -684,14 +687,18 @@ fn run_discovery_scope_picker(
684687
selected.push(entry.url.to_string());
685688
}
686689
} else if recommended && !full && !readonly {
687-
// Recommended: non-restricted + readonly, but exclude admin.* scopes
690+
// Recommended: non-restricted + readonly, but exclude admin.* and
691+
// Workspace-admin-only scopes (require domain admin; fail for @gmail.com).
688692
for entry in relevant_scopes {
689693
if is_app_only_scope(&entry.url) {
690694
continue;
691695
}
692696
if entry.short.starts_with("admin.") {
693697
continue;
694698
}
699+
if is_workspace_admin_scope(&entry.url) {
700+
continue;
701+
}
695702
if entry.is_readonly || entry.classification != ScopeClassification::Restricted
696703
{
697704
selected.push(entry.url.to_string());
@@ -1350,6 +1357,30 @@ fn is_app_only_scope(url: &str) -> bool {
13501357
|| url.contains("/auth/apps.alerts")
13511358
}
13521359

1360+
/// Helper: check if a scope requires Workspace domain admin access and therefore
1361+
/// cannot be granted to personal `@gmail.com` accounts via standard user OAuth.
1362+
///
1363+
/// These scopes are valid in Workspace environments with a domain admin, but
1364+
/// Google returns `400 invalid_scope` when requested by personal accounts.
1365+
/// They are excluded from the "Recommended" preset to avoid login failures.
1366+
///
1367+
/// Affected scope families:
1368+
/// - `apps.*` — Alert Center, Groups Settings, Licensing, Reseller
1369+
/// - `cloud-identity.*` — Cloud Identity: devices, groups, inbound SSO, policies
1370+
/// - `ediscovery` — Google Vault
1371+
/// - `directory.readonly`— Admin SDK Directory (read-only)
1372+
/// - `groups` — Groups Management
1373+
fn is_workspace_admin_scope(url: &str) -> bool {
1374+
let short = url
1375+
.strip_prefix("https://www.googleapis.com/auth/")
1376+
.unwrap_or(url);
1377+
short.starts_with("apps.")
1378+
|| short.starts_with("cloud-identity.")
1379+
|| short == "ediscovery"
1380+
|| short == "directory.readonly"
1381+
|| short == "groups"
1382+
}
1383+
13531384
#[cfg(test)]
13541385
mod tests {
13551386
use super::*;
@@ -1657,4 +1688,81 @@ mod tests {
16571688
let data = r#"{"key":{"access_token":"ya29","refresh_token":"1//tok"}}"#;
16581689
assert_eq!(extract_refresh_token(data), Some("1//tok".to_string()));
16591690
}
1691+
1692+
// ── is_workspace_admin_scope tests ──────────────────────────────────
1693+
1694+
#[test]
1695+
fn is_workspace_admin_scope_apps_alerts() {
1696+
assert!(is_workspace_admin_scope(
1697+
"https://www.googleapis.com/auth/apps.alerts"
1698+
));
1699+
}
1700+
1701+
#[test]
1702+
fn is_workspace_admin_scope_apps_groups_settings() {
1703+
assert!(is_workspace_admin_scope(
1704+
"https://www.googleapis.com/auth/apps.groups.settings"
1705+
));
1706+
}
1707+
1708+
#[test]
1709+
fn is_workspace_admin_scope_apps_licensing() {
1710+
assert!(is_workspace_admin_scope(
1711+
"https://www.googleapis.com/auth/apps.licensing"
1712+
));
1713+
}
1714+
1715+
#[test]
1716+
fn is_workspace_admin_scope_cloud_identity() {
1717+
assert!(is_workspace_admin_scope(
1718+
"https://www.googleapis.com/auth/cloud-identity.groups"
1719+
));
1720+
assert!(is_workspace_admin_scope(
1721+
"https://www.googleapis.com/auth/cloud-identity.devices"
1722+
));
1723+
assert!(is_workspace_admin_scope(
1724+
"https://www.googleapis.com/auth/cloud-identity.policies"
1725+
));
1726+
}
1727+
1728+
#[test]
1729+
fn is_workspace_admin_scope_ediscovery() {
1730+
assert!(is_workspace_admin_scope(
1731+
"https://www.googleapis.com/auth/ediscovery"
1732+
));
1733+
}
1734+
1735+
#[test]
1736+
fn is_workspace_admin_scope_directory_readonly() {
1737+
assert!(is_workspace_admin_scope(
1738+
"https://www.googleapis.com/auth/directory.readonly"
1739+
));
1740+
}
1741+
1742+
#[test]
1743+
fn is_workspace_admin_scope_groups() {
1744+
assert!(is_workspace_admin_scope(
1745+
"https://www.googleapis.com/auth/groups"
1746+
));
1747+
}
1748+
1749+
#[test]
1750+
fn is_workspace_admin_scope_normal_scopes_not_admin() {
1751+
// Consumer/personal-account scopes must NOT be classified as admin-only
1752+
assert!(!is_workspace_admin_scope(
1753+
"https://www.googleapis.com/auth/drive"
1754+
));
1755+
assert!(!is_workspace_admin_scope(
1756+
"https://www.googleapis.com/auth/gmail.modify"
1757+
));
1758+
assert!(!is_workspace_admin_scope(
1759+
"https://www.googleapis.com/auth/calendar"
1760+
));
1761+
assert!(!is_workspace_admin_scope(
1762+
"https://www.googleapis.com/auth/spreadsheets"
1763+
));
1764+
assert!(!is_workspace_admin_scope(
1765+
"https://www.googleapis.com/auth/chat.messages"
1766+
));
1767+
}
16601768
}

0 commit comments

Comments
 (0)