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 | |
|---|---|---|
| Scope | directory.readonly — ordinary user scope | admin.directory.user.readonly — admin only |
Works under idp_passthrough | For every caller | Only 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
| Tool | Direction | Notes |
|---|---|---|
search_directory_people | name/email → ID | The targeted lookup. Prefer this. |
batch_get_people | IDs → profiles | Resolves a whole member list in one call. |
get_person | ID → profile | Single lookup; use batch_get_people for more than one. |
list_directory_people | — | Whole-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
| Scope | Used for | |
|---|---|---|
| PBAC scopes (internal) | directory:read | Targeted resolution — search, get, batch get |
directory:roster | Whole-roster enumeration (list_directory_people) only | |
| Upstream OAuth scope | directory.readonly | The 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
The rule emits a require_authn_at obligation when a token request asks for
a directory: scope and input.subject.idp_provider is not google. It does
not fire today: the subject the AS builds names the IdP as input.subject.idp,
so idp_provider is undefined and the rule body fails. Gmail, Confluence and
Jira ship the same rule with the same gap. Drive and Calendar ship none.
What does stop a caller without a Google link is the gateway. Before it
forwards a call it checks for a usable linked Google token, and without one it
answers 401 with "error": "idp_link_required" and a link_url.
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 the blocked id lists 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. Over the REST proxy the
gateway merges the query string into the tool arguments
(GatewayRestArguments.java), so ?personFields= is checked the same way as
the MCP argument. Once an allow-list is configured, a request that names no
mask at all is denied, because policy cannot tell which fields it would
return.
On a local instance with allowed_person_fields set to ["names"],
/gateway/<name>/v1/people/123?personFields=names returned 200,
?personFields=birthdays returned 403, and the same path with no
personFields returned 403.
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/user%40example.com/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.1.0 - Resource type:
urn:connector:identos:google-directory
Supported auth modes
| Type | Details |
|---|---|
idp_passthrough | requires IdP google |
Setup fields
| ID | Label | Default | Secret? | Notes |
|---|---|---|---|---|
base_url | API base URL | https://people.googleapis.com | no | — |
Scopes
| Scope |
|---|
directory:read |
directory:roster |
Routes
| Method | Pattern | Scope | Resource template |
|---|---|---|---|
GET | /v1/people:searchDirectoryPeople | directory:read | — |
GET | /v1/people:batchGet | directory:read | — |
GET | /v1/people/{person_id} | directory:read | person://{{person_id}} |
GET | /v1/people:listDirectoryPeople | directory:roster | — |
MCP tools
| Name | Scope | Description |
|---|---|---|
search_directory_people | directory:read | Search 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_people | directory:read | Resolve 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_person | directory:read | Get a single directory profile by its numeric person ID. Use batch_get_people instead when resolving more than one. |
list_directory_people | directory:roster | Enumerate 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.
| Key | Type | Description |
|---|---|---|
allowed_person_fields | array | Allow-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_size | number | Per-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_ids | array | Person 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. |
Send feedback about this page