Skip to main content

Google Workspace: multi-connector setup

This page is not about a single connector — it's about running identos.google-drive, identos.google-gmail, identos.google-calendar, identos.google-chat and identos.google-directory together off a single Google IdP.

One IdP, many connectors​

The expected setup looks like this:

identity_provider
└── google (registered once, OAuth client_id/client_secret from Google Cloud)

managed_resource
├── drive-user → google-drive + idp_passthrough + google IdP
├── gmail-user → google-gmail + idp_passthrough + google IdP
├── calendar-user → google-calendar + idp_passthrough + google IdP
├── calendar-rooms → google-calendar + google_sa + service account
├── chat-user → google-chat + idp_passthrough + google IdP
└── directory → google-directory+ idp_passthrough + google IdP

A google_sa resource such as calendar-rooms needs one admin API step after you create it in the Admin Portal, or every call gets 502. The Google Calendar page gives the command.

A single Google login grants access to all of them at the same time. A Claude Code session hitting this gateway picks up tools from every one because they're all provisioned against the same authenticated subject.

Why you almost always want google-directory alongside the others​

The rest of the Google APIs identify people by opaque numeric ID. Chat returns members and message senders as users/102770538280811199852 with no name and no email; Drive's list_file_permissions returns permission holders; Calendar attendees arrive as bare addresses needing profile lookup. Two Chat tools — find_direct_message and add_member — require a users/{id} that no Chat tool can produce.

identos.google-directory is the resolver for all of that. Without it, an agent can read a Chat space's whole history and still not be able to tell you who said what.

Google's numeric ID spaces are not interchangeable

Do not assume an ID from one Google API works in another. Observed live: Drive permission IDs are 20 digits and can carry leading zeros (01611141363991203537), while Chat member IDs are 21 digits without (102770538280811199852) — different identifier spaces. google-directory resolves the People API space, which is the one Chat uses.

How scope growth works​

The tricky part is OAuth scopes. Each connector declares upstream_scopes on its idp_passthrough auth option, the Google OAuth scopes its users need. Every one of the five asks for openid and userinfo.email, and then these:

ConnectorGoogle scopes, after https://www.googleapis.com/auth/
Drivedrive.readonly, drive.file, drive.labels.readonly, drive.labels
Gmailgmail.readonly, gmail.send, gmail.modify, gmail.labels
Calendarcalendar.readonly, calendar.events, calendar.acls.readonly
Chatchat.spaces, chat.messages, chat.memberships
Directorydirectory.readonly

When you provision a managed resource in idp_passthrough mode, the gateway automatically merges that connector's upstream_scopes into the target IdP's scopes list (IdpScopeMerger). Subsequent user logins at /authorize pass the union of all those scopes to Google, and the user consents once for everything.

before drive-user provision:    idp.scopes = [openid, email]
after drive-user provision: idp.scopes = [openid, email, userinfo.email,
drive.readonly, drive.file,
drive.labels.readonly, drive.labels]
after gmail-user provision: idp.scopes = [... + gmail.readonly,
gmail.send, ...]
after calendar-user provision: idp.scopes = [... + calendar.readonly,
calendar.events,
calendar.acls.readonly]

Scope names are shortened here. The IdP row holds the full https://www.googleapis.com/auth/... form.

The merge is additive and idempotent — uninstalling a connector does not remove scopes (another managed resource may still need them, and we can't safely attribute scopes to a single installer).

When a user's stored Google token lacks a new scope​

If you install or provision a Google connector after users have already signed in at Google through PolicyArc, their stored Google tokens do not carry the newly added scopes. In idp_passthrough mode the gateway compares the stored token's granted scopes with the ones the connector requires, and the first call that finds one missing returns 401:

{"error": "idp_link_required", "idp": "google", "link_url": "https://<your-environment>/gateway/link?ls=..."}

The message alongside it is "Connect your google account to use this tool." A user who has never linked Google gets the same response.

Two paths out:

  1. Recommended: the user opens the link_url and consents on Google's screen. PolicyArc requests the expanded scope list. No operator action required.
  2. Batch: delete the affected users' stored tokens with DELETE /admin/api/upstream-identity-tokens?subjectId=<sub>&idpId=<id>, where <id> is the IdP's numeric id from GET /admin/api/identity-providers, not its idpId string. A string gets 400. Their next gateway call returns the same link.

Going forward, the rule is: install and provision Google connectors before inviting users to the gateway, and users never see this step.

Pre-configuring the Google OAuth app​

On the Google Cloud side, you still need to add these scopes to the OAuth 2.0 client's OAuth consent screen configuration so Google will permit them. The PBAC auto-merge handles the PBAC side; it can't teach Google about scopes its consent screen hasn't been told to offer.

The scopes the five connectors request in idp_passthrough mode, taken from their manifests. Add only the lines for the connectors you install; the table under How scope growth works says which connector needs which.

openid
https://www.googleapis.com/auth/userinfo.email
https://www.googleapis.com/auth/drive.readonly
https://www.googleapis.com/auth/drive.file
https://www.googleapis.com/auth/drive.labels.readonly
https://www.googleapis.com/auth/drive.labels
https://www.googleapis.com/auth/gmail.readonly
https://www.googleapis.com/auth/gmail.send
https://www.googleapis.com/auth/gmail.modify
https://www.googleapis.com/auth/gmail.labels
https://www.googleapis.com/auth/calendar.readonly
https://www.googleapis.com/auth/calendar.events
https://www.googleapis.com/auth/calendar.acls.readonly
https://www.googleapis.com/auth/chat.spaces
https://www.googleapis.com/auth/chat.messages
https://www.googleapis.com/auth/chat.memberships
https://www.googleapis.com/auth/directory.readonly

Set the consent screen to External → Production (or Internal for a Workspace-only deployment) once you're past testing and want tokens to survive the 7-day expiry.

Two managed resources, one connector​

Running the same connector in both idp_passthrough and google_sa mode requires two managed resources (e.g. calendar-user and calendar-rooms). A single managed resource picks one auth mode at provision time; there's no runtime switch.

Operator data does not tell them apart. room_only_mode is one key in pbac.operator.connectors.identos.google-calendar, so turning it on applies to every Google Calendar resource, calendar-user included. A rule you write yourself can tell the two resources apart by input.context.rs_context.resource_name.

Troubleshooting​

SymptomLikely causeFix
401 with "error": "idp_link_required"The user has no linked Google token, or signed in before the connector was provisionedOpen the link_url, or delete the stored token with the admin API
502 "Upstream auth failed: token resolution requires an authenticated subject"Client used client_credentials (no user context)Issue the call with an authorization_code token. google_sa mode does not avoid this, because it impersonates the calling user and a client_credentials call gets 502 No user identity in context; cannot impersonate
404 on /admin/api/connectors/identos.google-gmail/installJar was built before the connector was addedRebuild the jar (mvn package -DskipTests) and recreate the instance
PDP denies every Gmail send with "recipient domain ... is on the operator denylist"blocked_recipient_domains data is set and a recipient matchesReview the operator-data row pbac.operator.connectors.identos.google-gmail, which Rego reads as data["pbac.operator.connectors.identos.google-gmail"]

See also​