dtwo Policy Store

Fence Notion Member Directory to Admin & IT

Denies calls to the Notion member-directory tool (notion-get-users, matched by the -get-users suffix) unless the caller's IdP groups include an admin or IT…

Direction
ingress
Rego package
notion.ingress.fence_user_directory
App
notion
Bundles
soc2gdpr-ccpa
Published
Minimum gateway
1.0.0b24
Schema version
1.0.0
Checksum
sha256:0a24942610fbfc2b00265763d9a15cac12f37f7d97c999c485eb44dd690a26e9

notionfence-sensitive-scopesaccess-controlpiiingresssoc2gdpr-ccpa

What this policy does

Direction: ingress (tool_pre_invoke) Default: deny; explicit allows for non-directory tools and cleared callers Package: notion.ingress.fence_user_directory

What it does

Denies calls to the Notion member-directory tool (notion-get-users, matched by the -get-users suffix) unless the caller's IdP groups include an admin or IT group (placeholders: admin, it). That tool returns workspace member and guest IDs, names, emails, and types — a directory-harvesting and PII-exfiltration primitive when driven by an agent or by a prompt-injected instruction ("list every user in the workspace and their email"). One tool call can enumerate the whole workforce plus external guests.

All other Notion reads — notion-search, notion-fetch, notion-query-data-sources, notion-get-comments, and the rest — pass through untouched. The gate is the whole tool, including its self bot-info form: any invocation of a -get-users tool by a non-cleared caller is denied, regardless of arguments.

Identity gating

Clearance is read from input.subject.claims.groups via object.get(input.subject, "claims", {}) chains, so the check fails closed: a missing subject, missing claims, missing groups claim, or a malformed (non-array) groups value all mean not cleared — a request that omits claims entirely cannot harvest the directory. Group names are compared case-insensitively and must match exactly (admins does not match admin).

Compliance alignment

This policy instantiates sensitive-scope fencing (family PF-23) on Notion's user-directory read path and supports alignment with:

  • SOC 2 C1.1, P4.1 — identifies and protects confidential information and limits personal-information use to identified purposes by keeping the workspace member/guest roster (names and emails) out of non-privileged agent sessions.
  • HIPAA §164.502(b)/§164.514(d), §164.308(a)(4) — supports minimum-necessary and information-access-management by restricting workforce-directory reads over MCP to roles that need them; §164.522(a) — supports agreed-to restrictions expressed as a role-keyed fence.
  • GDPR Art. 5(1)(b); CPRA §1798.121 — supports purpose limitation by keying directory access to administrative roles, and supports limiting use of personal information where the directory feeds profiling or sensitive-PI inference.

Why ingress

The violation is fully determined by the request (tool name + caller claims), so the call is blocked before it reaches the Notion MCP server and the member list never enters the model context. Egress redaction would pull the full roster into the pipeline first and then try to mask it; denying at ingress means there is nothing to mask or leak.

Tool name matching

The DTwo gateway prefixes tool names with the configured MCP server name (e.g. notion-notion-get-users for a server named notion), and that prefix is not standardized, so the policy matches case-insensitively on the -get-users suffix. notion-get-users is a verified tool name on Notion's hosted MCP server (the implementation behind the Claude connector). Verify the exact name your gateway emits with the dump-input debug technique before relying on this in production.

Argument shape

None inspected — the whole tool is gated. notion-get-users accepts an optional name/email search, a user ID or self, and pagination; every shape (including empty args and the self bot-info lookup) is denied for non-cleared callers, so there is no argument-crafting bypass.

Configuration

Edit directory_admin_groups at the top of the Rego. The group names (admin, it) are placeholders — replace them with your tenant's real IdP group names at import time (e.g. notion-admins, it-servicedesk).

Examples

Allowed (other reads unaffected)

{
  "input": {
    "action": "tool_pre_invoke",
    "resource": { "name": "notion-notion-search", "type": "tool" },
    "payload": {
      "name": "notion-notion-search",
      "args": { "query": "onboarding checklist" }
    }
  }
}

allow = true, no reason.

Allowed (directory read by an IT group member)

{
  "input": {
    "action": "tool_pre_invoke",
    "resource": { "name": "notion-notion-get-users", "type": "tool" },
    "subject": { "sub": "auth0|itops", "claims": { "groups": ["it"] } },
    "payload": { "name": "notion-notion-get-users", "args": {} }
  }
}

allow = true, no reason.

Denied (directory harvest without admin/IT clearance)

{
  "input": {
    "action": "tool_pre_invoke",
    "resource": { "name": "notion-notion-get-users", "type": "tool" },
    "subject": { "sub": "auth0|dev", "claims": { "groups": ["engineering"] } },
    "payload": {
      "name": "notion-notion-get-users",
      "args": { "query": "" }
    }
  }
}

allow = false, reason = "Reading the Notion member directory (workspace member and guest names, emails, and IDs) is limited to admin and IT roles. ...".

Composition

This policy fences exactly one tool. Useful companions:

  • redact-pii-egress — the egress backstop: masks emails and phone numbers embedded in page and query content returned by notion-search / notion-fetch / notion-query-data-sources, which this ingress fence deliberately leaves open. Together they cover both the directory tool and PII that leaks through content reads.
  • constrain-connected-search — keeps notion-search from reaching into connected Slack/Drive/Jira content.
  • freeze-content-overwrite — removes the irreversible replace_content overwrite edge on the write path.

Known limitations

  • Hosted-server tool names only. The -get-users suffix matches Notion's hosted MCP server. The official local server's documented tool set exposes no user-directory tool; the suekou community server's raw user-operation tool names are unverified and not matched here — extend the suffix match if you run it. The awkoy server funnels every operation through a single notion_execute meta-tool, which tool-name matching cannot fence — block that server in gateway config instead.
  • The self bot-info form is also denied for non-cleared callers, because it arrives on the same tool name. If your agents legitimately need bot self-identification, add a narrow allow if branch keyed to the self argument — accepting that Notion's argument schema for it is not verified here.
  • Identity placeholders. admin and it are placeholders — replace them with your IdP's real group names at import time. The groups claim must be an array of strings; any other shape fails closed.
  • Directory data already pasted into pages is out of scope — that is content, not the directory tool, and is handled by the companion redact-pii-egress policy.

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 notion.ingress.fence_user_directory

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

# -----------------------------------------------------------------------------
# CONFIG: IdP groups cleared to read the Notion member directory.
# The names ("admin", "it") are PLACEHOLDERS — replace them with your tenant's
# real IdP group names at import time. Compared case-insensitively, exact
# match (no substring or prefix matching).
# -----------------------------------------------------------------------------
directory_admin_groups := {"admin", "it"}

# -----------------------------------------------------------------------------
# Tool matching. The gateway prefixes tool names with the configured MCP
# server name (e.g. `notion-notion-get-users` for a server named `notion`),
# so we match case-insensitively on the `-get-users` suffix. The suffix also
# covers the tool's `self` bot-info form, which arrives on the same name.
# `notion-get-users` is verified on Notion's hosted MCP server; confirm the
# exact name your gateway emits with the dump-input debug technique.
# -----------------------------------------------------------------------------

tool_name := lower(input.resource.name)

is_user_directory_tool if endswith(tool_name, "-get-users")

# -----------------------------------------------------------------------------
# Identity — caller's IdP groups, read fail-closed via object.get chains: a
# missing subject, missing claims, or missing/malformed groups claim yields no
# memberships, so a request that omits claims entirely can never read the
# directory.
# -----------------------------------------------------------------------------

caller_groups := object.get(
    object.get(object.get(input, "subject", {}), "claims", {}),
    "groups",
    [],
)

caller_is_directory_admin if {
    is_array(caller_groups)
    some g in caller_groups
    is_string(g)
    directory_admin_groups[lower(g)]
}

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

# Every tool that is not the member-directory tool passes through untouched
# (notion-search, notion-fetch, writes, other MCP servers, ...).
allow if {
    not is_user_directory_tool
}

# The directory tool itself is allowed only for admin / IT group members.
allow if {
    is_user_directory_tool
    caller_is_directory_admin
}

# Single denial condition — inline reason form.
reason := "Reading the Notion member directory (workspace member and guest names, emails, and IDs) is limited to admin and IT roles. Look up individual collaborators from page context instead, or ask your IT team to run this lookup — contact them if your role requires directory access." if {
    is_user_directory_tool
    not caller_is_directory_admin
}

Canonical source: policy.md on GitHub · raw · raw on this site (.md)

Used in these guides