Fence Glean Search by Datasource
Glean's search tool fans out across every system the tenant has indexed (Drive, Confluence, Slack, Jira, Gmail/Outlook, GitHub, Salesforce, Gong, HR…
- Direction
- ingress
- Rego package
glean.ingress.fence_datasource_scope- App
- glean
- Bundles
- soc2hipaagdpr-ccpa
- Published
- Minimum gateway
- 1.0.0b24
- Schema version
- 1.0.0
- Checksum
sha256:56e26218fcc730dcdd015c7e265a776ac7e01c2402521fae5498858ae798f324
gleanfence-sensitive-scopesaccess-controldatasourceingresssoc2hipaagdpr-ccpa
What this policy does
Direction: ingress (tool_pre_invoke)
Default: deny; explicit allows for non-fenced tools and cleared calls
Package: glean.ingress.fence_datasource_scope
What it does
Glean's search tool fans out across every system the tenant has indexed
(Drive, Confluence, Slack, Jira, Gmail/Outlook, GitHub, Salesforce, Gong,
HR systems…). This policy restricts which indexed datasource a search
call may target by inspecting the call's app argument — the datasource
enum (gong, salescloud, confluence, gdrive, slack, jira,
github, gmail, o365sharepoint, …).
Calls targeting a restricted datasource are denied unless the caller's IdP groups include the group cleared for that source. The shipped (placeholder) mapping is:
Datasource (app value) |
What it holds | Required IdP group |
|---|---|---|
gong |
Call recordings / conversation intelligence | sales |
salescloud |
Salesforce CRM | revops |
workday, bamboohr (HR datasources) |
HR / people records | hr |
When the app argument is absent, the search fans out across every
indexed system at once, so an unscoped query is treated like a query that
can reach all restricted sources: it is allowed only for a caller cleared
for every restricted datasource, and otherwise denied with a reason
directing the caller to pass an explicit app.
Every other tool — chat, read_document, code_search,
employee_search, gmail_search, meeting_lookup, memory, and all
non-Glean tools — passes through this policy untouched. Because chat
accepts only free text with no filterable datasource field, this ingress
fence covers search only and must be paired with the egress redaction
policy to cover chat (see Composition).
Identity gating
Clearance is granted per datasource via IdP group membership read from
input.subject.claims.groups through object.get(...) chains, so a missing
subject, missing claims, or a missing/malformed groups claim fails
closed: no matching group means no access to the restricted datasource.
The groups claim must be an array of strings; any other shape yields no
memberships. Group names are compared case-insensitively.
Compliance alignment
This policy instantiates sensitive-scope fencing (family PF-23) on Glean's cross-source search path and supports alignment with:
- SOC 2 C1.1, P4.1 — identifies and protects confidential information and limits personal-information use by fencing designated datasources (Gong call data, Salesforce CRM, HR systems) out of agent search unless the caller's role grants it.
- HIPAA §164.502(b)/§164.514(d), §164.308(a)(4) — minimum-necessary and information-access-management: an agent cannot trawl HR datasources over MCP unless the caller's role clears it; §164.522(a) — supports agreed-to restrictions expressed as datasource-level fences.
- PCI DSS 7.2.6 — supports restricting programmatic (agent) query access to stored data that may include account data (e.g. the Salesforce CRM datasource) by IdP role.
- GDPR Art. 9; CPRA §1798.121 — keeps special-category / sensitive personal information held in HR and CRM datasources out of agent result sets; Art. 5(1)(b) — supports purpose limitation by keying datasource access to the caller's team.
Why ingress
The target datasource is fully determined by the request alone (tool name,
the app argument, caller claims), so enforcement happens before the call
reaches Glean and restricted content is never fetched into the model
context. For defense in depth, pair with an egress redaction policy as a
backstop for content reached by paths this policy does not cover (notably
chat).
Tool name matching
The gateway prefixes tool names with the configured MCP server name (e.g.
glean-search), and the prefix is not standardized, so the policy matches
case-insensitively. Glean's remote server names its search tool the bare
word search, which collides with other servers' search tools and with
Glean's own code_search / employee_search / gmail_search /
outlook_search. To avoid mis-matching those, the policy matches only:
- the exact tool name
search, or - any name ending in
-search(e.g.glean-search,glean-mcp-search).
This deliberately excludes the _search sibling tools (they take no app
argument and are governed by companion policies). It also means the policy
should be attached to the Glean gateway/pipeline only — on a Glean-only
pipeline the sole -search/search tool is Glean's. Verify the exact name
your gateway emits with the dump-input debug technique before relying on
this in production. The deprecated local server exposed search as
company_search (a _search name, so not matched); add it only if a
tenant still runs the archived package.
Argument shape
The datasource is read from input.payload.args.app (verified from Glean's
search parameter list). The value is normalized with lower/trim_space
and compared against the restricted set. Both shapes are handled
defensively:
- a single string (
"app": "gong"), and - an array of strings (
"app": ["gdrive", "gong"]) — a call is denied if any entry names a restricted datasource the caller is not cleared for.
An empty, missing, or non-string app (and an array of only empty strings)
is treated as an unscoped query and fails closed as described above.
Configuration
Edit the restricted_sources object at the top of the Rego. The datasource
keys (gong, salescloud, workday, bamboohr) map to the required IdP
group. gong and salescloud are Glean's documented enum values; the HR
entries (workday, bamboohr) are placeholders — replace them with the
exact app enum values your tenant's HR systems are indexed under, and
remap the groups (sales, revops, hr) to your IdP's group names at
import time.
Examples
Allowed (search scoped to a non-restricted datasource)
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "glean-search", "type": "tool" },
"payload": {
"name": "glean-search",
"args": { "query": "deploy runbook", "app": "confluence" }
}
}
}
allow = true, no reason.
Allowed (restricted datasource, caller cleared)
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "glean-search", "type": "tool" },
"subject": { "sub": "auth0|rep", "claims": { "groups": ["sales"] } },
"payload": {
"name": "glean-search",
"args": { "query": "acme renewal", "app": "gong" }
}
}
}
allow = true, no reason.
Denied (restricted datasource, caller not cleared)
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "glean-search", "type": "tool" },
"subject": { "sub": "auth0|eng", "claims": { "groups": ["engineering"] } },
"payload": {
"name": "glean-search",
"args": { "query": "pipeline", "app": "salescloud" }
}
}
}
allow = false,
reason = "This Glean search targets restricted datasource(s) (salescloud) …".
Denied (unscoped search, caller not cleared for all restricted sources)
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "glean-search", "type": "tool" },
"payload": {
"name": "glean-search",
"args": { "query": "compensation" }
}
}
}
allow = false, reason = "This Glean search has no app datasource filter, …".
Composition
This policy covers the datasource-scoped search surface. Useful companions:
- An egress redaction policy on
search/chat/read_documentresponses so PII/CHD that is reached by paths this fence does not cover (notably free-textchat) is masked before it reaches the agent context. - A
meeting_lookuptranscript-extraction fence and a mailbox-search restriction for the other sensitive Glean read tools. - A default-deny-unknown-tools policy (PF-28) for Glean's admin-mutable tool inventory (agents-as-tools, gateway-proxied writes).
Known limitations
chatis not fenceable here.chattakes only free text with no datasource argument, so it can reach any indexed system regardless of this policy. It is intentionally passed through and must be covered by the egress redaction backstop.- Sibling read tools are out of scope by design.
code_search,employee_search,gmail_search,outlook_search,meeting_lookup,read_document, anduser_activityreach sensitive data but do not take theappdatasource argument, so this policy does not fence them (the tool-name match excludes_searchnames). Govern them with the companion policies listed above. dynamic_search_result_filtersis a bypass residual. Glean'ssearchalso accepts a structured result-filter argument that can re-scope results by datasource. A caller could in principle set a benignapp(or none) and steer results toward a restricted source through that filter. Its per-tenant schema is not documented, so this policy does not parse it; rely on the egress redaction backstop and cap/strip that argument with a separate transform if your tenant exposes it.- Denylist, not allowlist — an unrecognized
appvalue is not fenced. A scoped call is denied only whenappnames a restricted source the caller lacks; any other non-empty string (a real but non-restricted datasource, or a value the enum does not define) is treated as a benign scoped query and allowed. Glean's behavior on an unrecognizedappvalue is unverified: if Glean validates the enum and errors, there is no exposure; but if it silently ignores the value and fans out across all sources, a non-privileged caller could pass a junkapp(e.g.app: "everything") to reach restricted datasources while still settingapp_present, sidestepping the unscoped-search guard. The same gap covers obfuscated look-alikes: a homoglyph or otherwise-encoded value (e.g.gongspelled with a Greek omicron) is a distinct, unrecognized string thatlower/trim_spacedo not fold to the restricted key, so it is treated as a benign scoped query — though Glean's own enum will not resolve it either, so this yields no exposure beyond the fan-out case above. Do not rely on this fence alone against that case: keep the egress redaction backstop, and if you can enumerate your tenant's datasource enum, convertrestricted_sourceshandling to an allowlist (treat anyappoutside the known set as unscoped/fail-closed) at import time. - Bulk-export flags are not capped here.
exhaustiveandnum_results(up to 500) enable bulk pulls; this policy fences which datasource, not how much. Pair with a transform policy that capsnum_resultsand stripsexhaustiveif bulk export is a concern. - HR datasource names are placeholders. Only the configured
appenum values (workday,bamboohrby default) are treated as HR; a restricted HR system indexed under a differentappvalue is not caught until you add it torestricted_sources. - Tool-name portability. Bare
searchand any-searchsuffix match, so attach this to the Glean pipeline only — a non-Glean server whose tool is named…-searchwould otherwise be fenced too. Confirm the exact gateway tool name with the dump-input technique. The match also depends on the gateway joining the server prefix to the tool with a hyphen (glean-search, the DTwo convention). If a deployment instead joins with an underscore, the search tool is emitted asglean_search, which ends in_searchand is deliberately excluded (that suffix is how the siblingcode_search/gmail_search/employee_searchtools are skipped) — so the fence would silently pass the search tool through. Broadening the match to_searchis not an option (it would blanket-deny every no-appsibling search); verify your gateway emits a hyphen-joined name before relying on this fence. - Identity placeholders. Group names are placeholders — replace
sales,revops, andhrwith your IdP's group names at import time. Thegroupsclaim must be an array of strings; any other shape fails closed.
Compliance note. This policy supports alignment with the cited framework controls on the MCP path only. No policy or bundle makes an organization compliant with any framework; web-UI, native-API, and in-app access are outside the gateway's reach by design. Validate against your own compliance program before relying on it.
Policy source (Rego)
package glean.ingress.fence_datasource_scope
# Deny-by-default: only the explicit allow rules below permit the request.
default allow := false
# -----------------------------------------------------------------------------
# CONFIG: restricted Glean datasource (`app` enum value) -> IdP group cleared
# to search it. `gong` and `salescloud` are Glean's documented enum values;
# the HR entries are PLACEHOLDERS — replace them with the exact `app` values
# your HR systems are indexed under, and remap the groups (sales, revops, hr)
# to your IdP's group names at import time. Keys must be lowercase.
# -----------------------------------------------------------------------------
restricted_sources := {
"gong": "sales",
"salescloud": "revops",
"workday": "hr",
"bamboohr": "hr",
}
# -----------------------------------------------------------------------------
# Tool matching. The gateway prefixes tool names with the configured MCP
# server name. Glean's search tool is the bare word `search`, which collides
# with other `search` tools and with Glean's own `code_search` /
# `employee_search` / `gmail_search` (all `_search`). We match ONLY the exact
# name `search` or a `-search` suffix so those `_search` siblings are excluded.
# Attach to the Glean pipeline only. Verify with the dump-input technique.
# -----------------------------------------------------------------------------
tool_name := lower(input.resource.name)
is_glean_search_tool if tool_name == "search"
is_glean_search_tool if endswith(tool_name, "-search")
# -----------------------------------------------------------------------------
# Identity — caller's IdP groups, read fail-closed: a missing subject, missing
# claims, or a missing/malformed groups claim yields no memberships, so the
# caller is never treated as cleared by accident.
# -----------------------------------------------------------------------------
caller_groups := object.get(
object.get(object.get(input, "subject", {}), "claims", {}),
"groups",
[],
)
member_of(group) if {
is_array(caller_groups)
some g in caller_groups
is_string(g)
lower(g) == group
}
# Required to run an UNSCOPED search (no `app`): it fans out across every
# indexed system, so the caller must be cleared for ALL restricted sources.
caller_in_all_restricted_groups if {
every _, group in restricted_sources {
member_of(group)
}
}
# -----------------------------------------------------------------------------
# Arguments — the datasource(s) the search targets. Handles both a single
# string (`"app": "gong"`) and an array (`"app": ["gdrive", "gong"]`). Empty
# or non-string values contribute nothing, so a missing/blank `app` leaves
# requested_apps empty and is treated as an unscoped query (fail-closed).
# -----------------------------------------------------------------------------
req_args := object.get(object.get(input, "payload", {}), "args", {})
app_raw := object.get(req_args, "app", "")
requested_apps contains a if {
is_string(app_raw)
a := lower(trim_space(app_raw))
a != ""
}
requested_apps contains a if {
is_array(app_raw)
some x in app_raw
is_string(x)
a := lower(trim_space(x))
a != ""
}
app_present if count(requested_apps) > 0
# Restricted datasources the call targets that the caller is NOT cleared for.
denied_apps contains a if {
some a in requested_apps
group := object.get(restricted_sources, a, "")
group != ""
not member_of(group)
}
# -----------------------------------------------------------------------------
# Allow rules
# -----------------------------------------------------------------------------
# Any tool this policy does not fence passes through (chat, read_document,
# the *_search siblings, memory, all non-Glean tools).
allow if {
not is_glean_search_tool
}
# Search scoped to an explicit datasource: allowed unless it names a restricted
# source the caller is not cleared for.
allow if {
is_glean_search_tool
app_present
count(denied_apps) == 0
}
# Unscoped search (no `app`): fans out across every source, so only a caller
# cleared for ALL restricted datasources may run one.
allow if {
is_glean_search_tool
not app_present
caller_in_all_restricted_groups
}
# -----------------------------------------------------------------------------
# Reasons
# -----------------------------------------------------------------------------
reasons contains msg if {
is_glean_search_tool
app_present
count(denied_apps) > 0
src_list := concat(", ", sort([a | some a in denied_apps]))
msg := sprintf("This Glean search targets restricted datasource(s) (%s) your account is not cleared for. Search a datasource you have access to, or contact your InfoSec team if your role requires that source.", [src_list])
}
reasons contains "This Glean search has no `app` datasource filter, so it fans out across every indexed system, including restricted ones (Gong call recordings, Salesforce CRM, HR). Pass an explicit `app` naming the datasource you need, or contact your InfoSec team if you need broader search access." if {
is_glean_search_tool
not app_present
not caller_in_all_restricted_groups
}
reason := joined if {
count(reasons) > 0
reason_list := sort([r | some r in reasons])
joined := concat("; ", reason_list)
} Canonical source: policy.md on GitHub · raw · raw on this site (.md)
Used in these guides
Related policies
Airtable: Redact PII in Record Reads
Scans the responses of the Airtable record-read tools — the calls that return row fields values — and rewrites high-confidence PII shapes to a fixed…
Asana: Redact PII in Task & Comment Reads
On the Asana MCP read path, this transform scans the free-text business fields that ride back in task, comment/story, and status-update responses — notes,…
BigQuery: Redact PII in Query Results
Scans the content returned by BigQuery's result-returning tools and rewrites high-confidence PII shapes to fixed, non-recoverable redaction tokens before the…
Block Agent Email to External Recipients
Blocks agent-initiated Microsoft 365 email sends when any recipient address falls outside a corporate-domain allowlist.
Block BigQuery Exfiltration and Cross-Project Writes
Inspects the raw GoogleSQL string carried by BigQuery SQL tools and denies any statement that moves data out of the tenant's own project — even when the call…
bigqueryguard-warehouse-exportingresssqlexfiltrationsoc2pci-dssgdpr-ccpa
Block Bulk Export & External Staging (Snowflake)
Blocks Snowflake SQL-execution tool calls whose query text moves whole tables off the Snowflake perimeter — bulk export to cloud storage or a stage, and…
snowflakeguard-warehouse-sqlexportexfiltrationingresssoc2pci-dssgdpr-ccpa