Google Calendar
Proxy for the Google Calendar v3 REST API. Dual auth by design — the typical production setup runs two managed resources at once:
calendar-userinidp_passthroughmode for human users who want an agent to read their schedule, find free slots, or update their own events.calendar-roomsingoogle_samode, which uses a Workspace service account with domain-wide delegation. For each call it impersonates the calling user, so the user does not link a Google account, but the call still needs a user token. The user is the token'ssubwhen that is an email, otherwise the user's email, and its domain must match the Workspace domain setup field. No setup field names a user to impersonate. Aclient_credentialscall has no user and gets 502Upstream auth failed: No user identity in context; cannot impersonate(GoogleServiceAccountProvider.java). The connector sets no Google scopes for this mode, so the gateway asks Google fordrive.readonlyunless the resource's upstream auth carries ascopeslist.
google_sa set up from the Admin Portal fails in 0.65.0. Every call gets
502 Upstream auth failed: No Google SA key configured, because the form
stores the secret reference under sa_key_env
(ConnectorV1Adapter.java:316-320) and the gateway reads it only from
service_account_key_env (GoogleServiceAccountProvider.java:187-196).
After you create the resource, rename the key with the admin API:
NAME=calendar-rooms # the managed resource's name
curl -s -H "X-Admin-API-Key: $PBAC_ADMIN_API_KEY" "$PBAC_AS_URL/admin/api/managed-resources/$NAME" \
| jq '.upstreamAuth.service_account_key_env = .upstreamAuth.sa_key_env | del(.upstreamAuth.sa_key_env)' \
| curl -s -X PUT -H "X-Admin-API-Key: $PBAC_ADMIN_API_KEY" -H "Content-Type: application/json" \
"$PBAC_AS_URL/admin/api/managed-resources/$NAME" -d @-
To ask Google for other scopes, append
| .upstreamAuth.scopes = ["<scope URL>"] to the jq filter. We ran this
on a test instance with a stub token endpoint standing in for Google. After
the rename, the gateway sent a signed JWT-bearer assertion with the calling
user as sub and drive.readonly, or the scopes we set, as scope, and
forwarded the token it got back to the upstream. A user outside the
Workspace domain got 502 Email domain '...' does not match configured domain. We have not run a google_sa call through to Google.
Overview
Two operator settings shape what an agent can reach. Both apply to every Google Calendar resource on the instance, because both are keyed by connector, not by managed resource:
- A
write_blockedlist on the general file-access control, for calendars an agent may read but not edit (shared team, executive).governedlists the connector's resource type. room_only_mode: true, which refuses a call whosecalendar_idargument isn't a Workspace resource calendar (*@resource.calendar.google.com). A call with nocalendar_idargument, such aslist_calendars, is not refused. Turned on, it applies tocalendar-useras well ascalendar-rooms. It constrains calls made with a Google Calendar token only (see theroom_only_modedeny below).
Default policy
The connector ships policy/calendar.rego
(package pbac.connectors.identos.google_calendar). It has one rule,
room_only_mode. It emits no require_authn_at obligation. Blocked and
read-only calendar ids are operator config on the general file-access
control (see below), not rules here.
room_only_mode deny
deny contains msg if {
input.resource.type == "urn:connector:identos:google-calendar"
data[...].room_only_mode == true
cal := input.context.rs_context.tool_arguments.calendar_id
not endswith(cal, "@resource.calendar.google.com")
...
}
A booking-agent safety net. When enabled, every calendar_id argument must
look like a Workspace resource calendar. The rule reads only that argument, so
a call without one passes it. If the agent (or, more importantly,
a prompt-injected response it's acting on) tries to touch a human
calendar with a Google Calendar token, the request is denied.
The rule keys on input.resource.type (calendar.rego:36-37), so a token
of another connector type that carries the same scope name is not covered.
identos.ms-calendar also names its read scope calendar:read. In our run
with room_only_mode on, a Google Calendar token got 403 on
/calendar/v3/calendars/ceo@example.com/events, and a client_credentials
token issued for the identos.ms-calendar type with calendar:read got 200
on the same Google Calendar resource, over REST and over MCP. See
Check the scope and resource.
Supplying operator data
room_only_mode is the connector's own setting — a suffix test, not an id
set, so it stays connector rego:
KEY=pbac.operator.connectors.identos.google-calendar
curl -X PUT "http://localhost:8080/admin/api/policy-data/by-key?dataKey=$KEY" \
-H "X-Admin-API-Key: $PBAC_ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg k "$KEY" --argjson s '{
"room_only_mode": true
}' \
'{dataKey:$k, payload:($s|tojson)}')"
File access control
Blocking a specific calendar is operator config on the general file-access
control, shared with the Google file connectors — not a rule in this
connector. An identity source matches a calendar by its own id: ids in
blocked are denied on reads and writes, ids in write_blocked are denied on
writes only, so reads still work.
curl -X PUT "http://localhost:8080/admin/api/policy-data/by-key?dataKey=pbac.operator.controls.file_access" \
-H "X-Admin-API-Key: $PBAC_ADMIN_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"dataKey": "pbac.operator.controls.file_access",
"payload": {
"sources": {
"calendars": {
"type": "identity",
"blocked": ["ceo@example.com"],
"write_blocked": ["team-alpha@example.com"]
}
},
"governed": {
"urn:connector:identos:google-calendar": ["calendars"]
}
}
}'
The call replaces the whole row, so keep the sources and governed entries
of every other connector you govern in the payload.
The control reads the id from the calendar_id tool argument and applies to
calendar:read, calendar:events:write and calendar:admin calls alike. Listing a connector under governed is what activates it; omit it
and the control is inert. Full mechanics (pooling, deny-wins, fail-closed) live
in the File Access Control guide.
Scope model
| Scope | Used for | |
|---|---|---|
| PBAC scopes (internal) | calendar:read, calendar:events:write, calendar:admin | Route authorization |
| Upstream OAuth scopes | openid, userinfo.email, calendar.readonly, calendar.events, calendar.acls.readonly | Google-side grants the user consents to (idp_passthrough mode). google_sa mode sets none and gets drive.readonly |
See Google Workspace: multi-connector setup for how calendar's scopes compose with drive and gmail on one Google IdP.
Manifest reference
- ID:
identos.google-calendar - Version:
1.2.0 - Resource type:
urn:connector:identos:google-calendar
Supported auth modes
| Type | Details |
|---|---|
idp_passthrough | requires IdP google |
google_sa | setup fields: sa_key_env, domain |
Setup fields
| ID | Label | Default | Secret? | Notes |
|---|---|---|---|---|
base_url | API base URL | https://www.googleapis.com | no | — |
upstream_auth.type | Authentication | idp_passthrough | no | idp_passthrough forwards each user's own Google OAuth token (recommended). google_sa uses a Workspace service account with domain-wide delegation — useful for shared / room calendars where no human caller is involved. |
sa_key_env | Service account key | — | yes | Pick a secret containing the service-account JSON. Required only for the google_sa auth mode. / shown when upstream_auth.type == 'google_sa' |
domain | Workspace domain | — | no | placeholder: example.com / Required only for the google_sa auth mode. / shown when upstream_auth.type == 'google_sa' |
Scopes
| Scope |
|---|
calendar:read |
calendar:events:write |
calendar:admin |
Routes
| Method | Pattern | Scope | Resource template |
|---|---|---|---|
GET | /calendar/v3/users/me/calendarList | calendar:read | — |
GET | /calendar/v3/calendars/{calendar_id} | calendar:read | calendar://{{calendar_id}} |
GET | /calendar/v3/calendars/{calendar_id}/events | calendar:read | calendar://{{calendar_id}} |
GET | /calendar/v3/calendars/{calendar_id}/events/{event_id} | calendar:read | calendar://{{calendar_id}}/event/{{event_id}} |
POST | /calendar/v3/calendars/{calendar_id}/events | calendar:events:write | calendar://{{calendar_id}} |
PATCH | /calendar/v3/calendars/{calendar_id}/events/{event_id} | calendar:events:write | calendar://{{calendar_id}}/event/{{event_id}} |
DELETE | /calendar/v3/calendars/{calendar_id}/events/{event_id} | calendar:events:write | calendar://{{calendar_id}}/event/{{event_id}} |
POST | /calendar/v3/calendars/{calendar_id}/events/quickAdd | calendar:events:write | calendar://{{calendar_id}} |
POST | /calendar/v3/freeBusy | calendar:read | — |
GET | /calendar/v3/calendars/{calendar_id}/acl | calendar:admin | calendar://{{calendar_id}} |
MCP tools
| Name | Scope | Description |
|---|---|---|
list_calendars | calendar:read | List every calendar the authenticated user has on their calendar list (primary + shared + subscribed). |
get_calendar | calendar:read | Get a calendar's metadata (summary, timezone, description). |
list_events | calendar:read | List events on a calendar. Supports time filters (timeMin/timeMax), single-event expansion, and free-text search. |
get_event | calendar:read | Get a single event by ID. |
create_event | calendar:events:write | Create a new event on a calendar. Provide either the Google Calendar event body (full schema) or the convenience fields summary/start/end/attendees. |
update_event | calendar:events:write | Update an event (partial). Only supplied fields are modified. |
delete_event | calendar:events:write | Delete an event. Recurring events: pass sendUpdates=all to notify attendees. |
quick_add_event | calendar:events:write | Create an event from natural-language text (e.g. "Lunch with Alice Thursday 1pm"). Uses Calendar's quickAdd parser. |
query_free_busy | calendar:read | Query free/busy information for a set of calendars over a time window. Returns only busy intervals — no event details. |
list_acl | calendar:admin | List the access-control rules on a calendar (who it's shared with and how). |
Operator data schema
Keys the operator can supply under data.pbac.operator.connectors["identos.google-calendar"].* — consumed by the connector's policy.
| Key | Type | Description |
|---|---|---|
room_only_mode | boolean | When true, only calendars whose ID ends with @resource.calendar.google.com (Google Workspace resource/room calendars) are addressable. Useful for booking agents that should never touch human calendars. |
Send feedback about this page