---
name: "Confluence: Deny Org-Wide & Public Publication"
tags:
  - confluence
  - atlassian
  - deny-public-exposure
  - publication
  - governance
  - ingress
  - finserv-comms
  - eu-ai-act
  - soc2
  - gdpr-ccpa
publishedAt: 2026-07-12
description: |
  # confluence / deny-public-publication

  **Direction:** ingress (`tool_pre_invoke`)
  **Default:** deny on org-wide / public publications, allow everything else
  **Package:** `confluence.ingress.deny_public_publication`

  ## What it does

  Stops a prompt-injected or erring agent from broadcasting Confluence content
  org-wide or to anonymous external readers. On the two Confluence page
  create/update tools (`createConfluencePage` / `updateConfluencePage`, and the
  community `confluence_create_page` / `confluence_update_page`), the policy
  **denies** a write when either:

  - `contentType: "blog"` — a blog post broadcasts to the entire organization; or
  - `spaceId` (official) / `space_id` (community) is on a configured
    public / anonymous-access space list — a write there publishes
    externally-visible content instantly.

  Members of a placeholder `comms` group are exempt from the deny (they are the
  humans authorized to broadcast).

  On **creates** by callers outside the `comms` group, the policy additionally
  applies an ingress **transform** that forces `status: "draft"` (instead of
  `"current"`) and `isPrivate: true`, so the agent stakes out a draft and a human
  publishes it deliberately rather than the page going live the instant the agent
  calls the tool. Updates are never transformed (they operate on content a human
  already created), and comms-group callers keep full control.

  Every other Confluence tool — reads, searches, comment and label writes,
  attachment uploads, deletions — passes through untouched. This policy owns one
  surface: publication scope on page create/update.

  ## Compliance alignment

  This policy instantiates the public-exposure-deny family (PF-27,
  `deny-public-exposure`) on Confluence's publication surface, and supports
  alignment with:

  - **SOC 2 CC6.6, CC6.7** — boundary protection and restriction on the
    transmission/movement of information: denying agent-initiated org-wide blogs
    and public / anonymous-access-space writes keeps content from moving to a
    broad or external audience over the MCP path, and forcing agent creates to
    `draft` + `isPrivate` holds new content inside the boundary until a human
    publishes it. **CC6.3** — role-based restriction: only the placeholder
    `comms` group may broadcast, so publication authority is scoped to a role.
  - **FINRA Rule 2210(b)(1)** — principal pre-approval of retail communications
    (Partial in the coverage matrix). By blocking agent-initiated org-wide blogs
    and public-space writes, and forcing agent creates to draft, the agent cannot
    unilaterally push content to a broad or external audience — a human in the
    comms group reviews and publishes, which is the pre-approval gate the rule
    contemplates on the MCP path.
  - **EU AI Act Art. 50(4)** — disclosure / human-review marker for
    AI-generated-or-manipulated published text (Partial; PF-27 supplies the
    human-review marker). Forcing agent-authored creates to `draft` inserts a
    human review point before AI-produced text is published, and denying
    instant org-wide / public publication keeps un-reviewed AI text off broadly
    disseminated channels.
  - **GDPR Art. 5(1)(f) / Art. 32(1)(b), 32(2)** — integrity & confidentiality
    / security of processing: denying agent-initiated org-wide blogs and
    public / anonymous-access-space writes, and forcing agent creates to a
    private draft, is a technical measure against the accidental or unlawful
    disclosure of personal data that may sit in a page body to a broad or
    external audience over the MCP path. **CCPA/CPRA §1798.121** — supports
    limiting disclosure of sensitive personal information by keeping
    agent-authored content off public / org-wide channels until a human
    publishes it.

  **Why no `hipaa` / `pci-dss` / `sox` bundle tag.** This policy governs
  publication *scope* (blog vs page, public vs internal space, draft vs
  current), not content — it does not process PHI, cardholder, or
  financial-record data — so those three framework bundles do not apply. It is
  tagged `soc2` because denying org-wide / public broadcast is a genuine SOC 2
  boundary / information-movement control (CC6.6 / CC6.7), and `gdpr-ccpa`
  because that same broadcast denial is an Art. 5(1)(f) / Art. 32 measure
  against unauthorised disclosure of personal data (both cited above). The
  coverage matrix additionally maps PF-27 to FINRA 2210(b)(1) and EU AI Act
  50(4), tracked via the `finserv-comms` / `eu-ai-act` tags.

  ## Tool name matching

  The gateway prefixes tool names with the configured MCP server name (e.g.
  `atlassian-createconfluencepage` or `mcp-atlassian-confluence_create_page`),
  and that prefix is not standardized. The policy matches on the lowercased
  tool-name **suffix** so it stays portable across server-name conventions:

  - creates: `*createconfluencepage`, `*confluence_create_page`
  - updates: `*updateconfluencepage`, `*confluence_update_page`

  The official Rovo names (`createConfluencePage` / `updateConfluencePage`) are
  verified in the app landscape note; the community sooperset names
  (`confluence_create_page` / `confluence_update_page`) are verified as tool
  names, but their per-field argument schemas are **not** independently verified
  (see Known limitations). Confirm the exact name your gateway sends with the
  dump-input debug technique before relying on this in production. If your server
  exposes a differently-named publish tool, add its suffix to
  `create_tool_suffixes` / `update_tool_suffixes` in `policy.md`.

  ## Argument shape

  Read via `object.get`, so a missing key never crashes the rule:

  - `contentType` (official) with a `content_type` fallback (community
    snake_case) — string; a value of `"blog"` (case-insensitive, surrounding
    whitespace stripped) triggers the org-wide-broadcast deny. `contentType` is
    verified on the official connector; `content_type` is the community
    naming-convention fallback (its schema is unverified — see Known
    limitations).
  - `spaceId` (official) with a `space_id` fallback (community) — string; matched
    against the `public_space_ids` set.
  - `status` / `isPrivate` — set by the create transform. `status` defaults to
    `"current"` on the official server (instant publish); the transform forces
    `"draft"`. `isPrivate` is a create-only flag on the official server.

  ## Identity / exemption

  The `comms` exemption reads the caller's IdP-issued `groups` claim via
  `object.get(object.get(input.subject, "claims", {}), "groups", [])`. It fails
  closed: a caller with no `subject`, no `claims`, or no `comms` group is **not**
  exempt, so the blog/public-space write is denied and the create transform
  applies.

  ## Examples

  ### Denied (agent tries to publish an org-wide blog)

  ```jsonc
  {
    "input": {
      "action": "tool_pre_invoke",
      "resource": { "name": "atlassian-createconfluencepage", "type": "tool" },
      "subject": { "sub": "google-apps|agent@acme.com", "claims": { "groups": ["engineering"] } },
      "payload": {
        "name": "atlassian-createconfluencepage",
        "args": { "spaceId": "TEAM123", "title": "Q3 launch", "contentType": "blog", "body": "..." }
      }
    }
  }
  ```

  `allow = false`, `reason = "This Confluence write publishes a blog post, ..."`.

  ### Denied (write into a public / anonymous-access space)

  ```jsonc
  {
    "input": {
      "action": "tool_pre_invoke",
      "resource": { "name": "atlassian-updateconfluencepage", "type": "tool" },
      "subject": { "sub": "google-apps|agent@acme.com", "claims": { "groups": ["engineering"] } },
      "payload": {
        "name": "atlassian-updateconfluencepage",
        "args": { "spaceId": "PUBLIC-SPACE-ID", "pageId": "123", "title": "Notice", "body": "..." }
      }
    }
  }
  ```

  `allow = false`, `reason = "This Confluence write targets a public or anonymous-access space, ..."`.

  ### Allowed + transformed (agent creates an ordinary page)

  ```jsonc
  {
    "input": {
      "action": "tool_pre_invoke",
      "resource": { "name": "atlassian-createconfluencepage", "type": "tool" },
      "subject": { "sub": "google-apps|agent@acme.com", "claims": { "groups": ["engineering"] } },
      "payload": {
        "name": "atlassian-createconfluencepage",
        "args": { "spaceId": "TEAM123", "title": "Runbook", "contentType": "page", "body": "..." }
      }
    }
  }
  ```

  `allow = true`; the call is rewritten so `args.status = "draft"` and
  `args.isPrivate = true`. A human publishes the draft.

  ### Allowed (comms-group member publishes a blog)

  ```jsonc
  {
    "input": {
      "action": "tool_pre_invoke",
      "resource": { "name": "atlassian-createconfluencepage", "type": "tool" },
      "subject": { "sub": "google-apps|comms-lead@acme.com", "claims": { "groups": ["comms"] } },
      "payload": {
        "name": "atlassian-createconfluencepage",
        "args": { "spaceId": "NEWS", "title": "All-hands recap", "contentType": "blog", "body": "..." }
      }
    }
  }
  ```

  `allow = true`, no reason, no transform (comms keeps full control).

  ## Composition

  Single-purpose by design. Useful companions in the
  [`atlassian`](../../../bundles/atlassian/README.md) bundle:

  - [`confluence/freeze-page-deletion`](../freeze-page-deletion/policy.md) —
    freezes the irreversible Confluence deletion tools.
  - [`confluence/block-secrets`](../block-secrets/policy.md) — keeps credentials
    out of page bodies.
  - A companion Jira policy denying `*transitionjiraissue` calls that carry
    `historyMetadata` (change-history actor spoofing) — the other half of the
    PF-27 publication/audit-integrity story on the Atlassian suite.

  ## Known limitations

  - **Group names are placeholders — replace `comms` with your IdP's group name
    at import time.** The exemption is only as trustworthy as the `groups` claim
    your IdP issues; if callers can self-assert group membership, remap it to a
    claim your IdP controls. `is_admin`, `teams`, and the nested `user` claim are
    stripped before policies see them and must not be used here.
  - **Public-space list is a placeholder.** `public_space_ids`
    (`PUBLIC-SPACE-ID`, `ANONYMOUS-SPACE-ID`) must be remapped to your tenant's
    actual public / anonymous-access space identifiers at import time. A space
    not on the list is treated as internal; the policy has no way to discover a
    space's anonymous-access setting from the request alone.
  - **Community argument schema unverified.** The community
    `confluence_create_page` / `confluence_update_page` tool names are verified,
    but their per-field shapes are not independently verified. To defend the
    community surface the policy reads both spellings of the two fields that
    gate a deny: the blog check reads `contentType` **and** the community
    snake_case `content_type`, and the space check reads `spaceId` **and**
    `space_id`. If the community server names one of these something else again
    (or does not expose a blog content type at all), that specific check reads
    its default and fails open for that field — the tool still matches, but a
    blog may not be recognized as such. The injected `status` / `isPrivate`
    transform keys are camelCase only and may be ignored (or need to be
    `is_private`) on the community server; the transform is a best-effort nudge,
    not a deny, so a silently-ignored key does not widen the hard-denied blog /
    public-space surface. Verify the community schema before relying on it there.
  - **Suffix match only.** A future tool whose name ends differently (e.g.
    `createconfluenceblogpost`) is not covered — add its suffix. The policy does
    not fire on names where the verb is embedded mid-string.
  - **Draft-forcing is a create-time nudge, not an enforced human gate (for
    internal pages).** The transform forces agent *creates* to `draft` +
    `isPrivate`, but ordinary updates to internal (non-blog, non-public) pages
    pass through untouched. So a non-comms agent can create a page as a forced
    draft and then, in a follow-up `*updateconfluencepage` call, set
    `status: "current"` to publish it itself — no human in the loop for
    internal-space content. This is deliberate (blocking status flips on updates
    would break the legitimate "human already drafted, agent edits" flow), and it
    does **not** widen the org-wide *blog* surface: blog creates *and updates* are
    hard-denied regardless of the create-then-update sequence (because
    `contentType` travels in both requests), and creates into a public space are
    hard-denied. Updates to a page that *already resides* in a public space are a
    separate, documented gap — see the public-space-on-update limitation below. If
    you need a true human gate on internal publication too, pair this with a
    `require-human-approval`-style update policy.
  - **Public-space enforcement is reliable on creates, best-effort on updates.**
    The official `updateConfluencePage` / community `confluence_update_page`
    identify the target page by `pageId` / `page_id`; the page's space is **not**
    part of an update request (only creates carry `spaceId` — `isPrivate` is
    likewise create-only). So a non-comms agent editing a page that *already*
    lives in a public / anonymous-access space sends no `spaceId`,
    `is_public_space` reads its empty default, and the update passes through
    (un-transformed, since updates are never draft-forced). Creates are
    unaffected: `spaceId` is required on create, so a create *into* a public space
    is hard-denied. Blog edits are also still caught on update, because
    `contentType` travels in the request — only the space dimension is missing on
    updates. If you must stop edits to already-public pages over MCP, pair this
    with a page-ID allow/deny-list policy or otherwise freeze updates to public
    spaces. (The two "public-space update" examples above deny only because the
    caller happens to pass `spaceId`; a realistic pageId-only update would not.)
  - **Sibling community write tools are not publication-scope-checked.** On the
    community server, `confluence_move_page` (relocates an existing page —
    potentially *into* a public / anonymous-access space) and
    `confluence_update_page_section` are not matched by this policy, so a
    non-comms agent could expose a page publicly by moving it rather than by
    creating/updating it. `move_page`'s destination-space argument key is not
    verified in the landscape note, so a reliable public-space check cannot be
    built from the request alone; treat move/section as out of scope here and, on
    community deployments, freeze or group-gate them with a companion policy.
  - **Public-space list is matched exactly and by type.** `public_space_ids`
    membership is an exact string comparison: a `spaceId` sent by the tool as a
    JSON number will not equal a string-configured ID (and vice-versa). Configure
    the list with values that match the exact type and format your server emits
    on the wire (confirm with the dump-input debug technique).
  - **Body/link content not inspected.** This policy governs *publication scope*
    (blog vs page, public vs internal space, draft vs current), not what the body
    contains. Pair it with `block-secrets` and an egress PII policy for content
    control.
  - **Other paths are out of reach.** This covers only the MCP channel. A user
    publishing a blog or public page via the Confluence web UI or REST API is
    outside the gateway's scope by design.

  > **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.
direction: ingress
apps:
  - confluence
industries: []
bundles:
  - atlassian
  - soc2
  - gdpr-ccpa
experimental: true
schemaVersion: 1.0.0
minimumGatewayVersion: 1.0.0b24
---

```rego
package confluence.ingress.deny_public_publication

# Deny-by-default: only the explicit allow rules below permit a request. Every
# tool that is not a Confluence page create/update passes through; create/update
# calls are denied when they would broadcast org-wide (a blog) or publish into a
# public / anonymous-access space, unless the caller is in the comms group.
default allow := false

# -----------------------------------------------------------------------------
# TOOL MATCHING. The gateway prefixes tool names with the configured MCP server
# name, which is not standardized, so we match on the lowercased suffix to stay
# portable. Official Rovo names (createConfluencePage / updateConfluencePage) and
# community sooperset names (confluence_create_page / confluence_update_page) are
# both covered. Verify the exact name your gateway sends with the dump-input
# debug technique before relying on this in production.
# -----------------------------------------------------------------------------
create_tool_suffixes := {
    "createconfluencepage",
    "confluence_create_page",
}

update_tool_suffixes := {
    "updateconfluencepage",
    "confluence_update_page",
}

tool_name := lower(input.resource.name)

is_create_tool if {
    some suffix in create_tool_suffixes
    endswith(tool_name, suffix)
}

is_update_tool if {
    some suffix in update_tool_suffixes
    endswith(tool_name, suffix)
}

is_publish_tool if {
    is_create_tool
}

is_publish_tool if {
    is_update_tool
}

# -----------------------------------------------------------------------------
# PUBLIC / ANONYMOUS-ACCESS SPACES. Placeholder spaceIds — remap to your tenant's
# public / anonymous-access space identifiers at import time. A write into any of
# these publishes externally-visible content.
# -----------------------------------------------------------------------------
public_space_ids := {
    "PUBLIC-SPACE-ID",
    "ANONYMOUS-SPACE-ID",
}

# -----------------------------------------------------------------------------
# COMMS EXEMPTION. Members of this IdP group may publish blogs and to public
# spaces, and are not subject to the draft-forcing transform. Placeholder — remap
# `comms` to your IdP's group name at import time. Fail closed: a missing
# subject / claims / groups yields no exemption.
# -----------------------------------------------------------------------------
comms_group := "comms"

# Tool arguments, null-safe: missing payload or args yields {}.
args := object.get(object.get(input, "payload", {}), "args", {})

caller_in_comms if {
    subject := object.get(input, "subject", {})
    groups := object.get(object.get(subject, "claims", {}), "groups", [])
    some g in groups
    lower(g) == comms_group
}

# A blog broadcasts to the whole organization (contentType: "blog"). Read the
# official camelCase `contentType` key, falling back to the community snake_case
# `content_type` key, so a blog posted through the community server is caught too
# (its other args — space_id, page_id — are snake_case, so contentType would be
# as well). Compared case-insensitively and with surrounding whitespace stripped,
# so a padded value like " blog\n" cannot slip past the check if the server would
# still coerce it.
is_blog if {
    ct := object.get(args, "contentType", object.get(args, "content_type", ""))
    trim_space(lower(ct)) == "blog"
}

# The write targets a public / anonymous-access space. Read the official
# `spaceId` key, falling back to the community `space_id` key.
is_public_space if {
    sid := object.get(args, "spaceId", object.get(args, "space_id", ""))
    public_space_ids[sid]
}

# -----------------------------------------------------------------------------
# ALLOW: everything that isn't a publish tool, plus publish calls that are
# neither a blog nor a public-space write (or are made by a comms-group caller).
# -----------------------------------------------------------------------------
allow if {
    not is_publish_tool
}

allow if {
    is_publish_tool
    not is_blocked
}

# A publish is blocked when a non-comms caller broadcasts a blog...
is_blocked if {
    is_publish_tool
    not caller_in_comms
    is_blog
}

# ...or writes into a public / anonymous-access space.
is_blocked if {
    is_publish_tool
    not caller_in_comms
    is_public_space
}

reasons contains "This Confluence write publishes a blog post, which broadcasts to your whole organization. Agent-initiated blog posts are blocked. Post it as a regular page in a team space instead, or ask a member of the comms team to publish it. Contact your admin if you believe this is a false positive." if {
    is_publish_tool
    not caller_in_comms
    is_blog
}

reasons contains "This Confluence write targets a public or anonymous-access space, which would publish externally visible content. Agent-initiated writes to public spaces are blocked. Move the content to an internal space, or ask a member of the comms team to publish it. Contact your admin if this space should not be treated as public." if {
    is_publish_tool
    not caller_in_comms
    is_public_space
}

reason := joined if {
    count(reasons) > 0
    reason_list := sort([r | some r in reasons])
    joined := concat("; ", reason_list)
}

# -----------------------------------------------------------------------------
# TRANSFORM: on page CREATES by non-comms callers, force the page to draft and
# private so a human publishes it deliberately (instead of status:"current"
# going live immediately). Applies only to allowed creates — the gateway ignores
# the transform on a denied request. Comms-group callers keep full control, and
# updates are never rewritten.
# -----------------------------------------------------------------------------
transform := {"transformed_payload": merged} if {
    is_create_tool
    not caller_in_comms
    merged := object.union(args, {"status": "draft", "isPrivate": true})
}
```
