Skip to main content
Version: Latest

Google Directory

Read-only proxy for the Google People API directory endpoints. Its job is to resolve the opaque principal IDs every other Google connector hands back.

Chat, for example, returns members and message senders as bare numeric IDs and nothing else:

{ "member": { "name": "users/102770538280811199852", "type": "HUMAN" } }

No email, no display name — the Chat API does not return member email addresses and omits displayName for humans. DM spaces come back with no displayName and no participant list at all. And two shipped Chat tools, find_direct_message and add_member, require a users/{id} that no Chat tool can produce. So "DM Bruno" is unreachable without this connector.

The same gap shows up in Calendar (attendee emails needing profiles), Drive (list_file_permissions returns permission holders) and Docs (comment authors). This is shared infrastructure, not a Chat patch.

Why People API and not Admin SDK

Both can resolve a principal. The deciding constraint is that Google directory reads here are passthrough-only:

People API (used here)Admin SDK Directory
Scopedirectory.readonly — ordinary user scopeadmin.directory.user.readonlyadmin only
Works under idp_passthroughFor every callerOnly if the caller is a Workspace admin

Under a passthrough connector the Admin SDK would resolve identities for admins and fail for everyone else — the wrong shape for a primitive the rest of the Google family depends on. There is deliberately no google_sa mode: service-account directory reads would let an agent enumerate the roster with no human in the loop, which is the main risk this connector is built to bound.

Tools

ToolDirectionNotes
search_directory_peoplename/email → IDThe targeted lookup. Prefer this.
batch_get_peopleIDs → profilesResolves a whole member list in one call.
get_personID → profileSingle lookup; use batch_get_people for more than one.
list_directory_peopleWhole-roster enumeration. Separate scope.

batch_get_people is the one to reach for when attributing a Chat member or sender list. An agent that loops get_person instead produces N audit rows and N chances to drift past a page cap.

Both directory-wide tools need an explicit sources argument — the People API refuses a call that names no source (HTTP 400 Must request at least one source). DIRECTORY_SOURCE_TYPE_DOMAIN_PROFILE is the Workspace staff directory; add DIRECTORY_SOURCE_TYPE_DOMAIN_CONTACT for the domain's shared external contacts. The schema marks it required, so a client supplies it without prior knowledge.

Scope model

ScopeUsed for
PBAC scopes (internal)directory:readTargeted resolution — search, get, batch get
directory:rosterWhole-roster enumeration (list_directory_people) only
Upstream OAuth scopedirectory.readonlyThe Google-side grant the user consents to

The split is the primary enumeration control, and it is stronger than any rego rule: don't grant directory:roster and whole-roster reads are refused by the gateway's own scope check. Targeted resolution — the actual use case — needs only directory:read.

Default policy

Ships policy/directory.rego, package pbac.connectors.identos.google_directory. Four things:

1. IdP-routing subject obligation

Same shape as drive/gmail/calendar — a non-Google session in idp_passthrough mode triggers a require_authn_at obligation.

2. allowed_person_fields allow-list

The People API field mask decides whether a directory read returns "name and work email" or "name, home address, birthday and phone". This rule denies any request whose readMask / personFields names a field outside the list.

An allow-list, not a deny-list — deliberately breaking the convention set by private_calendar_ids / blocked_space_ids elsewhere. A deny-list fails open on People API fields nobody enumerated: birthdays, genders, addresses, phoneNumbers, biographies, relations, and whatever Google adds next. Cost: adding a legitimately-needed field takes an operator edit.

Recommended value: ["names", "emailAddresses", "organizations", "photos"] — exactly enough to answer "who is users/1027705…".

Empty or unset means no restriction. That is the intended observe-mode posture for a first rollout.

3. Fail-closed on an invisible field mask

Rule 2 reads the mask out of the tool arguments. Direct-HTTP callers reach the gateway with no tool-arguments object at all, so their mask is invisible and rule 2 would silently pass them.

So once an allow-list is configured, a request whose mask policy cannot see is denied. The practical effect: with an allow-list set, this connector is MCP-path-only/gateway/google-directory/** via the raw HTTP proxy returns a policy denial. That is a real functional restriction and it is the right trade for a directory: the alternative is an allow-list that anyone can step around by changing transport.

4. max_page_size

Caps pageSize on a single call. It does not bound cumulative paging — policy is evaluated per request and has no memory, so a caller can page fifty times under any cap. Bounding that needs a rate-limit obligation, which the gateway does not have.

Partial control: blocked_person_ids

Shields individuals on the ID-addressed routes (get_person, batch_get_people) only. It cannot remove a shielded person from search_directory_people or list_directory_people results — that needs response-body filtering, and the only response-side primitive available (filter_words) does whole-word literal replacement and cannot target a JSON field. Labelled partial here rather than implied to be complete.

Both sides are normalized to the resource name's last segment, so the caller cannot step around the shield by switching spelling: policy sees the original tool arguments (GatewayCallRequest passes args verbatim), while the gateway happily strips a people/ prefix off a bare-ID path param before calling upstream — so the two spellings address the same profile and must compare equal.

Supplying operator data

KEY=pbac.operator.connectors.identos.google-directory

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 '{
"allowed_person_fields": ["names", "emailAddresses", "organizations", "photos"],
"max_page_size": 100,
"blocked_person_ids": ["102770538280811199852"]
}' \
'{dataKey:$k, payload:($s|tojson)}')"

blocked_person_ids accepts either spelling — 102770538280811199852 or people/102770538280811199852. Both shield the same person, because the gateway accepts either form on get_person's person_id.

Operational notes

Directory lookups record who looked up whom. A search_directory_people query argument is a person's name, and tool_arguments is on the C7 rs_context audit allow-list (RsContextProperties.DEFAULT_ALLOWED_KEYS), which is flat — so the query is persisted to the introspect audit row rather than dropped.

This is accepted, not overlooked. Removing tool_arguments from the allow-list would not stop personal data reaching audit_log, because identifiers are routinely path segments and GatewayAuditContext records the path:

/gateway/google-calendar/calendar/v3/calendars/alec%40identos.ca/events

So the correct posture is that audit_log is a PII store and should be governed as one — retention and access controls, not per-field redaction. Directory rows make it sharper, since the aggregate is a social graph of who looked up whom, but they do not create the category.

Google's numeric ID spaces are not interchangeable. People API's people/{id} is Chat's users/{id} — confirmed live, which is what makes this connector work. But Drive permission IDs are a different space: Drive returns 20-digit IDs with leading zeros (01611141363991203537) where Chat returns 21 digits without (102770538280811199852). Verify before assuming an ID from one Google API is usable in another.

Full resource names are accepted where a bare ID is expected. Passing people/102770538280811199852 to get_person works, as does the bare 102770538280811199852 — the gateway strips a redundant collection prefix, so batch_get_people output can be handed straight to get_person. Same for Chat's spaces/... values.

See Google Workspace: multi-connector setup for how this composes with drive, gmail, calendar and chat on one Google IdP.

Manifest reference

  • ID: identos.google-directory
  • Version: 1.0.0
  • Resource type: urn:connector:identos:google-directory

Supported auth modes

TypeDetails
idp_passthroughrequires IdP google

Setup fields

IDLabelDefaultSecret?Notes
base_urlAPI base URLhttps://people.googleapis.comno

Scopes

Scope
directory:read
directory:roster

Routes

MethodPatternScopeResource template
GET/v1/people:searchDirectoryPeopledirectory:read
GET/v1/people:batchGetdirectory:read
GET/v1/people/{person_id}directory:readperson://{{person_id}}
GET/v1/people:listDirectoryPeopledirectory:roster

MCP tools

NameScopeDescription
search_directory_peopledirectory:readSearch the Workspace directory by free text (name, email, title) and return matching profiles. This is the email-or-name to ID direction — use it to turn 'Bruno' into a people/{id} you can pass to Chat's find_direct_message or add_member. Targeted lookup: prefer this over list_directory_people.
batch_get_peopledirectory:readResolve many principal IDs to profiles in ONE call. This is the ID to human direction and the preferred way to attribute a member or sender list — pass every resourceName at once rather than looping get_person, which multiplies audit rows and page-cap risk. resourceNames take the form people/{id}; the {id} is the same numeric principal ID Chat returns as users/{id}.
get_persondirectory:readGet a single directory profile by its numeric person ID. Use batch_get_people instead when resolving more than one.
list_directory_peopledirectory:rosterEnumerate the entire Workspace directory, paginated. Whole-roster read — requires the separate directory:roster scope, which most clients should not be granted. Prefer search_directory_people for any task that only needs specific people.

Operator data schema

Keys the operator can supply under data.pbac.operator.connectors["identos.google-directory"].* — consumed by the connector's policy.

KeyTypeDescription
allowed_person_fieldsarrayAllow-list of People API person fields this connector may request. When non-empty, any request whose readMask / personFields names a field outside the list is denied, AND any request whose field mask is not visible to policy is denied (fail-closed). Leave empty for observe mode — no field restriction. Recommended: ["names","emailAddresses","organizations","photos"].
max_page_sizenumberPer-request cap on pageSize. Bounds a single enumeration call; it does NOT bound cumulative paging (policy is stateless per request — see the connector README). Omit or set 0 for no cap.
blocked_person_idsarrayPerson IDs shielded from get_person and batch_get_people — executives, at-risk staff. Either spelling works: the bare numeric ID (102770538280811199852) or the People API resource name (people/102770538280811199852). NOTE: this is a partial control. It cannot suppress a blocked individual from search_directory_people or list_directory_people results, because that needs response filtering the gateway cannot do.