Fence Tableau Datasource Scope
Tableau's MCP server is a warehouse proxy: query-datasource runs a VizQL Data Service (VDS) query and returns raw row-level data — PII, PHI, payroll,…
- Direction
- ingress
- Rego package
tableau.ingress.fence_datasource_scope- App
- tableau
- Bundles
- soc2gdpr-ccpa
- Published
- Minimum gateway
- 1.0.0b24
- Schema version
- 1.0.0
- Checksum
sha256:21a1f19606553712a3c9d296f79e427199979cbb3de679a2236a084799e310bc
tableaufence-sensitive-scopesaccess-controldatasourceingresssoc2gdpr-ccpa
What this policy does
Direction: ingress (tool_pre_invoke)
Default: deny; explicit allows for non-fenced tools, approved-datasource queries, and cleared image renders
Package: tableau.ingress.fence_datasource_scope
What it does
Tableau's MCP server is a warehouse proxy: query-datasource runs a VizQL
Data Service (VDS) query and returns raw row-level data — PII, PHI,
payroll, financials — from whatever the published datasource connects to, and
the image-render tools return the same data drawn as pixels. This policy
fences two of those surfaces at ingress, before the call reaches Tableau.
It enforces two independent, group-scoped controls:
query-datasource— per-datasource allowlist. The call is denied unless itsdatasourceLuidargument is a member of a per-tenant allowlist of approved datasource LUIDs (approved_datasources).datasourceLuidis the clean scope dimension the VDS schema exposes, so this confines the agent channel to datasources an operator has cleared (minimum-necessary / least-privilege). A missing, empty, or non-allowlisteddatasourceLuidfails closed and is denied.get-view-image/get-custom-view-image— analyst-only. These tools return PNG renders of a view. Egress redaction cannot parse pixels, so masking is impossible and deny is the only meaningful control. The policy denies these two tools for any caller whose IdP groups do not include the analyst group (data-analysts, a placeholder). A missing subject, missing claims, or missing/malformedgroupsclaim yields no memberships and fails closed.
Every other Tableau tool — catalog/metadata reads (list-datasources,
get-datasource-metadata, get-view, list-workbooks…), the CSV data reads
(get-view-data, get-custom-view-data), Pulse, admin-insights, token, and
mutation tools — and all non-Tableau tools pass through this policy untouched.
Those surfaces are governed by companion policies (see Composition).
Identity gating
The image-render control reads the caller's IdP groups from
input.subject.claims.groups through object.get(...) chains, so a missing
subject, missing claims, or a missing/malformed groups claim resolves to an
empty membership set: no matching group means no access to the image tools.
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 Tableau's data-query and image-render paths and supports alignment with:
- SOC 2 C1.1 — supports identification and protection of confidential information by confining agent queries to a governed set of datasources on the MCP path; P4.1 — supports limiting personal-information use to identified purposes by keeping un-cleared datasources and un-redactable image renders off the agent path.
- HIPAA §164.502(b)/§164.514(d) — supports the minimum-necessary / role-based-limit standard by scoping agent queries to approved datasources rather than every datasource the connected identity can reach; §164.308(a)(4) — supports information access management: which datasources the agent may query and who may pull image renders are operator decisions enforced at the gateway; §164.522(a) — the allowlist can encode agreed-to restrictions on specific datasources.
- GDPR Art. 9 — supports special-category protection by keeping datasources holding health, HR, or other Art. 9 data off the agent path until their LUID is allowlisted, and by denying image renders (which cannot be redacted) to non-analysts; CPRA §1798.121 — supports the right to limit use of sensitive personal information by fencing SPI-bearing datasources to a minimal allowlist; Art. 5(1)(b) — supports purpose limitation by keying datasource and image-render access to the caller's approved scope.
Why ingress
Both violations are fully determined by the request alone — the tool name, the
datasourceLuid argument, and the caller's claims — so enforcement happens
before the call reaches Tableau and restricted rows or renders are never
fetched into the model context. This matters most for image renders: once a
PNG is returned there is no egress control that can clean it, so the leak must
be prevented at ingress. For defense in depth, pair with the egress redaction
companion for the CSV data-read surfaces this policy does not fence.
Tool name matching
The official Tableau server uses kebab-case tool names with no vendor
prefix (query-datasource, get-view-image); the gateway prefixes them
with the configured MCP server name joined by a hyphen (e.g.
tableau-query-datasource), and that prefix is not standardized. The policy
matches case-insensitively by suffix on the distinctive tails:
query-datasource— matchesquery-datasource,tableau-query-datasource, etc. This tail is distinctive; it does not collide withget-datasource-metadataorlist-datasources.get-view-image— the standard-view PNG render.get-custom-view-image— the custom-view PNG render. (get-custom-view-imagedoes not end inget-view-image, so both suffixes are matched explicitly.)
Suffix matching keeps the policy portable across gateway prefixes. Verify the
exact names your gateway sends with the dump-input debug technique before
relying on this in production. Tableau Next (the Salesforce-hosted
analytics product) uses disjoint snake_case names (analyze_data,
get_visualization) and is not covered by this policy — author a separate
policy for that server.
Argument shape
query-datasource carries the target datasource as a scalar string
datasourceLuid (verified against the VDS query-tool schema). The policy
reads it with object.get(args, "datasourceLuid", "") and compares it
verbatim against approved_datasources. Tableau LUIDs are canonical
lowercase UUIDs; store them in the allowlist exactly as Tableau emits them. A
call that omits datasourceLuid, sends an empty value, or carries it under a
different key resolves to "", which is not in the allowlist, and is denied
(fail closed). The image-render tools take a viewId/customViewId (opaque
LUID) plus optional filters; this policy does not inspect their arguments — it
denies them wholesale for non-analysts.
Examples
Allowed — query against an approved datasource
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "tableau-query-datasource", "type": "tool" },
"payload": {
"name": "tableau-query-datasource",
"args": { "datasourceLuid": "11111111-1111-1111-1111-111111111111" } // on the allowlist
}
}
}
allow = true, no reason.
Allowed — image render by a data analyst
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "tableau-get-view-image", "type": "tool" },
"subject": { "sub": "auth0|amy", "claims": { "groups": ["data-analysts"] } },
"payload": {
"name": "tableau-get-view-image",
"args": { "viewId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" }
}
}
}
allow = true, no reason.
Denied — query against a datasource that is not allowlisted
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "tableau-query-datasource", "type": "tool" },
"payload": {
"name": "tableau-query-datasource",
"args": { "datasourceLuid": "99999999-9999-9999-9999-999999999999" } // not on the allowlist
}
}
}
allow = false, reason = "Tableau datasource 99999999-9999-9999-9999-999999999999 is not on the approved-datasource allowlist ...".
Denied — image render by a non-analyst
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "tableau-get-custom-view-image", "type": "tool" },
"subject": { "sub": "auth0|eng", "claims": { "groups": ["engineering"] } },
"payload": {
"name": "tableau-get-custom-view-image",
"args": { "customViewId": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" }
}
}
}
allow = false, reason = "Tableau image renders (get-view-image / get-custom-view-image) return PNGs that cannot be redacted ...".
Composition
This policy fences the datasource-query and image-render surfaces. Useful companions:
- An egress PII/PHI/PAN redaction policy on
query-datasource,get-view-data, andget-custom-view-dataresponses — those return row-level data / CSV as text and can be redacted, unlike the PNG renders this policy denies outright. This is the mandatory backstop for the CSV data-read path, which this ingress fence does not cover. - A
calculation-field guard onquery-datasourcefor non-analyst groups: the VDScalculationfield variant accepts an arbitrary Tableau calc expression that can reference any column in the (already-approved) datasource, sofieldCaption-level column fencing is bypassable — treat the presence ofcalculationas elevated. - A token-management deny (
get-embed-token,revoke-access-token,reset-consent) and a mutation gate on the delete/update tools and theirconfirm-twins. - A default-deny-unknown-tools policy (PF-28): the hosted Tableau server ships new tools automatically, so the tool inventory drifts forward without any client change.
Known limitations
- CSV data reads are not fenced here.
get-view-dataandget-custom-view-datareturn the same underlying data as the image tools, but as CSV text. A non-analyst deniedget-view-imagecan pull the same view's data throughget-view-data. That is intentional: CSV can be egress-redacted, so it is governed by the egress redaction companion rather than an ingress deny. Attach that companion — this policy alone leaves the CSV path open. calculationescape hatch inside an approved datasource. Once a datasource LUID is allowlisted, this policy does not restrict which columns or rows the query reads. The VDScalculationfield can reference any column in that datasource, so column-level fencing is out of scope here. Pair with thecalculation-field guard companion.- Allowlist is literal LUIDs.
datasourceLuidis compared verbatim againstapproved_datasources; a datasource reached by any other LUID is denied (the intended default-deny), which also means the allowlist must contain each cleared datasource's exact canonical LUID. The shipped LUIDs are placeholders — replace them with your tenant's real datasource LUIDs at import time. A caller cannot gain access by re-casing an approved LUID: a case-altered value is a different string, is not in the set, and is denied. - Off-schema
datasourceLuidstill fails closed, but its reason string is cosmetic. The VDS schema typesdatasourceLuidas a scalar string. A call that sends it as a non-string (number, array, object) or under a different key is not on the allowlist and is denied — the security decision is correct. For a non-string scalar the denial reason interpolates the raw value with%s, which can render a formatting artifact (e.g.%!s(int=123)); the deny is unaffected. SenddatasourceLuidas the canonical lowercase-UUID string. - Image deny is all-or-nothing. The image-render control is a pure
group gate — an analyst may render any view (subject to Tableau's own
permissions), and a non-analyst may render none. It does not scope image
renders by datasource, because the render tools take an opaque
viewId, not adatasourceLuid. - Only the official kebab-case server is fenced; snake_case servers pass
through. Suffix matching is hyphen-specific (
query-datasource,get-view-image), so any Tableau server that exposes the same data surfaces under snake_case names is not matched and passes through un-fenced. This covers the Salesforce-hosted Tableau Next product (analyze_data,get_visualization) and the community FastMCP servers the landscape note flags (e.g.query_datasource,get_view_image,get_view_data— tool names there are unverified). A non-allowlisted datasource query or a non-analyst image render issued against such a server would be allowed. This is by design — the policy is pinned to the official server's verified names and must not guess at unverified underscore names — but it means you must author a separate policy (and/or a PF-28 default-deny-unknown gate) for any non-kebab Tableau server your gateway exposes. Confirm the exacttools/listnames with the dump-input debug technique before trusting this fence. - Suffix matching anchors on the tail, so a name with extra characters
after the distinctive suffix is not matched. The match is
endswith(name, "query-datasource")/endswith(name, "get-view-image")/endswith(name, "get-custom-view-image"), which fires only when the distinctive tail is the end of the name. A drifted or versioned variant on the official server whose name carries a further suffix — e.g.tableau-query-datasource-v2,…-query-datasource-async, or…-get-view-image-hd— does not end in the anchored tail, so it is treated as an unfenced tool and passes through: a non-allowlisted datasource query or a non-analyst image render issued under such a name would be allowed. This is the flip side of anchoring on the tail rather than substring-matching (acontainsmatch would false-positive on names likeget-query-datasource-metadata), and it is why the policy must be paired with a PF-28 default-deny-unknown-tools gate: the hosted Tableau server ships new/renamed tools automatically, so re-verifytools/listwith the dump-input debug technique whenever the server version changes and pin any new query/image variant names into this policy's suffix list. - Identity placeholders. The analyst group name (
data-analysts) and every entry inapproved_datasourcesare placeholders — replace them with your IdP's group name and your tenant's real datasource LUIDs 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 tableau.ingress.fence_datasource_scope
# Deny-by-default: only the explicit allow rules below permit the request.
default allow := false
# ---------------------------------------------------------------------------
# CONFIG — PLACEHOLDERS, replace at import time.
#
# Approved datasource LUIDs the agent channel may query via `query-datasource`.
# Tableau LUIDs are canonical lowercase UUIDs; compare verbatim (not lowered).
# Pin the exact LUIDs an operator has cleared for the agent.
approved_datasources := {
"11111111-1111-1111-1111-111111111111", # e.g. the governed sales-metrics datasource
"22222222-2222-2222-2222-222222222222", # e.g. the governed ops datasource
}
# IdP group cleared to pull image renders (PNGs that cannot be redacted).
# PLACEHOLDER — remap to your IdP's group name at import time. Compared
# case-insensitively.
image_render_group := "data-analysts"
# ---------------------------------------------------------------------------
# Tool matching. Official server uses kebab-case, no vendor prefix; the gateway
# prefixes with the configured server name joined by a hyphen. Match
# case-insensitively by distinctive suffix so any prefix is covered. Verify
# exact names with the dump-input debug technique. Tableau Next (snake_case) is
# NOT matched by design.
tool_name := lower(object.get(object.get(input, "resource", {}), "name", ""))
# The VDS row-level query surface. `query-datasource` is distinctive and does
# not collide with `get-datasource-metadata` / `list-datasources`.
is_query_datasource_tool if endswith(tool_name, "query-datasource")
# The two PNG image-render surfaces. `get-custom-view-image` does not end in
# `get-view-image`, so both tails are matched explicitly.
is_image_render_tool if endswith(tool_name, "get-view-image")
is_image_render_tool if endswith(tool_name, "get-custom-view-image")
# ---------------------------------------------------------------------------
# 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
}
# ---------------------------------------------------------------------------
# Arguments — object.get everywhere so a missing field fails closed.
args := object.get(object.get(input, "payload", {}), "args", {})
requested_luid := object.get(args, "datasourceLuid", "")
# ---------------------------------------------------------------------------
# Allow rules.
# Any tool this policy does not fence passes through untouched (catalog reads,
# CSV data reads, Pulse, admin-insights, mutations, and all non-Tableau tools).
allow if {
not is_query_datasource_tool
not is_image_render_tool
}
# query-datasource: allowed only when the datasourceLuid is on the allowlist.
# A missing/empty LUID resolves to "" which is not in the set -> deny.
allow if {
is_query_datasource_tool
approved_datasources[requested_luid]
}
# Image renders: allowed only for callers in the analyst group.
allow if {
is_image_render_tool
member_of(image_render_group)
}
# ---------------------------------------------------------------------------
# Deny reasons.
# query-datasource naming a datasource that is not on the allowlist.
reasons contains msg if {
is_query_datasource_tool
requested_luid != ""
not approved_datasources[requested_luid]
msg := sprintf("Tableau datasource %s is not on the approved-datasource allowlist, so the agent may not query it. Query an approved datasource, or request datasource onboarding through your data-governance owner if you believe this one should be cleared.", [requested_luid])
}
# query-datasource with no datasourceLuid at all — fail closed.
reasons contains msg if {
is_query_datasource_tool
requested_luid == ""
msg := "This Tableau query supplied no datasourceLuid, so it cannot be matched against the approved-datasource allowlist. Re-issue the call naming an approved datasource, and request datasource onboarding through your data-governance owner if the one you need is not yet approved."
}
# Image render by a caller outside the analyst group.
reasons contains msg if {
is_image_render_tool
not member_of(image_render_group)
msg := "Tableau image renders (get-view-image / get-custom-view-image) return PNGs that cannot be redacted, so they are restricted to the data-analyst group. Use a CSV data read (get-view-data) or contact your data-governance owner if your role requires image exports."
}
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