From 50c5953e69a93fa35b21f45b0be6f81832041281 Mon Sep 17 00:00:00 2001 From: Rian Stockbower Date: Wed, 13 May 2026 14:56:28 -0400 Subject: [PATCH 1/2] fix(auth): add missing profile scope for people/me verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gro init calls people/me with PersonFields("names,emailAddresses") to verify the People API and power `gro me`. The contacts scope covers the contacts list but not the authenticated user's own profile — Google requires userinfo.profile for that endpoint. Without this scope, gro init always fails with a 403 on the People API verification step, making fresh credential setup impossible. Closes #119 --- internal/auth/auth.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 121f200..fa76c97 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -26,11 +26,13 @@ import ( // The architecture test (TestNoDestructiveAPIMethodsInProductionCode) prevents accidental misuse. // Contacts uses the full contacts scope for group management and starring. // The contacts scope is a superset of contacts.readonly — it includes all read access. +// Profile is required for people/me (names, emailAddresses fields) used by `gro me` and init verification. var AllScopes = []string{ gmail.GmailModifyScope, calendar.CalendarReadonlyScope, calendar.CalendarEventsScope, people.ContactsScope, + people.UserinfoProfileScope, drive.DriveReadonlyScope, drive.DriveMetadataScope, } @@ -43,6 +45,7 @@ var ScopeDescriptions = map[string]string{ calendar.CalendarEventsScope: "Calendar Events — read and update events (RSVP, color). No calendar settings access.", people.ContactsScope: "Contacts — read contacts and groups, plus manage group membership and starring.", people.ContactsReadonlyScope: "Contacts Read-Only — read contacts and groups.", + people.UserinfoProfileScope: "Profile — read the authenticated user's name and email address (required for 'gro me').", drive.DriveReadonlyScope: "Drive Read-Only — read files and metadata.", drive.DriveMetadataScope: "Drive Metadata — read and update file metadata (star/unstar). No file content write access.", } From 3efe5290fa04771d6eb4100be8ec19714b2bca95 Mon Sep 17 00:00:00 2001 From: Rian Stockbower Date: Wed, 13 May 2026 15:02:53 -0400 Subject: [PATCH 2/2] test: update scope guardrails for userinfo.profile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update the hardcoded count in TestAllScopes (6→7) and add the scope assertion. Add userinfo.profile to the architecture test allowlist with a note that it is read-only and scoped to the authenticated user's own profile, not the contacts list. --- internal/architecture/architecture_test.go | 1 + internal/auth/auth_test.go | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/internal/architecture/architecture_test.go b/internal/architecture/architecture_test.go index 2aba48c..bb3367e 100644 --- a/internal/architecture/architecture_test.go +++ b/internal/architecture/architecture_test.go @@ -316,6 +316,7 @@ var allowedScopes = map[string]bool{ "https://www.googleapis.com/auth/calendar.events": true, // RSVP, color (NOT calendar settings) "https://www.googleapis.com/auth/contacts.readonly": true, "https://www.googleapis.com/auth/contacts": true, // group membership, starring (NOT create/delete contacts) + "https://www.googleapis.com/auth/userinfo.profile": true, // read authenticated user's name/email for people/me (NOT contacts list) "https://www.googleapis.com/auth/drive.readonly": true, "https://www.googleapis.com/auth/drive.metadata": true, // star/unstar files (NOT file content write) } diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index d715ef2..fd3eef4 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -101,8 +101,8 @@ func TestDeprecatedWrappers(t *testing.T) { func TestAllScopes(t *testing.T) { t.Parallel() - if len(AllScopes) != 6 { - t.Errorf("got length %d, want %d", len(AllScopes), 6) + if len(AllScopes) != 7 { + t.Errorf("got length %d, want %d", len(AllScopes), 7) } scopeSet := strings.Join(AllScopes, " ") if !strings.Contains(scopeSet, "https://www.googleapis.com/auth/gmail.modify") { @@ -123,6 +123,9 @@ func TestAllScopes(t *testing.T) { if !strings.Contains(scopeSet, "https://www.googleapis.com/auth/drive.metadata") { t.Errorf("expected AllScopes to contain %q", "https://www.googleapis.com/auth/drive.metadata") } + if !strings.Contains(scopeSet, "https://www.googleapis.com/auth/userinfo.profile") { + t.Errorf("expected AllScopes to contain %q", "https://www.googleapis.com/auth/userinfo.profile") + } } func TestCheckScopesMigration_NoGrantedScopes(t *testing.T) {