Fence Regulated BigQuery Datasets by Group
Fences customer-designated regulated BigQuery data domains by data-domain IdP group, at ingress, before any statement or metadata lookup reaches BigQuery.
- Direction
- ingress
- Rego package
bigquery.ingress.fence_sensitive_datasets- App
- bigquery
- Bundles
- soc2hipaapci-dssgdpr-ccpa
- Published
- Minimum gateway
- 1.0.0b24
- Schema version
- 1.0.0
- Checksum
sha256:5b131ef35203846caed781b9046d6b3969d0eae2f827cc7d4a8377cb16ba6102
bigqueryfence-sensitive-scopesingressrbacsoc2hipaapci-dssgdpr-ccpa
What this policy does
Direction: ingress (tool_pre_invoke)
Default: deny a regulated-dataset reference for callers outside the mapped data-domain group, allow otherwise
Package: bigquery.ingress.fence_sensitive_datasets
What it does
Fences customer-designated regulated BigQuery data domains by data-domain IdP
group, at ingress, before any statement or metadata lookup reaches BigQuery. The
policy carries one placeholder map, sensitive_domains, pairing a dataset name
prefix with the IdP group required to touch that domain:
phi_→clinical-datafinance_→financepii_→data-privacy
It inspects two surfaces so that recon and enumeration are fenced before any
SELECT runs — not just the query itself:
- Raw SQL on the query tools (
execute_sql,execute_sql_readonly,query,execute-query). Every string argument value on these tools is scanned for a table reference whose dataset component begins with a fenced prefix (e.g.phi_labresults.records,myproj.finance_ledger.gl,`pii_customers.profiles`). Scanning all string args — not just the documentedsql/querykeys — means a community server that names its SQL argument something else cannot slip a query past the fence fail-open. dataset_id/table_idarguments on the metadata and read tools (get_dataset_info,get_table_info,list_table_ids,list-tables,describe-table). This closes the enumeration path: a caller cannot map or describe a regulated dataset's schema before deciding what toSELECT.
If a fenced prefix is referenced on either surface and the caller's IdP groups
claim does not include the mapped group, the call is denied. The deny reason
names the data domain and the exact group required, plus an escalation hint.
Group membership is read through object.get(input.subject, "claims", {}) chains
that fail closed: a missing, empty, or malformed subject/claims/groups
never grants a fenced domain — no group means not exempt. A caller holding the
wrong domain group (e.g. finance querying a phi_ dataset) is likewise denied,
because each domain requires its own specific group. Calls that reference no fenced
prefix, and calls to tools outside the two inspected surfaces, pass through
untouched.
Compliance alignment
- HIPAA §164.502(b)/§164.514(d) — supports the minimum-necessary and
role-based-limits standard: agent access to a PHI-bearing dataset (
phi_) is gated to its mappedclinical-datagroup on the MCP path, including the metadata/enumeration tools so a caller cannot even map PHI schema without the entitlement; §164.308(a)(4) — supports information access management by authorizing sensitive-domain access via IdP group. - PCI DSS 7.2.6 — supports restricting programmatic query access to stored
cardholder data by role: fence the
finance_prefix so only thefinancegroup can query or enumerate it through an agent. - SOC 2 C1.1 — supports identification and protection of confidential information by gating agent SQL and metadata calls against designated confidential datasets; P4.1 — supports limiting personal-information use to identified purposes by keeping PI-bearing datasets behind a role fence.
- GDPR Art. 9 — supports special-category protection by fencing datasets
holding health (
phi_) or other Art. 9 data; CPRA §1798.121 — supports the right to limit use of sensitive personal information by fencing thepii_domain to a minimal group. - SOX (ITGC — access to programs & data) — supports least-privilege access to
financial data: fencing the
finance_domain to thefinancegroup confines agent reads of financially relevant warehouse data to authorized personnel on the MCP path.
Why ingress and least-privilege
This is the minimum-necessary / least-privilege control for the warehouse's data
plane: it stops a regulated-dataset reference — read, query, or enumeration —
before it executes, rather than masking a response the query already produced.
Fencing the metadata tools matters because schema recon (get_dataset_info,
list_table_ids) is itself a disclosure and a targeting step; blocking it early
denies the agent the map it would use to craft an exfiltration SELECT. Pair with
an egress redaction backstop (see Composition) for defense in depth, since
which specific tables hold regulated data is not knowable from the wire.
Tool name matching
Tools are matched by suffix (the gateway prefixes tool names with the configured MCP server name, which is not standardized). Two classes are inspected:
- SQL query tools —
execute_sqlandexecute_sql_readonly(Google official remote server + MCP Toolboxbigquerytoolset, snake_case),query(ergut/mcp-bigquery-server),execute-query(LucasHild/mcp-server-bigquery, kebab-case).execute_sql_readonlyis matched explicitly because a read-only SELECT still bulk-reads a regulated dataset. Every string argument value on these tools is scanned (not justsql/query). - Metadata / read tools —
get_dataset_info,get_table_info,list_table_ids,list-tables,describe-table. Theirdataset_id/table_idarguments are scanned.
Any other tool (including list_dataset_ids, which lists dataset names with no
target argument) is not inspected and passes through — see Known
limitations. Verify the exact tool names your gateway emits with the
dump-input debug technique before relying on this in production.
Argument shape
- SQL query tools —
sql/queryare the documented keys carrying the GoogleSQL text, but all string argument values are read and scanned, so a server that carries the SQL under a different (unverified) key is still fenced rather than passing fail-open. dataset_id/table_id— the dataset/table identifiers (strings) on the metadata tools. A fully-qualifiedtable_id(finance_gl.journal) is matched as readily as a bare dataset id (pii_customers). The community kebab-case tools (list-tables,describe-table) whose argument key is unverified are also read under the aliasesdataset,table, andtable_name, so a metadata tool this policy claims to inspect is fenced regardless of which of these keys it uses.
Only string argument values are inspected; a non-string value contributes no text.
Examples
Allowed — cleared caller queries a fenced dataset
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "bigquery-mcp-execute_sql", "type": "tool" },
"subject": { "sub": "google-apps|nurse@example.com", "claims": { "groups": ["clinical-data"] } },
"payload": {
"name": "bigquery-mcp-execute_sql",
"args": { "sql": "SELECT patient_id, visit_date FROM phi_records.encounters WHERE patient_id = 42" }
}
}
}
allow = true, no reason.
Denied — uncleared caller references a fenced dataset in SQL
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "bigquery-mcp-execute_sql", "type": "tool" },
"subject": { "sub": "google-apps|dev@example.com", "claims": { "groups": ["engineering"] } },
"payload": {
"name": "bigquery-mcp-execute_sql",
"args": { "sql": "SELECT customer_id, email FROM pii_customers.profiles WHERE id = 5" }
}
}
}
allow = false, reason names the personal-data (PII) domain and the required
data-privacy group.
Denied — schema recon on a fenced dataset via a metadata tool
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "bigquery-mcp-get_dataset_info", "type": "tool" },
"subject": { "sub": "google-apps|analyst@example.com", "claims": { "groups": ["finance"] } },
"payload": {
"name": "bigquery-mcp-get_dataset_info",
"args": { "dataset_id": "phi_labresults" }
}
}
}
allow = false — the caller holds finance, not the clinical-data group the
phi_ domain requires, so even the metadata lookup is fenced.
Composition
This policy is single-purpose. Curated companions for the BigQuery data plane:
guard-warehouse-sql(ingress) — deny DML/DDL/GRANTso a fenced-out caller cannot pivot to a destructive write; keeps ordinary agent callers read-only.guard-warehouse-export(ingress) — denyEXPORT DATA/EXTERNAL_QUERYexfiltration constructs that move regulated data out without looking like a write.redact-pii-egress(egress) — redact SSN/PAN/email patterns from returned rows: the defense-in-depth backstop for regulated tables not yet pinned here, or referenced by a dynamic construct this ingress fence cannot see.
Known limitations
- Placeholder configuration. The prefixes (
phi_,finance_,pii_) and group names (clinical-data,finance,data-privacy) are placeholders — replace them with your tenant's real dataset naming scheme and your IdP's group-claim values at import time. Which datasets hold regulated data is not knowable from the wire, so pinning the naming convention is mandatory for this policy to do anything. - Dynamic and indirect references evade the prefix regex. Detection matches the
literal fenced prefix at an identifier boundary in the wire text. A dataset
reference built dynamically (
EXECUTE IMMEDIATE, string concatenation, a parameter/bind) or reached indirectly through a view defined in another dataset — where the un-prefixed view name is what appears in the SQL — is not fenced. This is a fundamental limit of wire-level inspection; pair withguard-warehouse-sql(to scope dynamic SQL) and keep the egress redaction backstop in place. Treat the ingress fence as one layer, not the sole control. - Dataset-listing tool is not inspected.
list_dataset_idslists dataset names with no target argument, so it passes through — an uncleared caller can still learn that aphi_-prefixed dataset exists (but not its schema or rows).execute_sql_readonlyis now fenced (added tosql_tool_suffixes), so a read-onlySELECTfrom a fenced dataset is denied for an uncleared caller just likeexecute_sql. INFORMATION_SCHEMA/ region-level enumeration is not fenced. A query such asSELECT schema_name FROM `region-us`.INFORMATION_SCHEMA.SCHEMATAenumerates every dataset name in a region without ever writing a fenced prefix as a literal table reference, so the boundary-anchored detection does not fire and the call passes. This is metadata recon that the wire-level prefix match cannot see; pair with a block-schema-recon companion policy that deniesINFORMATION_SCHEMA/__TABLES__references for callers outside analytics/engineering groups, and keep the egress backstop in place.- AI-analytics and catalog-search tools are not inspected. The MCP Toolbox
tools
ask_data_insights(which ships table contents to Google's Conversational Analytics API),forecast,analyze_contribution, andsearch_catalogare not among the inspected suffixes. The first three take a table reference plus a natural-language question (not asql/dataset_idargument), so an uncleared caller can reference a fenced table through one of them and move its data without tripping this fence; their table-reference field name is unverified across Toolbox revisions.search_catalogis a Dataplex catalog search: it enumerates and describes datasets/tables by keyword, so an uncleared caller can use it to discover a fencedphi_/finance_/pii_dataset's existence and metadata (a recon/enumeration path parallel tolist_dataset_idsandINFORMATION_SCHEMAabove) even though it never carries a literal fenced table reference in a scanned argument. Gate the AI tools with a companion gate-ai-analytics policy and the search tool with a block-schema-recon companion (deny by group), and keep the egress redaction backstop in place; do not rely on this fence to cover the AI-analytics or catalog-search paths. - Non-string / nested argument shapes are not inspected. Detection reads only
top-level string argument values (SQL tools scan every string arg; metadata
tools scan the string-valued alias keys). A tool that carries its SQL or target
identifier inside a nested object or an array (e.g.
args.query.sql, or astatements: [...]batch) contributes no scanned text, so the call is treated as carrying no readable target and passes through fail-open. No verified BigQuery SQL/metadata server uses such a shape (all passsql/dataset_idas top-level strings — see the landscape note), so this is a residual for an unverified/future community server rather than a live bypass; if you adopt a server with a nested argument shape, extendinspected_textto walk it, and keep the egress backstop in place. - Convention-dependent, boundary-anchored. Detection keys on a prefix at an
identifier boundary (
\bphi_), so a regulated dataset that does not carry the pinned prefix is not fenced, and — conversely — a benign column, alias, or literal that literally begins with a fenced prefix (e.g. a column namedphi_flag) produces a conservative (fail-safe) denial for an un-cleared caller. Enforce the dataset naming convention in BigQuery and rely on the egress backstop for the residual. groupsclaim must be an array of strings. A string-valued or otherwise malformed claim fails closed (fenced domains deny). On Auth0 tenants without RBAC /permissions configured, nogroupsclaim reaches the policy and every fenced domain denies until the claim is wired up. If your IdP emits groups under a different claim name (e.g. a namespaced custom claim), updatecaller_groupsin the Rego.- Never uses stripped claims. Authorization is driven solely by the IdP
groupsclaim. The ContextForge-internal claimsis_admin,user, andteamsare stripped before reaching the policy and must never be used for these grants — a rule referencing them would silently never match.
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 bigquery.ingress.fence_sensitive_datasets
# Deny-by-default: only the explicit allow rules below permit the request.
default allow := false
# ---------------------------------------------------------------------------
# Fence configuration — PLACEHOLDERS, replace at import time.
#
# Maps a BigQuery dataset NAME PREFIX -> the IdP group required to touch that
# data domain. Prefixes are matched case-insensitively at an identifier
# boundary (\bPREFIX); groups are compared case-insensitively against
# `subject.claims.groups`. Replace with the tenant's real dataset naming scheme
# and IdP group names.
sensitive_domains := {
"phi_": "clinical-data", # e.g. phi_records.encounters, phi_labresults
"finance_": "finance", # e.g. finance_ledger.gl, finance_ar
"pii_": "data-privacy", # e.g. pii_customers.profiles, pii_events
}
# Human-readable domain label for the deny reason.
domain_labels := {
"phi_": "protected health information (PHI)",
"finance_": "financial",
"pii_": "personal data (PII)",
}
# ---------------------------------------------------------------------------
# Identity — read groups via object.get chains so a missing subject/claims/
# groups fails closed (no group -> no access to a fenced domain).
caller_groups := object.get(object.get(input, "subject", {}), "claims", {})
caller_group_list := object.get(caller_groups, "groups", [])
# True when the caller's groups claim (an array of strings) contains `group`.
# A malformed (non-array) claim makes the iteration fail -> fail closed.
caller_has_group(group) if {
some g in caller_group_list
lower(g) == lower(group)
}
# ---------------------------------------------------------------------------
# Tool classification — match by suffix (gateway prefixes the server name).
# Raw-SQL query tools. `execute_sql_readonly` is matched explicitly (its
# `_readonly` suffix is NOT caught by the `execute_sql` suffix) because a
# read-only SELECT still bulk-reads a regulated dataset, which is exactly what
# this fence governs. Every string argument on these tools is scanned, so a
# community server that names its SQL argument something other than `sql`/
# `query` cannot slip a query through fail-open.
sql_tool_suffixes := ["execute_sql_readonly", "execute_sql", "execute-query", "query"]
# Metadata / read tools whose `dataset_id`/`table_id` argument is scanned.
metadata_tool_suffixes := [
"get_dataset_info",
"get_table_info",
"list_table_ids",
"list-tables",
"describe-table",
]
tool_name := lower(object.get(input.resource, "name", ""))
is_sql_tool if {
some suffix in sql_tool_suffixes
endswith(tool_name, suffix)
}
is_metadata_tool if {
some suffix in metadata_tool_suffixes
endswith(tool_name, suffix)
}
# ---------------------------------------------------------------------------
# Argument extraction — object.get everywhere; only string values contribute.
args := object.get(object.get(input, "payload", {}), "args", {})
# SQL text (from a query tool). Scan EVERY string argument value, not only the
# documented `sql`/`query` keys, so a divergent/unverified argument key on a
# community server cannot let an un-fenced query pass fail-open. A stray non-SQL
# string arg that happens to start with a fenced prefix yields a conservative
# (fail-safe) denial — consistent with the boundary-anchored detection below.
inspected_text contains t if {
is_sql_tool
some _, v in args
is_string(v)
v != ""
t := v
}
# Metadata identifiers (from a metadata tool). Official servers name these
# `dataset_id`/`table_id`; the community kebab-case tools (LucasHild
# `list-tables`/`describe-table`) may name them `dataset`/`table`/`table_name`.
# Read the union so a metadata tool this policy CLAIMS to inspect cannot slip
# through fail-open just because it used a different (unverified) key name.
metadata_arg_keys := ["dataset_id", "table_id", "dataset", "table", "table_name"]
inspected_text contains t if {
is_metadata_tool
some key in metadata_arg_keys
t := object.get(args, key, "")
is_string(t)
t != ""
}
# This policy only inspects the SQL and metadata surfaces above; every other
# tool, and an inspected tool carrying no readable target text, passes through.
is_inspected_call if {
count(inspected_text) > 0
}
# ---------------------------------------------------------------------------
# Detection.
# True when `text` references an identifier beginning with `prefix`. `\b`
# anchors to an identifier boundary (start of string, whitespace, `.`, `(`,
# backtick, comma), so phi_records and myproj.finance_ledger match but a prefix
# buried mid-identifier (my_pii_col) does not.
domain_in_text(prefix, text) if {
regex.match(sprintf(`(?i)\b%s`, [prefix]), text)
}
# A fenced domain the caller is NOT cleared for is referenced in the request.
denied_domains contains prefix if {
some prefix, group in sensitive_domains
some t in inspected_text
domain_in_text(prefix, t)
not caller_has_group(group)
}
# ---------------------------------------------------------------------------
# Allow rules.
# Any call this policy does not inspect passes through untouched.
allow if {
not is_inspected_call
}
# Inspected call that references no fenced domain the caller lacks clearance for.
allow if {
is_inspected_call
count(denied_domains) == 0
}
# ---------------------------------------------------------------------------
# Deny reasons — name the data domain, the dataset prefix, and the required
# group, with an escalation hint.
reasons contains msg if {
some prefix in denied_domains
group := sensitive_domains[prefix]
label := domain_labels[prefix]
msg := sprintf("Access to the %s data domain (BigQuery dataset prefix '%s') requires membership in the '%s' IdP group, which your identity does not carry. Request the '%s' group from your data-governance owner and retry; if you believe this dataset is misclassified, contact your data platform team.", [label, prefix, group, group])
}
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)
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