fix(auth): map People service to contacts/directory scope prefixes (#414)

The People API exposes scopes like `contacts`, `contacts.readonly`,
and `directory.readonly`, none of which start with `people`. When
users ran `gws auth login -s people`, zero scopes matched because
`map_service_to_scope_prefix` returned `"people"` verbatim.

Change `map_service_to_scope_prefix` to `map_service_to_scope_prefixes`
returning a Vec to support services that map to multiple scope
prefixes. Add the `people` → `["contacts", "directory"]` mapping.

Chat scopes (chat.spaces, chat.messages) already matched correctly
since they share the `chat` prefix, but this is now verified by tests.

Closes #310
Closes #316
This commit is contained in:
Anshul Garg
2026-03-14 00:50:56 +05:30
committed by GitHub
parent 8ef27a262c
commit 44767ed8ee
2 changed files with 56 additions and 9 deletions
@@ -0,0 +1,5 @@
---
"@googleworkspace/cli": patch
---
Map People service to `contacts` and `directory` scope prefixes so `gws auth login -s people` includes the required OAuth scopes
+51 -9
View File
@@ -549,18 +549,23 @@ fn scope_matches_service(scope_url: &str, services: &HashSet<String>) -> bool {
let prefix = short.split('.').next().unwrap_or(short);
services.iter().any(|svc| {
let mapped_svc = map_service_to_scope_prefix(svc);
prefix == mapped_svc || short.starts_with(&format!("{mapped_svc}."))
let prefixes = map_service_to_scope_prefixes(svc);
prefixes
.iter()
.any(|mapped| prefix == *mapped || short.starts_with(&format!("{mapped}.")))
})
}
/// Map user-friendly service names to their OAuth scope prefixes.
fn map_service_to_scope_prefix(service: &str) -> &str {
/// Some services map to multiple scope prefixes (e.g. People API uses
/// both `contacts` and `directory` scopes).
fn map_service_to_scope_prefixes(service: &str) -> Vec<&str> {
match service {
"sheets" => "spreadsheets",
"slides" => "presentations",
"docs" => "documents",
s => s,
"sheets" => vec!["spreadsheets"],
"slides" => vec!["presentations"],
"docs" => vec!["documents"],
"people" => vec!["contacts", "directory"],
s => vec![s],
}
}
@@ -1344,8 +1349,11 @@ fn find_unmatched_services(scopes: &[String], services: &HashSet<String>) -> Has
if matched_services.contains(service) {
continue;
}
let mapped_svc = map_service_to_scope_prefix(service);
if prefix == mapped_svc || short.starts_with(&format!("{mapped_svc}.")) {
let prefixes = map_service_to_scope_prefixes(service);
if prefixes
.iter()
.any(|mapped| prefix == *mapped || short.starts_with(&format!("{mapped}.")))
{
matched_services.insert(service.clone());
}
}
@@ -1927,6 +1935,40 @@ mod tests {
));
}
#[test]
fn scope_matches_service_people_contacts() {
let services: HashSet<String> = ["people"].iter().map(|s| s.to_string()).collect();
assert!(scope_matches_service(
"https://www.googleapis.com/auth/contacts",
&services
));
assert!(scope_matches_service(
"https://www.googleapis.com/auth/contacts.readonly",
&services
));
assert!(scope_matches_service(
"https://www.googleapis.com/auth/contacts.other.readonly",
&services
));
assert!(scope_matches_service(
"https://www.googleapis.com/auth/directory.readonly",
&services
));
}
#[test]
fn scope_matches_service_chat() {
let services: HashSet<String> = ["chat"].iter().map(|s| s.to_string()).collect();
assert!(scope_matches_service(
"https://www.googleapis.com/auth/chat.spaces",
&services
));
assert!(scope_matches_service(
"https://www.googleapis.com/auth/chat.messages",
&services
));
}
// ── services filter integration tests ────────────────────────────────
#[test]