Skip to main content
Version: Latest

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 how required/blocked are 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 works

    Google'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}. An identity source 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 identity only. 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 resolve mode. An inline connector (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 ID
  • blocked = every source's blocked (restricted labels ∪ blocked folders/drives)
  • required = every source's required (approved labels ∪ approved folders/drives)

and runs three rules:

  1. If any value is in blockeddeny (blocked_match). Deny always wins, even over an otherwise-satisfied required.
  2. Else if required is non-empty and none of the values are in it → deny (not_in_required).
  3. 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 parents include 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 pointFires onObligationBehavior
Inline post-dispatch filterDrive list_files / get_file — the response body already carries file metadatafilter_file_accessList responses are pruned: disallowed items are silently dropped. A get_file on a single disallowed item is denied outright (403).
Pre-dispatch resolve guardA direct fetch by ID whose response isn't a file-metadata shape — Docs get_document, Sheets get_spreadsheet, Slides get_presentation, Confluence get_pagerequire_file_checkFacts 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 type isn'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
}
}
FieldApplies toMeaning
typeevery sourcelabel (label IDs) or location (container IDs)
requiredevery sourceAllowlist — any-of. Pooled across sources. Empty everywhere means no allow-constraint (pure denylist mode).
blockedevery sourceDenylist — 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 location source's required — Google Drive forbids folder labels, so there is nothing to inherit. A whole-subtree boundary needs a dedicated shared drive matched by its location container 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.
  • Obligations — the general obligation mechanism this control's filter_file_access and require_file_check obligations 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.