dtwo Policy Store

Fence Sensitive Box Folders by IdP Group

Fences pinned sensitive Box subtrees (HR, Finance, Legal, …) by ID.

Direction
ingress
Rego package
box.ingress.fence_sensitive_folders
App
box
Bundles
soc2hipaagdpr-ccpa
Published
Minimum gateway
1.0.0b24
Schema version
1.0.0
Checksum
sha256:44bf6f7b7b2874b2aa81637a97f8375767cd559bb8566acac8be339f6dac5ab4

boxfence-sensitive-scopesingresssoc2hipaagdpr-ccpa

What this policy does

Direction: ingress (tool_pre_invoke) Default: deny fenced targets for callers outside the mapped group, allow otherwise Package: box.ingress.fence_sensitive_folders

What it does

Fences pinned sensitive Box subtrees (HR, Finance, Legal, …) by ID. The policy carries two placeholder maps — fenced_folders and fenced_files — that pair Box folder/file IDs with the IdP group required to touch them (e.g. folder 1234567890finance). At ingress it denies:

  • File reads, moves, and copiesget_file_content, get_download_url, get_file_preview, ai_qa_single_file, ai_qa_multi_file, ai_extract_*, move_file, copy_file (official server) and box_file_download_tool / box_file_text_extract_tool / box_file_copy_tool (community server) — when the target file_id (or any items[].id / file_ids[] entry on multi-file AI tools) appears in fenced_files and the caller lacks the mapped group.
  • Folder listings, details, moves, and copieslist_folder_content_by_folder_id, get_folder_details, move_folder, copy_folder (official) and box_folder_items_list_tool / box_folder_move_tool (community) — when the target folder_id appears in fenced_folders and the caller lacks the mapped group.
  • Search — community box_search_tool is denied for non-privileged callers unless ancestor_folder_ids is present and every fenced ID in it maps to a group the caller holds (an unscoped search is allowed only for callers holding every fenced group, since it can surface content from any tree). Official search_files_keyword / search_files_metadata folder-scoping is checked the same way where the scoping argument is present.

Group membership is read from input.subject.claims.groups via object.get chains and fails closed: a missing, empty, or malformed groups claim never grants access to a fenced target. 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 Box trees to their mapped groups; P4.1 — supports limiting personal-information use to identified purposes by keeping PI-bearing folders behind role fences on the agent channel.
  • HIPAA §164.308(a)(4) — supports information access management: access to PHI-bearing Box folders is authorized by IdP group on the MCP path; §164.522(a) — fenced file IDs can encode agreed-to restrictions on specific patient records.
  • 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 folders to a minimal group.

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. box-remote-get_file_content):

  • Official (mcp.box.com): get_file_content, get_download_url, get_file_preview, ai_qa_single_file, ai_qa_multi_file, move_file, copy_file, list_folder_content_by_folder_id, get_folder_details, move_folder, copy_folder, search_files_keyword, search_files_metadata; any tool whose name contains ai_extract_ (covers all six ai_extract_* variants).
  • Community (box-community/mcp-server-box): box_file_download_tool, box_file_text_extract_tool, box_file_copy_tool, box_folder_items_list_tool, box_folder_move_tool, box_search_tool.

Verify the exact names your gateway sends with the dump-input debug technique before relying on this in production.

Argument shape

  • File tools: file_id (string or number). Multi-file AI tools: items[].id; a flat file_ids[] array is also checked defensively.
  • Folder tools: folder_id (string or number).
  • Search tools: ancestor_folder_ids as an array of IDs or a comma-separated string. On the official server the exact scoping field name is partially unverified (see Known limitations).

Examples

Allowed

{
  "input": {
    "action": "tool_pre_invoke",
    "resource": { "name": "box-remote-get_file_content", "type": "tool" },
    "subject": { "sub": "google-apps|dev@example.com", "claims": { "groups": ["engineering"] } },
    "payload": {
      "name": "box-remote-get_file_content",
      "args": { "file_id": "5550001111" }   // not in fenced_files
    }
  }
}

allow = true, no reason.

Denied

{
  "input": {
    "action": "tool_pre_invoke",
    "resource": { "name": "box-remote-get_file_content", "type": "tool" },
    "subject": { "sub": "google-apps|dev@example.com", "claims": { "groups": ["engineering"] } },
    "payload": {
      "name": "box-remote-get_file_content",
      "args": { "file_id": "9876543210" }   // fenced_files → finance
    }
  }
}

allow = false, reason = "Box file 9876543210 is fenced as sensitive and requires the 'finance' IdP group. (...)".

Composition

This policy is single-purpose: it fences reads/moves/search of pinned sensitive IDs. Useful companions:

  • An external-sharing guard on create_collaboration / *shared_link* tools, so fenced content that an authorized caller reads cannot be re-shared outward.
  • An egress PII/PHI redaction policy on get_file_content, ai_qa_*, and search responses — it also mops up snippets an unscoped official search may surface (see Known limitations).
  • A destructive-op gate for the community server's box_file_delete_tool / box_folder_delete_tool.

Known limitations

  • Literal-ID matching only — no ancestry resolution. The policy matches the exact IDs in its maps and cannot resolve Box folder ancestry statelessly. A file reached through an unlisted descendant ID is not fenced. Customers must pin the sensitive subtree's folder IDs (the root and any high-value descendant folders/files) at import time; the shipped ID lists are placeholders.
  • Broad operations can still surface fenced descendants. The same statelessness means an operation whose own target is an unfenced ancestor of a fenced tree is not caught: a recursive community listing of a parent folder (box_folder_items_list_tool with is_recursive: true, e.g. folder_id: "0") enumerates items inside fenced subtrees, and a community search scoped to an unfenced ancestor (ancestor_folder_ids: ["0"], or a malformed non-comma scope string that parses to a single non-fenced token) reads into fenced subtrees — both satisfy the "scoped is safe" allow rule and thereby sidestep the unscoped-search restriction. The scoped checks only test whether the literal ancestor IDs supplied are themselves fenced. Mitigate by pinning the sensitive root folder IDs (so a recursive listing or scoped search of the root itself is denied), pairing with an egress redaction policy (see Composition), and/or restricting recursive listing and root-scoped search operationally.
  • Move/copy argument names partially unverified. The copy_file / box_file_copy_tool fences read file_id, and move_folder / copy_folder / box_folder_move_tool read folder_id, matching the underlying Box API and the move/read tools already covered. Box does not publish these schemas; if a live tool names the moved/copied source item differently, that fence silently does not fire. Confirm against a live tools/list.
  • Placeholder configuration. Folder/file IDs and group names are placeholders — replace finance with your IdP's group name at import time, and replace the IDs with your real Box folder/file IDs.
  • Official search scoping is partially unverified. Box's docs do not publish the official server's full argument schemas; this policy assumes the scoping argument is named ancestor_folder_ids (matching the underlying Box Search API). If the live tool uses a different field name, the scoping check silently never fires. Confirm against a live tools/list before relying on it.
  • Unscoped official search passes. Per the design, search_files_keyword / search_files_metadata are only checked when the scoping argument is present — an unscoped official search can still surface fenced-tree snippets (Box enforces the caller's own Box permissions, but not this policy's group fence). Pair with an egress redaction policy, or tighten this policy to deny unscoped official search if that residual is unacceptable.
  • AI-extract argument shapes are partially unverified. The ai_extract_* and ai_qa_multi_file checks read file_id, items[].id, and file_ids[]; if the live schema nests file references elsewhere, those calls pass unchecked.
  • Metadata reads are not fenced. get_file_details, list_file_comments, and community box_file_info_tool can still reveal fenced item names/metadata; this policy targets content reads, listings, moves, and search only.
  • 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 (e.g. a namespaced custom claim), 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 box.ingress.fence_sensitive_folders

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

# ---------------------------------------------------------------------------
# Fence configuration — PLACEHOLDERS, replace at import time.
#
# The policy matches literal IDs and cannot resolve Box folder ancestry
# statelessly, so pin the folder IDs of every sensitive subtree root AND any
# high-value descendant folders here. Group names must match your IdP's
# `groups` claim values (compared case-insensitively).

# Box folder ID -> IdP group required to list/read/search inside it.
fenced_folders := {
    "1234567890": "finance", # e.g. /Finance subtree root
    "1234567891": "hr",      # e.g. /HR subtree root
    "1234567892": "legal",   # e.g. /Legal subtree root
}

# Box file ID -> IdP group required to read or move it. Use for pinned
# high-value documents (payroll exports, cap tables, case files).
fenced_files := {
    "9876543210": "finance", # e.g. payroll-2026.xlsx
    "9876543211": "hr",      # e.g. employee-roster.xlsx
}

# ---------------------------------------------------------------------------
# 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)
}

# Every distinct group referenced by the fence maps. A caller holding all of
# them may run unscoped community searches (they could read every tree anyway).
fence_groups contains group if {
    some _, group in fenced_folders
}

fence_groups contains group if {
    some _, group in fenced_files
}

privileged_search_caller if {
    every group in fence_groups {
        caller_has_group(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. Verify the exact names your
# gateway sends with the dump-input debug technique.

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]))
}

# File-targeting tools (official server) — content reads, previews, AI Q&A,
# and moves of a specific file ID.
is_file_tool if tool_matches("get_file_content")

is_file_tool if tool_matches("get_download_url")

is_file_tool if tool_matches("get_file_preview")

is_file_tool if tool_matches("ai_qa_single_file")

is_file_tool if tool_matches("ai_qa_multi_file")

is_file_tool if tool_matches("move_file")

# Copying a fenced file duplicates its content into a caller-chosen (unfenced)
# location, from which it can be read freely — so fence copy like move/read.
is_file_tool if tool_matches("copy_file")

# All six official ai_extract_* variants send file content through Box AI.
is_file_tool if contains(tool_name, "ai_extract_")

# File-targeting tools (community box-community/mcp-server-box).
is_file_tool if tool_matches("box_file_download_tool")

is_file_tool if tool_matches("box_file_text_extract_tool")

is_file_tool if tool_matches("box_file_copy_tool")

# Folder-targeting tools — listings, details, and moves/copies of a specific
# folder ID. Relocating or duplicating a fenced folder by its literal ID is
# fenced too (symmetric with move_file); reaching a fenced tree through an
# UNFENCED ancestor ID cannot be caught statelessly — see Known limitations.
is_folder_tool if tool_matches("list_folder_content_by_folder_id")

is_folder_tool if tool_matches("get_folder_details")

is_folder_tool if tool_matches("move_folder")

is_folder_tool if tool_matches("copy_folder")

is_folder_tool if tool_matches("box_folder_items_list_tool")

is_folder_tool if tool_matches("box_folder_move_tool")

# Search tools.
is_community_search_tool if tool_matches("box_search_tool")

is_official_search_tool if tool_matches("search_files_keyword")

is_official_search_tool if tool_matches("search_files_metadata")

is_search_tool if is_community_search_tool

is_search_tool if is_official_search_tool

# Any tool this policy inspects.
is_fenced_scope_tool if is_file_tool

is_fenced_scope_tool if is_folder_tool

is_fenced_scope_tool if is_search_tool

# ---------------------------------------------------------------------------
# Argument extraction — object.get everywhere; Box IDs may arrive as strings
# or numbers, so normalize both to a trimmed string.

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

to_id(x) := trim_space(x) if is_string(x)

to_id(x) := sprintf("%v", [x]) if is_number(x)

# Target file IDs: single `file_id`, multi-file `items[].id`, and a flat
# `file_ids[]` array (defensive — multi-file AI schemas are partially
# unverified).
requested_file_ids contains id if {
    id := to_id(object.get(args, "file_id", ""))
    id != ""
}

requested_file_ids contains id if {
    some item in object.get(args, "items", [])
    id := to_id(object.get(item, "id", ""))
    id != ""
}

requested_file_ids contains id if {
    some raw in object.get(args, "file_ids", [])
    id := to_id(raw)
    id != ""
}

requested_folder_id := to_id(object.get(args, "folder_id", ""))

# Search folder scoping: `ancestor_folder_ids` as an array of IDs or a
# comma-separated string (the official server's exact field name is partially
# unverified — see Known limitations).
ancestor_ids contains id if {
    raw := object.get(args, "ancestor_folder_ids", [])
    is_array(raw)
    some x in raw
    id := to_id(x)
    id != ""
}

ancestor_ids contains id if {
    raw := object.get(args, "ancestor_folder_ids", "")
    is_string(raw)
    some part in split(raw, ",")
    id := trim_space(part)
    id != ""
}

# ---------------------------------------------------------------------------
# Fence checks.

blocked_file_access if {
    some id in requested_file_ids
    group := object.get(fenced_files, id, "")
    group != ""
    not caller_has_group(group)
}

blocked_folder_access if {
    group := object.get(fenced_folders, requested_folder_id, "")
    group != ""
    not caller_has_group(group)
}

blocked_ancestor if {
    some id in ancestor_ids
    group := object.get(fenced_folders, id, "")
    group != ""
    not caller_has_group(group)
}

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

# Any tool this policy does not inspect passes through untouched.
allow if {
    not is_fenced_scope_tool
}

allow if {
    is_file_tool
    not blocked_file_access
}

allow if {
    is_folder_tool
    not blocked_folder_access
}

# Scoped community search: allowed when every fenced ancestor ID maps to a
# group the caller holds (or no fenced ID is present).
allow if {
    is_community_search_tool
    count(ancestor_ids) > 0
    not blocked_ancestor
}

# Unscoped community search can surface content from any fenced tree, so it
# is reserved for callers holding every fenced group.
allow if {
    is_community_search_tool
    count(ancestor_ids) == 0
    privileged_search_caller
}

# Official search: fence check applies only when the scoping argument is
# present (unscoped official search passes — documented residual).
allow if {
    is_official_search_tool
    not blocked_ancestor
}

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

reasons contains msg if {
    is_file_tool
    some id in requested_file_ids
    group := object.get(fenced_files, id, "")
    group != ""
    not caller_has_group(group)
    msg := sprintf("Box file %s is fenced as sensitive and requires the '%s' IdP group. Ask your Box administrator for access, or contact InfoSec if this fence looks wrong.", [id, group])
}

reasons contains msg if {
    is_folder_tool
    group := object.get(fenced_folders, requested_folder_id, "")
    group != ""
    not caller_has_group(group)
    msg := sprintf("Box folder %s is a fenced sensitive tree and requires the '%s' IdP group. Ask your Box administrator for access, or contact InfoSec if this fence looks wrong.", [requested_folder_id, group])
}

reasons contains msg if {
    is_search_tool
    some id in ancestor_ids
    group := object.get(fenced_folders, id, "")
    group != ""
    not caller_has_group(group)
    msg := sprintf("Searching inside Box folder %s requires the '%s' IdP group. Remove it from ancestor_folder_ids, or ask your Box administrator for access.", [id, group])
}

reasons contains "Unscoped Box search is restricted while sensitive folders are fenced. Re-run the search with ancestor_folder_ids scoped to folders you may access, or ask your Box administrator for the fenced groups." if {
    is_community_search_tool
    count(ancestor_ids) == 0
    not privileged_search_caller
}

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)