dtwo Policy Store

Fence Sensitive Dropbox Paths by Team

Fences protected Dropbox subtrees by path prefix . Dropbox addresses files and folders by a root-relative path (/Finance/2026/payroll.

Direction
ingress
Rego package
dropbox.ingress.fence_sensitive_paths
App
dropbox
Bundles
soc2hipaagdpr-ccpa
Published
Minimum gateway
1.0.0b24
Schema version
1.0.0
Checksum
sha256:a21a98aab34382aadfad11cf231ef36da8229ffd562ebc31200ab803061e646e

dropboxfence-sensitive-scopesingresssoc2hipaagdpr-ccpa

What this policy does

Direction: ingress (tool_pre_invoke) Default: deny fenced targets for callers outside the mapped team, allow otherwise Package: dropbox.ingress.fence_sensitive_paths

What it does

Fences protected Dropbox subtrees by path prefix. Dropbox addresses files and folders by a root-relative path (/Finance/2026/payroll.xlsx), so the policy carries a placeholder fenced_prefixes map that pairs a top-level path prefix with the IdP team group required to touch anything under it — /HR/hr, /Finance/finance, /Legal/legal, /Customers/customers. At ingress it gates every path-addressed Dropbox tool and denies, for callers who lack the mapped group:

  • Reads, listings, moves, copies, writes, and deletesListFolder, GetFileMetadata, GetFileContent, CreateFile, Copy, Move, Delete (official server) and their community synonyms (list_files, get_file_metadata, get_file_content, download_file, upload_file, copy_item, move_item, safe_delete_item on dbx-mcp-server; dropbox_list, dropbox_get_metadata, dropbox_download, dropbox_upload, dropbox_copy, dropbox_move, dropbox_delete on ngs) — when any path-bearing argument falls under a fenced prefix.
  • SearchSearch (official), search_file_db (dbx), dropbox_search (ngs). A search scoped to a path (or options.path) under a fenced prefix is denied for callers lacking that group. Because a search that supplies no path can surface names of files in any fenced tree (the same enumeration risk as ListFolder), the fail-closed rule below denies it as well — an agent cannot enumerate restricted filenames through ListFolder or Search.

A caller may touch a protected prefix only when their input.subject.claims.groups include the matching team group; everyone else is denied. Group membership is read from input.subject.claims.groups via object.get chains and fails closed: a missing, empty, or non-array groups claim never grants access to a fenced target.

The policy fails closed on the path too: a protected tool whose path argument is missing, empty, or not a string (an evasion attempt) is treated as unauthorised rather than allowed — the fence cannot verify a target it cannot read, so the request is denied. The prefix check runs case-insensitively on a normalised path: the path is lowercased, trimmed, given a single leading slash, and has runs of / collapsed to one (so //finance/x cannot dodge the /finance/ prefix) before comparison, and matching is prefix-aware — /finance fences /finance and /finance/... but not the sibling /finance-public. All tools this policy does not inspect pass through untouched.

Compliance alignment

  • SOC 2 C1.1 — supports identification and protection of confidential information by gating agent access to designated confidential Dropbox trees to their mapped groups; P4.1 — supports limiting personal-information use to identified purposes by keeping PI-bearing folders behind team fences on the agent channel.
  • HIPAA §164.308(a)(4) — supports information access management: access to PHI-bearing Dropbox folders is authorized by IdP group on the MCP path; §164.502(b) / §164.514(d) — supports the minimum-necessary standard by fencing PHI folders to the minimal team that needs them.
  • GDPR Art. 9 — supports special-category protection by fencing folders holding health, HR, or other Art. 9 data; CPRA §1798.121 — supports the right to limit use of sensitive personal information by fencing SPI paths to a minimal group; Art. 5(1)(b) — supports purpose limitation by keeping each protected tree accessible only to its owning team.

Tool name matching

Tool names are matched case-insensitively as an exact name or by -/_- separated suffix, so the policy tolerates any gateway server-name prefix (e.g. dropbox-GetFileContent):

  • Official (mcp.dropbox.com): listfolder, getfilemetadata, getfilecontent, createfile, copy, move, delete, search.
  • Community (dbx-mcp-server): list_files, get_file_metadata, get_file_content, download_file, upload_file, copy_item, move_item, safe_delete_item, search_file_db.
  • Community (ngs): dropbox_list, dropbox_get_metadata, dropbox_download, dropbox_upload, dropbox_copy, dropbox_move, dropbox_delete, dropbox_search.

Share-link and external-URL tools are intentionally not matched here — gate those with guard-share-links-external. Verify the exact names your gateway sends with the dump-input debug technique before relying on this in production.

Argument shape

Dropbox does not publish MCP JSON schemas, so the argument names are unverified and follow the Dropbox API v2 (files/*, files/search_v2) and the community server READMEs — see Known limitations. The policy reads the path defensively from the likely keys: path, from_path, to_path, src, dest, source, destination, and the search scope from path / options.path. Before shipping a policy pack, capture a live tools/list through the gateway and pin the real argument key(s) — if the live tool names the path under a key not in this list, that fence silently does not fire (though a present-but-non-string path still fails closed). Dropbox id:... handles are not resolvable to a path statelessly and are not fenced (see Known limitations).

Examples

Allowed — unfenced path

{
  "input": {
    "action": "tool_pre_invoke",
    "resource": { "name": "dropbox-GetFileContent", "type": "tool" },
    "subject": { "sub": "google-apps|dev@example.com", "claims": { "groups": ["engineering"] } },
    "payload": {
      "name": "dropbox-GetFileContent",
      "args": { "path": "/Projects/roadmap.pdf" }   // not under a fenced prefix
    }
  }
}

allow = true, no reason.

Denied — fenced path, caller lacks the team group

{
  "input": {
    "action": "tool_pre_invoke",
    "resource": { "name": "dropbox-GetFileContent", "type": "tool" },
    "subject": { "sub": "google-apps|dev@example.com", "claims": { "groups": ["engineering"] } },
    "payload": {
      "name": "dropbox-GetFileContent",
      "args": { "path": "/Finance/2026/payroll.xlsx" }   // under /Finance/ -> finance
    }
  }
}

allow = false, reason = "Dropbox path '/Finance/2026/payroll.xlsx' is inside the protected '/finance/' folder, which is restricted to the 'finance' group. (...)".

Denied — protected tool with no readable path (fail closed)

{
  "input": {
    "action": "tool_pre_invoke",
    "resource": { "name": "dropbox-Search", "type": "tool" },
    "subject": { "sub": "google-apps|dev@example.com", "claims": { "groups": ["engineering"] } },
    "payload": {
      "name": "dropbox-Search",
      "args": { "query": "salary" }   // no path scope -> cannot verify -> deny
    }
  }
}

allow = false, reason = "This Dropbox call is a protected file operation but supplied no readable path (...)".

Composition

This policy is single-purpose: it fences every path-addressed read, listing, move, copy, write, delete, and search of pinned sensitive path prefixes by team. Useful companions:

  • guard-share-links-external — so fenced content that an authorized caller reads cannot be re-shared outward via a public link or file request.
  • An egress PII/PHI redaction policy on GetFileContent / download_file and Search responses — it also mops up filenames returned by an allowed listing of an unfenced ancestor (see Known limitations).
  • A destructive-op gate for RestoreFolder / RestoreFileRevision, which this policy does not cover.

Known limitations

  • Prefix matching only — no path canonicalization or ID resolution. The policy matches the literal (lowercased, leading-slash-normalized, repeated slashes collapsed) path against its prefixes. It does not resolve Dropbox file id:... handles, ns:<namespace_id>/... namespace-relative paths, shared-link URLs, or ../. traversal tricks (Dropbox itself does not resolve ../., so those literal forms do not reach the real file, but a file legitimately addressed by its id: or ns: form is not fenced). Pin the sensitive subtree's path prefixes at import time and, where your workflow uses id:/ns: addressing, pair with an egress redaction policy.
  • Fail-closed on missing/unparseable path denies pathless calls. A protected tool with no readable string path — a Search with only a query, a ListFolder of the root, or a path supplied under an unrecognized key — is denied rather than allowed. This is deliberate: it is what stops an agent from enumerating fenced filenames through an unscoped ListFolder/Search, but it means legitimate whole-drive searches must be re-scoped to a path the caller may access.
  • Broad operations on an unfenced ancestor can still surface fenced descendants. A recursive ListFolder of an unfenced parent (e.g. /Projects) that happens to contain a fenced subtree enumerates items inside it. Mitigate by pinning the sensitive root prefixes (so listing the root itself is caught) and pairing with an egress redaction policy.
  • Argument names are unverified. Path and search-scope keys follow the Dropbox API v2 and the community server READMEs; Dropbox publishes no MCP schemas. If a live tool names the path differently, that fence silently does not fire — confirm against a live tools/list and pin the real key(s). A present-but-non-string path fails closed, so the safe failure mode holds for the keys that are checked.
  • Both source and destination paths are checked on move/copy. Moving a file out of a fenced tree (fenced from_path/src) and moving one into a fenced tree (fenced to_path/dest) both require the mapped group. This is deliberately conservative; a non-group caller relocating unrelated files into a fenced path is denied.
  • Placeholder configuration. Path prefixes and group names are placeholders — replace /hr, /finance, /legal, /customers and the group names with your real Dropbox paths and IdP groups values at import time. Group names are placeholders — replace finance with your IdP's group name at import time.
  • groups claim must be an array of strings. A string-valued or otherwise malformed claim fails closed (fenced targets deny). If your IdP emits groups under a different claim name, update caller_groups in the Rego.

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 dropbox.ingress.fence_sensitive_paths

# Deny-by-default: only the explicit allow rules below permit the request.
default allow := false

# ---------------------------------------------------------------------------
# Fence configuration — PLACEHOLDERS, replace at import time.
#
# Dropbox path prefix (lowercase, leading slash, NO trailing slash) -> IdP team
# group required to read/list/move/copy/write/delete/search inside it. A prefix
# fences itself and everything under it (`/finance` fences `/finance` and
# `/finance/...`, but not the sibling `/finance-public`). Groups are compared
# case-insensitively.
fenced_prefixes := {
	"/hr": "hr",
	"/finance": "finance",
	"/legal": "legal",
	"/customers": "customers",
}

# ---------------------------------------------------------------------------
# Identity — read groups via object.get chains so a missing subject/claims/
# groups fails closed (no group -> no access to fenced targets).

caller_groups := object.get(object.get(object.get(input, "subject", {}), "claims", {}), "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_groups
	lower(g) == lower(group)
}

# ---------------------------------------------------------------------------
# Tool matching. The gateway prefixes tool names with the configured MCP server
# name (separator not standardized), so match the exact name or a `-`/`_`-
# separated suffix, case-insensitively.

tool_name := lower(object.get(object.get(input, "resource", {}), "name", ""))

tool_matches(suffix) if tool_name == suffix

tool_matches(suffix) if endswith(tool_name, sprintf("-%s", [suffix]))

tool_matches(suffix) if endswith(tool_name, sprintf("_%s", [suffix]))

# Every path-addressed Dropbox tool this policy gates, across all three
# dialects: reads, listings, moves, copies, writes, deletes, and search.
protected_tool_suffixes := [
	# official (PascalCase, lowercased here)
	"listfolder", "getfilemetadata", "getfilecontent", "createfile",
	"createfolder", "copy", "move", "delete", "search",
	"listfilerevisions",
	# dbx-mcp-server (snake_case)
	"list_files", "get_file_metadata", "get_file_content", "download_file",
	"upload_file", "create_folder", "copy_item", "move_item",
	"safe_delete_item", "search_file_db",
	# ngs (dropbox_ prefix)
	"dropbox_list", "dropbox_get_metadata", "dropbox_download",
	"dropbox_upload", "dropbox_create_folder", "dropbox_copy",
	"dropbox_move", "dropbox_delete", "dropbox_search",
	"dropbox_get_revisions",
]

is_protected_tool if {
	some s in protected_tool_suffixes
	tool_matches(s)
}

# ---------------------------------------------------------------------------
# Argument extraction — object.get everywhere. Read the path defensively from
# the likely keys plus the search scope nested under `options.path`.

args := object.get(object.get(input, "payload", {}), "args", {})

path_keys := ["path", "from_path", "to_path", "src", "dest", "source", "destination"]

# Non-empty string paths supplied on the call.
requested_paths contains p if {
	some k in path_keys
	v := object.get(args, k, "")
	is_string(v)
	v != ""
	p := v
}

requested_paths contains p if {
	v := object.get(object.get(args, "options", {}), "path", "")
	is_string(v)
	v != ""
	p := v
}

# A path-like argument present but not a string (an evasion of the string
# match) — fail closed.
path_malformed if {
	some k in path_keys
	v := object.get(args, k, null)
	v != null
	not is_string(v)
}

path_malformed if {
	v := object.get(object.get(args, "options", {}), "path", null)
	v != null
	not is_string(v)
}

# ---------------------------------------------------------------------------
# Path normalization + prefix test. Case-insensitive, single leading slash.

# Collapse runs of `/` to a single slash so `//finance/x` cannot slip past the
# `/finance/` prefix test. RE2 pattern `/+` -> `/`.
normalized_path(p) := out if {
	t := regex.replace(lower(trim_space(p)), `/+`, "/")
	startswith(t, "/")
	out := t
}

normalized_path(p) := out if {
	t := regex.replace(lower(trim_space(p)), `/+`, "/")
	not startswith(t, "/")
	out := concat("", ["/", t])
}

# True when path `p` is at or under fenced prefix `prefix`.
path_under(p, prefix) if normalized_path(p) == prefix

path_under(p, prefix) if startswith(normalized_path(p), concat("", [prefix, "/"]))

# True when some requested path falls under a fenced prefix whose group the
# caller does not hold.
blocked_by_fence if {
	some p in requested_paths
	some prefix, group in fenced_prefixes
	path_under(p, prefix)
	not caller_has_group(group)
}

# ---------------------------------------------------------------------------
# Allow rules.

# Any tool this policy does not inspect passes through untouched.
allow if not is_protected_tool

# Protected tools: allowed only when a readable string path is present (fail
# closed on missing/unparseable path), no path argument is malformed, and no
# requested path is fenced-without-group.
allow if {
	is_protected_tool
	not path_malformed
	count(requested_paths) > 0
	not blocked_by_fence
}

# ---------------------------------------------------------------------------
# Deny reasons.

# Fenced target the caller may not touch — names the folder rule and the group.
reasons contains msg if {
	is_protected_tool
	some p in requested_paths
	some prefix, group in fenced_prefixes
	path_under(p, prefix)
	not caller_has_group(group)
	msg := sprintf("Dropbox path '%s' is inside the protected '%s/' folder, which is restricted to the '%s' group. Ask your Dropbox administrator to grant you the '%s' group, or contact InfoSec if this fence looks wrong.", [p, prefix, group, group])
}

# Fail closed: a protected tool with no readable string path cannot be verified.
reasons contains "This Dropbox call is a protected file operation but supplied no readable path, so the sensitive-path fence cannot verify it and the request fails closed. Re-issue with an explicit string path (for example the `path` argument scoped to a folder you may access)." if {
	is_protected_tool
	not path_malformed
	count(requested_paths) == 0
}

# Fail closed: a path argument that is present but not a string.
reasons contains "This Dropbox call carries a malformed path argument (not a string), so the sensitive-path fence cannot evaluate it and the request fails closed. Re-issue with a string path." if {
	is_protected_tool
	path_malformed
}

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)