Skip to content

Commit d1e8473

Browse files
committed
fix(search): run live Calendar service search on the crawl scope
PR #8208 widened Google Calendar's domain-wide delegation scopes to include calendar.readonly for every service-account token. Google refuses the whole token exchange when any requested scope is not authorized for the client, so indexed admin-mode Calendar syncs configured for calendar.events.readonly failed every run and were eventually disabled. Live service search only needed the wider scope to read an all-day event's calendar time zone. events.list returns the calendar's timeZone under calendar.events.readonly, so live search now reads it there and delegates with the same single scope as the indexed crawl, matching Drive and Gmail. The admin calendar picker keeps its own calendar.readonly request, as documented.
1 parent 3385594 commit d1e8473

7 files changed

Lines changed: 17 additions & 16 deletions

File tree

‎apps/docs/content/docs/search/google-calendar.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ https://www.googleapis.com/auth/calendar.readonly
2222
https://www.googleapis.com/auth/admin.directory.user.readonly
2323
```
2424

25-
`calendar.readonly` is needed for calendar metadata and the CalendarList picker; event-read scope alone is insufficient. See [CalendarList authorization](https://developers.google.com/workspace/calendar/api/v3/reference/calendarList/list).
25+
Search reads events with `calendar.events.readonly`. The calendar picker also needs `calendar.readonly` to list the delegated administrator's calendars. See [CalendarList authorization](https://developers.google.com/workspace/calendar/api/v3/reference/calendarList/list).
2626

2727
3. In **Settings → Sources → Google Calendar**, choose **Service account**, then **Add connection**. Add or select the Google credential and set **Delegated administrator** to an active Workspace administrator with Directory user-read access.
2828
4. Select calendars, users, a date range, and optional event-text and attendee settings. Save and select this connection as the service source.
@@ -44,7 +44,7 @@ Sim does not copy the organizer's richer event details into an attendee's view o
4444

4545
| Problem | Next step |
4646
| --- | --- |
47-
| Calendar picker or metadata read fails | Include `calendar.readonly` in delegation, along with the other scopes above. |
47+
| Calendar picker fails | Include `calendar.readonly` in delegation, along with the other scopes above. |
4848
| A member gets no matches | Check their personal connection, primary Workspace identity, Users selection, calendars, and source date window. |
4949
| Shared calendar is missing | Confirm both the member and delegated source identity can access that calendar ID. |
5050
| Event is outside the configured range | Adjust the service source's range or narrow the user's query to its allowed window. |

‎apps/sim/connectors/google-calendar/company-crawl.test.ts‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,7 @@ describe('Google Calendar company crawl', () => {
7474
requiredScopes: ['https://www.googleapis.com/auth/calendar'],
7575
adminCredentialType: 'service_account',
7676
adminServiceAccountScopes: ['https://www.googleapis.com/auth/admin.directory.user.readonly'],
77-
serviceAccountDelegationScopes: [
78-
'https://www.googleapis.com/auth/calendar.events.readonly',
79-
'https://www.googleapis.com/auth/calendar.readonly',
80-
],
77+
serviceAccountDelegationScopes: ['https://www.googleapis.com/auth/calendar.events.readonly'],
8178
serviceAccountSubjectFieldId: 'adminEmail',
8279
})
8380
expect(

‎apps/sim/connectors/google-calendar/meta.ts‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,7 @@ export const googleCalendarConnectorMeta: ConnectorMeta = {
1919
adminCredentialType: 'service_account',
2020
serviceAccountScopes: ['https://www.googleapis.com/auth/calendar.events.readonly'],
2121
adminServiceAccountScopes: ['https://www.googleapis.com/auth/admin.directory.user.readonly'],
22-
serviceAccountDelegationScopes: [
23-
'https://www.googleapis.com/auth/calendar.events.readonly',
24-
'https://www.googleapis.com/auth/calendar.readonly',
25-
],
22+
serviceAccountDelegationScopes: ['https://www.googleapis.com/auth/calendar.events.readonly'],
2623
serviceAccountSubjectFieldId: 'adminEmail',
2724
},
2825

‎apps/sim/lib/knowledge/connectors/access-token.test.ts‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -461,10 +461,7 @@ describe.each([
461461
name: 'Google Calendar',
462462
auth: googleCalendarConnectorMeta.auth,
463463
contentScope: 'https://www.googleapis.com/auth/calendar.events.readonly',
464-
delegatedScopes: [
465-
'https://www.googleapis.com/auth/calendar.events.readonly',
466-
'https://www.googleapis.com/auth/calendar.readonly',
467-
],
464+
delegatedScopes: ['https://www.googleapis.com/auth/calendar.events.readonly'],
468465
},
469466
])('$name declared company authentication', ({ auth, contentScope, delegatedScopes }) => {
470467
const directoryScope = 'https://www.googleapis.com/auth/admin.directory.user.readonly'

‎apps/sim/lib/sim-search/live/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ Selected labels are alternatives. Source date/query settings are authoritative r
4848

4949
### Google Calendar
5050

51-
Member mode searches calendars accessible through the member's account. Service mode verifies the account's primary-calendar identity, Directory customer, and source user selection before delegating to that same Workspace user. Selected calendar IDs constrain retrieval and source verification; `primary` means that member's primary calendar. The admin picker browses the delegated administrator's calendar list and stores `primary` as a per-member alias. Domain-wide delegation must authorize both Calendar events read and Calendar read scopes; CalendarList requires the latter ([CalendarList authorization](https://developers.google.com/workspace/calendar/api/v3/reference/calendarList/list)).
51+
Member mode searches calendars accessible through the member's account. Service mode verifies the account's primary-calendar identity, Directory customer, and source user selection before delegating to that same Workspace user. Selected calendar IDs constrain retrieval and source verification; `primary` means that member's primary calendar. The admin picker browses the delegated administrator's calendar list and stores `primary` as a per-member alias. Service search delegates with only `calendar.events.readonly` (plus `admin.directory.user.readonly` for the Directory check), the same scope as the indexed crawl; all-day events take the calendar's time zone from the events list response. The picker additionally needs `calendar.readonly` ([CalendarList authorization](https://developers.google.com/workspace/calendar/api/v3/reference/calendarList/list)).
5252

5353
Each event must exist under the source's delegated token, be in an allowed calendar, not be cancelled, and overlap the source's configured rolling time window. The existing default is 30 days before and after the request. A stable UTC-day envelope around that window is intersected with the user's date bounds in the provider query, ensuring recurring events expand and query bounds stay stable between pages. The exact rolling source window is still checked for each result. Nonoverlapping date ranges return no results; continuations spanning a UTC-day change may require a fresh search. A source search query is checked with Calendar's event search and exact event-ID matching. All-day events use the calendar's timezone. Attendee details follow the source's include-attendees setting. The member's API access still determines which event details they can see.
5454

‎apps/sim/lib/sim-search/live/google-service.test.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,13 @@ describe('Google service source filtering', () => {
269269
container: 'primary',
270270
})
271271
).toBe(true)
272+
expect(delegated.json).toHaveBeenCalledWith(
273+
'/calendar/v3/calendars/reader%40example.com/events',
274+
{
275+
query: { maxResults: '1', fields: 'timeZone' },
276+
}
277+
)
278+
for (const [path] of delegated.json.mock.calls) expect(path).toMatch(/\/events(\/|$)/)
272279
})
273280
it('verifies twenty all-day events within the request budget and refreshes timezones next session', async () => {
274281
vi.setSystemTime(new Date('2026-09-22T02:00:00Z'))

‎apps/sim/lib/sim-search/live/google-service.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,15 @@ export async function createGoogleServiceVerifier(input: {
120120
}
121121
const calendarTimeZones = new Map<string, Promise<string>>()
122122
const calendarWindow = calendarSourceWindow(config, Date.now())
123+
/** An events.list response carries the calendar's time zone under the crawl's events-only scope. */
123124
const calendarTimeZone = (subject: string, calendarId: string, client: NativeClient) => {
124125
const key = JSON.stringify([subject, calendarId])
125126
let pending = calendarTimeZones.get(key)
126127
if (!pending) {
127128
pending = client
128-
.json(`/calendar/v3/calendars/${segment(calendarId)}`)
129+
.json(`/calendar/v3/calendars/${segment(calendarId)}/events`, {
130+
query: { maxResults: '1', fields: 'timeZone' },
131+
})
129132
.then((row) => string(object(row).timeZone))
130133
calendarTimeZones.set(key, pending)
131134
}

0 commit comments

Comments
 (0)