dtwo Policy Store

Read-Only Stripe by Default (Role-Gate Billing Writes)

Establishes a read-only-by-default Stripe posture over the MCP path. The named write and destructive billing tools —

Direction
ingress
Rego package
stripe.ingress.role_gate_writes
App
stripe
Bundles
soc2pci-dsssoxgdpr-ccpa
Published
Minimum gateway
1.0.0b24
Schema version
1.0.0
Checksum
sha256:39f42427691ac49396db200c521de055b6091fe155b8d33c1e0b8ea83e20125b

striperole-gate-writesingressleast-privilegerbacsoc2pci-dsssoxgdpr-ccpa

What this policy does

Direction: ingress (tool_pre_invoke) Default: deny gated billing writes unless the caller is in an allowed IdP group; allow everything else Package: stripe.ingress.role_gate_writes

What it does

Establishes a read-only-by-default Stripe posture over the MCP path. The named write and destructive billing tools —

  • *create_customer, *create_product, *create_price, *create_payment_link
  • *create_invoice, *create_invoice_item, *finalize_invoice, *create_coupon
  • *update_subscription, *update_dispute, *cancel_subscription

— are denied unless the caller's IdP groups claim includes finance or billing-admin. Read, search, and fetch tools (*list_*, *search_stripe_resources, *fetch_stripe_resources, *stripe_api_read, *retrieve_balance, *get_stripe_account_info, documentation/search tools) always pass — they never match the gated suffix list, so the blocklist design leaves them untouched.

Missing identity fails closed for the gate: if the gateway populates no subject, no claims, or no groups claim, the caller is not in an allowed group and the write is denied. Reads remain available to everyone regardless of claims.

Refunds (*create_refund) and the *stripe_api_write passthrough are intentionally out of scope — they are governed by the dedicated refund-cap and escape-hatch companion policies (see Composition). This keeps the policy to the single job of gating ordinary billing writes.

Compliance alignment

  • SOC 2 CC6.1 — supports logical access security over a protected financial system by restricting who can mutate Stripe billing objects through the agent channel; CC6.3 — supports role-based access and least privilege: writes are tied to named IdP groups, read-only is the default for everyone else; CC6.2 — because the gate reads live IdP claims per call, deprovisioning a user from the finance group revokes agent write access at the next token; PI1.2 — supports input authorization: billing inputs enter Stripe only from authorized roles.
  • PCI DSS 7.2.1 / 7.2.2 — supports a least-privilege access model over the cardholder-adjacent billing environment: access to modify customers, invoices, subscriptions, and disputes is limited to job-classified roles; 7.2.5 — supports least privilege for application/system accounts by narrowing what the agent's broad OAuth grant or restricted key can actually be used for on the MCP path.
  • SOX ITGC — access to programs and data — supports least-privilege access to a financial system that feeds revenue records (invoices, subscriptions, coupons, disputes); SoD (COSO Principle 10) — supports initiate/approve separation by keeping billing mutations out of non-finance hands.
  • GDPR Art. 25 — supports data protection by design/default on the agent channel: creating customer objects (name, email — PII) requires an authorized role; Art. 29 / 32(4) — supports processing only on the controller's instructions: unauthorized actors (including a prompt-injected agent acting for a non-finance user) cannot mutate personal or billing data; Art. 5(1)(b) — supports purpose limitation; CCPA §1798.100(e) — supports reasonable security procedures over consumers' personal information.

Tool name matching

Matching is by suffix on lower(input.resource.name) — the DTwo gateway prefixes tool names with the configured MCP server name (e.g. stripe-mcp-create_invoice), and that prefix is not standardized. Suffix matching keeps the policy portable across:

  • Official legacy @stripe/mcp (≤ v0.8.x) verb_resource names — all eleven gated names verified from the stripe/ai repo history (the Claude Desktop .dxt manifest still ships them).
  • Community noun_verb names (atharvagupta2003/mcp-stripe style, e.g. customer_create) — the inverted forms of the same eleven tools are also gated. Only customer_create is verified from that repo; the other inversions are defensive and unverified (see Known limitations).
  • Official current server (mcp.stripe.com) — its per-resource write surface is collapsed into stripe_api_write, which this policy deliberately does not match (see Composition). Its read/meta tools never match the gated suffixes and pass.

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

Argument shape

None. The decision is made purely from the tool name and the caller's identity (input.subject.claims.groups); input.payload.args is never inspected. The tool name is read from both input.resource.name and input.payload.name, and the gate fires if either ends with a gated suffix — so a call that carries the tool name only in the payload (a missing/empty resource.name) is still gated rather than falling through to the read-only allow branch. The groups claim is expected to be an array of strings (the common IdP shape); group comparison is case-insensitive. Any other shape (single string, CSV, object, null) fails closed — the caller is treated as not in an allowed group.

Examples

Allowed — read tool, no identity needed

{
  "input": {
    "action": "tool_pre_invoke",
    "resource": { "name": "stripe-mcp-list_customers", "type": "tool" },
    "payload": { "name": "stripe-mcp-list_customers", "args": { "limit": 10 } }
  }
}

allow = true, no reason.

Allowed — billing write from a finance-group member

{
  "input": {
    "action": "tool_pre_invoke",
    "resource": { "name": "stripe-mcp-create_invoice", "type": "tool" },
    "subject": {
      "sub": "google-apps|ap@example.com",
      "claims": { "groups": ["finance", "employees"] }
    },
    "payload": {
      "name": "stripe-mcp-create_invoice",
      "args": { "customer": "cus_123", "days_until_due": 30 }
    }
  }
}

allow = true, no reason.

Denied — billing write without an allowed group

{
  "input": {
    "action": "tool_pre_invoke",
    "resource": { "name": "stripe-mcp-create_customer", "type": "tool" },
    "subject": {
      "sub": "google-apps|dev@example.com",
      "claims": { "groups": ["engineering"] }
    },
    "payload": {
      "name": "stripe-mcp-create_customer",
      "args": { "name": "Jane Doe", "email": "jane@example.com" }
    }
  }
}

allow = false, reason = "Stripe billing writes (creating or updating customers, products, prices, payment links, invoices, coupons, subscriptions, or disputes) are restricted to the finance and billing-admin groups — your account is not in either group. Read and search tools remain available. Ask a billing administrator to make this change, or contact your InfoSec team if you believe you should have write access."

Composition

This policy is single-purpose: it gates ordinary billing writes by role. Pair it with:

  • Refund cap (apps/stripe, PF-09 family) — governs *create_refund / *refund_create: amount ceilings and deny-on-full-refund. Refunds move money and deserve their own thresholds, not just a group gate.
  • Escape-hatch deny (apps/stripe, PF-22 family) — governs *stripe_api_write, the current official server's generic POST/PATCH/PUT/DELETE passthrough. Without that companion, a caller on the current server bypasses this policy's gate entirely.
  • Dispute-submit gate (PF-15 family) — update_dispute with submit: true files evidence irreversibly with the card network; a human-approval policy can strip or deny submit even for finance-group members.
  • Egress PII redaction / PAN masking (PF-02 / PF-01 families) — the read tools this policy leaves open are bulk PII egress channels (customer lists with emails).

Known limitations

  • Group names are placeholders — replace finance and billing-admin with your IdP's group names at import time. If your IdP emits roles under a different claim (e.g. Auth0 permissions, or a namespaced claim like https://acme.com/groups), change the claim key in caller_groups.
  • No claims → no writes. If the gateway's jwt_audience is misconfigured or the IdP omits the groups claim, every gated write is denied for everyone. That is the intended fail-closed direction, but verify claims with dtwo-list-claims or the dump-input technique before rollout.
  • groups must be an array. A string-valued claim ("finance") or CSV ("finance,hr") does not iterate and fails closed. Adapt caller_groups if your IdP emits a non-array shape.
  • Unverified community suffixes. Of the noun_verb inversions, only customer_create is verified from the community server's source; the other ten are defensive guesses. Over-matching on the deny side fails safe, but a community tool with a different name (e.g. a hypothetical invoice_send) would not be gated.
  • Enumerated allowlist, not a create_*/update_* wildcard. The gate matches a fixed list of eleven known write/destructive tools (plus their community inversions), not every create_*/update_* tool. This is deliberate — a broad *create_* wildcard would also catch create_refund, which is intentionally delegated to the refund-cap companion. The trade-off: money-relevant writes that exist under other names are not gated here. In particular the community atharvagupta2003/mcp-stripe server exposes payment_intent_create (and, by the same noun_verb convention, plausibly charge_create / payout_create / transfer_create / topup_create); none of these are on the list, so an unauthorized caller can invoke them through this policy. Charges, payment intents, payouts, and transfers are money movement — gate them with the PF-09 gate-money-movement companion (and RAK scoping), not this role gate. On the official current server the same operations arrive as stripe_api_write (see below), which this policy also does not match.
  • Not a complete write freeze. *stripe_api_write, *create_refund, and stripe_report (whose report creation is a write) pass through this policy by design — deploy the companion policies above for full coverage. Composio's STRIPE_* catalog (~415 auto-generated tools) and Stripe's unpublished Treasury "agentic finance" preview tools use different names and are not covered by these suffixes.
  • MCP path only. The Stripe dashboard, direct API keys, and webhooks are outside the gateway's reach — layer this policy with Restricted API Key (RAK) scoping rather than treating either as sufficient alone.

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 stripe.ingress.role_gate_writes

# Read-only Stripe by default: deny the gated billing writes below unless the
# caller's IdP groups include an allowed billing role. Everything else passes.
default allow := false

# Ordinary billing write / destructive tool suffixes to gate.
# The gateway prefixes tool names with the configured MCP server name
# (e.g. `stripe-mcp-create_invoice`), so we match on the suffix.
# NOTE: `*create_refund` and `*stripe_api_write` are intentionally absent —
# they are governed by the dedicated refund-cap and escape-hatch policies.
gated_write_suffixes := [
    # Official legacy @stripe/mcp (<= v0.8.x) verb_resource names — verified
    # from the stripe/ai repo history and the Claude Desktop .dxt manifest.
    "create_customer",
    "create_product",
    "create_price",
    "create_payment_link",
    "create_invoice",
    "create_invoice_item",
    "finalize_invoice",
    "create_coupon",
    "update_subscription",
    "update_dispute",
    "cancel_subscription",
    # Community noun_verb inversions (atharvagupta2003/mcp-stripe style).
    # Only customer_create is verified from that repo's source; the rest are
    # defensive inversions — over-matching on the deny side fails safe.
    "customer_create",
    "product_create",
    "price_create",
    "payment_link_create",
    "invoice_create",
    "invoice_item_create",
    "invoice_finalize",
    "coupon_create",
    "subscription_update",
    "dispute_update",
    "subscription_cancel",
]

# IdP groups permitted to perform billing writes. PLACEHOLDERS — replace with
# your IdP's group names at import time. Compared case-insensitively.
allowed_groups := {"finance", "billing-admin"}

# All tool-name fields this call carries. The gateway normally populates
# resource.name; we also consider payload.name so the gate still fires when
# resource.name is absent — a gated write is never allowed merely because the
# name field the policy reads happened to be empty (fail-safe).
candidate_tool_names := {name |
    some raw in [
        object.get(object.get(input, "resource", {}), "name", ""),
        object.get(object.get(input, "payload", {}), "name", ""),
    ]
    name := lower(raw)
}

# The call targets one of the gated billing write tools (matched on either the
# resource name or the payload name).
is_gated_write if {
    some name in candidate_tool_names
    some suffix in gated_write_suffixes
    endswith(name, suffix)
}

# Caller's IdP groups. Missing subject/claims/groups resolves to [] — the
# membership check below then never fires, so the gate fails closed.
caller_groups := object.get(object.get(object.get(input, "subject", {}), "claims", {}), "groups", [])

# Caller is in at least one allowed billing group (case-insensitive).
caller_in_allowed_group if {
    some group in caller_groups
    allowed_groups[lower(group)]
}

# Reads, searches, fetches, and anything else not on the gated list pass.
allow if {
    not is_gated_write
}

# Gated billing writes require an allowed IdP group.
allow if {
    is_gated_write
    caller_in_allowed_group
}

reasons contains "Stripe billing writes (creating or updating customers, products, prices, payment links, invoices, coupons, subscriptions, or disputes) are restricted to the finance and billing-admin groups — your account is not in either group. Read and search tools remain available. Ask a billing administrator to make this change, or contact your InfoSec team if you believe you should have write access." if {
    is_gated_write
    not caller_in_allowed_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)

Used in these guides