File Access Control
An agent reaching a file connector (Google Drive, Docs, Sheets, Slides,
Confluence) through the managed gateway can otherwise list and read every
file its token can reach. oauth.file_access lets an operator restrict
what the gateway actually hands back, using one general control with two
typed sources of file facts and a single predicate applied over all of
them at once.
Configure it under the operator key pbac.operator.controls.file_access.
When to use it
Use this control when you need to gate file-shaped resources by something the file itself carries — a classification label — or by which folder/drive it lives in. It is the general mechanism: a connector declares which of its routes are file-shaped and how to resolve a file's metadata, and this control decides. Connectors are not expected to hand-roll their own per-file allow and deny lists alongside it.
Source types
Each source resolves a set of values for a file. Only the value space differs:
-
label— values are the label IDs carried by the file (its own labels only; there is no folder/parent inheritance). -
location— values are the file's container IDs: its parent folder ID(s) plus its shared-drive ID. A folder ID and a drive ID are both opaque container IDs to this control; there's no folder-vs-drive split in howrequired/blockedare matched. -
identity— the value is the resource's own ID, nothing else. This is how you block one specific thing: a document, a Chat space, a calendar. It needs no resolver and makes no upstream call, so it works on connectors that carry no metadata at all.Either ID form worksGoogle's APIs return a qualified resource name —
spaces/AAA,people/123— and that is what you will copy out of a UI or a response. The gateway sees the bare tool argument (AAA), because the route is/v1/spaces/{space_id}. Anidentitysource matches on both forms, on both sides, so either is correct and you do not have to know which one the route uses.This applies to
identityonly. A label ID is opaque and may legitimately contain a slash, so label and location values are matched exactly as written.
Each source declares type, required, blocked, and optionally
write_blocked. There is no on_absent and no inheritance flag.
write_blocked is the read-only-resource setting: those values deny only on
calls carrying one of the connector's write scopes, so reads still succeed.
The connector declares which scopes those are, and the PDP folds
write_blocked into blocked when the call is a write — the gateway only
ever sees one pooled denylist.
Write-side enforcement needs a pre-dispatch guard, so it applies to connectors in
resolvemode. Aninlineconnector (Drive) filters the response body, which cannot stop a write; its sources gate reads only.
The predicate
All sources governing a resource type are pooled into one decision — not a per-source check. A label ID, a container ID and a resource's own ID can never collide, so their value spaces pool safely. The gateway builds:
values= the file's own labels ∪ its container IDs ∪ its own IDblocked= every source'sblocked(restricted labels ∪ blocked folders/drives)required= every source'srequired(approved labels ∪ approved folders/drives)
and runs three rules:
- If any value is in
blocked→ deny (blocked_match). Deny always wins, even over an otherwise-satisfiedrequired. - Else if
requiredis non-empty and none of the values are in it → deny (not_in_required). - Otherwise → allow.
This is OR across approvals, deny-wins over the top: a file is returnable
if it carries an approved label or sits in an approved folder; a
restricted label or a blocked folder denies it regardless of any approval.
There is no on_absent — a file matching nothing approved is denied by rule 2
whenever any approval is configured, and in denylist-only mode (no required
anywhere) any non-blocked file is allowed.
Approving whole folders
To make the contents of a folder returnable regardless of their own labels,
add a location source with the folder's ID in required. A location
source matches a file by its own ID as well as its parent container(s), so
putting a folder ID in required returns:
- the folder itself (its own ID matches), and
- every file/sub-folder directly inside it (their
parentsinclude the folder ID).
Matching is one level only — own-ID + direct parent, no tree walking. A file in a sub-folder of an approved folder is not covered; approve the sub-folder too, or use a shared drive for a whole subtree (below).
Google Drive does not allow labels on folders — so folder governance is
always by ID via a location source, never by labelling the folder. (This is
also why there is no label inheritance: there is no folder label to inherit.)
A blocked folder works the same way in reverse: put the folder ID in a
location source's blocked and the folder itself and everything directly
inside it is denied by rule 1, even if a file carries an approved label.
Approving a whole shared drive (subtree boundary)
A location source also matches a file's shared-drive ID (driveId),
which every file in a shared drive reports regardless of nesting depth. So
putting a shared-drive ID in required covers the entire drive transitively —
the escape hatch for the one-level folder rule. Put a dedicated "AI working
set" in its own shared drive and approve the drive once; block a shared-drive
ID to deny the whole drive (deny-wins). This works on both enforcement points
(the connector and the resolvers are shared-drive-aware).
Combining sources: pooled OR, and the audit reason
governed[resource_type] is a list of source IDs, not a single source.
Every listed source's required and blocked entries are pooled into the one
predicate above, so adding a location source alongside a label source
widens what's reachable — either approval works — while every source's
blocked entries remain hard denies. A withheld file's audit reason is the
deciding rule (blocked_match or not_in_required) over the pooled sets, so
"why did my file vanish?" traces back to the restricted label / blocked folder
that denied it (deny-wins always beats any approval the file also had).
There is no per-source AND: an operator cannot require a file to be both labelled and in an approved folder. OR is the intuitive default for access approval; if a future need for AND appears it is a follow-up, not built now.
Enforcement points
The same control is enforced at two points, chosen automatically per connector based on how that connector's response is shaped:
| Enforcement point | Fires on | Obligation | Behavior |
|---|---|---|---|
| Inline post-dispatch filter | Drive list_files / get_file — the response body already carries file metadata | filter_file_access | List responses are pruned: disallowed items are silently dropped. A get_file on a single disallowed item is denied outright (403). |
| Pre-dispatch resolve guard | A direct fetch by ID whose response isn't a file-metadata shape — Docs get_document, Sheets get_spreadsheet, Slides get_presentation, Confluence get_page | require_file_check | Facts are resolved out-of-band (separate Drive API calls using the caller's own passthrough token) before the call dispatches. A disallowed fetch never reaches the upstream — it's denied with 403 up front. |
Both points share the same operator config and the same pooled predicate; only
how the values get resolved differs (from the response body inline, versus a
per-source lookup on the resolve path).
Fail-closed rules
The control fails closed — denies rather than silently passing through — whenever it cannot establish a trustworthy answer:
- The caller's passthrough token is missing the Drive scope needed to resolve labels or containers (see Required OAuth scopes).
- The resolver call to Google errors out or times out.
- The file/folder ID being resolved is unknown or invalid.
- A governed source's
typeisn't known to the connector's projection (misconfiguration) — the whole request is denied rather than under-enforced.
Operator config schema
{
"sources": {
"<source_id>": {
"type": "label" | "location",
"required": ["<id>", "..."], // allowlist; empty = no allow-constraint
"blocked": ["<id>", "..."] // denylist; always wins
}
},
"governed": {
"<resource_type>": ["<source_id>", "..."] // pooled OR across the list
}
}
| Field | Applies to | Meaning |
|---|---|---|
type | every source | label (label IDs) or location (container IDs) |
required | every source | Allowlist — any-of. Pooled across sources. Empty everywhere means no allow-constraint (pure denylist mode). |
blocked | every source | Denylist — any-of. Pooled across sources. Always wins over required. |
Worked example
A Drive-family connector governed by a Security-Policy shape — approved folders or approved labels; restricted labels and blocked folders deny:
{
"sources": {
"drive-labels": {
"type": "label",
"required": ["sKFCys64ExampleApprovedId"],
"blocked": ["kJ2mQpExampleSecretId"]
},
"drive-location": {
"type": "location",
"required": ["1AbcApprovedFolderId"],
"blocked": ["1XyzHRFolderId"]
}
},
"governed": {
"urn:connector:identos:google-drive": ["drive-labels", "drive-location"],
"urn:connector:identos:google-docs": ["drive-labels", "drive-location"]
}
}
Read this as: a file is returnable if it carries the approved label or sits directly in the approved folder — and is withheld if it carries the restricted label or lives directly in the HR folder, no matter what else is true about it (deny-wins). A file with neither an approved label nor an approved-folder parent matches no approval and is denied by rule 2.
Write it with:
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": {
"drive-labels": {
"type": "label",
"required": ["sKFCys64ExampleApprovedId"],
"blocked": ["kJ2mQpExampleSecretId"]
},
"drive-location": {
"type": "location",
"required": ["1AbcApprovedFolderId"],
"blocked": ["1XyzHRFolderId"]
}
},
"governed": {
"urn:connector:identos:google-drive": ["drive-labels", "drive-location"],
"urn:connector:identos:google-docs": ["drive-labels", "drive-location"]
}
}
}'
Omitting a resource type from governed leaves it ungoverned — inert, no
check at all — even if sources defines entries that could apply to it.
For getting real label IDs, folder IDs, and shared-drive IDs, and for the required OAuth scopes, see the Google file access setup guide.
Default label on create
An agent's own output is a problem under an allowlist: a file it creates is
born unlabelled, so it's denied on the very next read. Give a label source a
default and the control stamps that label on newly-created files — so
agent output is readable back and carries a default classification from birth.
"sources": {
"org-labels": {
"type": "label",
"required": ["AI-Available-id", "Team-X-id"],
"blocked": ["Confidential-id"],
"default": { "label_id": "AI-Available-id" } // stamped on create
}
}
The default should be one of that source's required labels, so the
created file is also returnable (a warning if not). Mechanics: the control emits
an apply_label obligation on create — recognized via the connector
projection's create descriptor (write scope + where the new id is) — and a
label applier (the write-side twin of a resolver) performs the write.
Google Drive Labels (google-drive-modify-labels) covers Drive/Docs/Sheets/
Slides. Best-effort: if the write fails the file stays unlabelled (invisible,
the safe direction) and it's logged. See the design spec for the general model.
Non-goals
- No label inheritance / recursive tree walking. A source resolves only
the file's own values. To make a folder's direct contents returnable, put
the folder ID in a
locationsource'srequired— Google Drive forbids folder labels, so there is nothing to inherit. A whole-subtree boundary needs a dedicated shared drive matched by itslocationcontainer ID, not folder nesting. - No per-source AND. Sources pool into one OR-of-approvals decision.
- No re-labelling of existing files / reclassification on update — only the default-label-on-create below writes labels, and only for newly-created files.
Related docs
- Obligations — the general obligation mechanism this
control's
filter_file_accessandrequire_file_checkobligations are built on. - Google file access setup — creating Drive Labels, getting bare label/folder/drive IDs, granting the required OAuth scopes, and a full end-to-end worked example.