dtwo Policy Store

JIRA: Role-Gated Writes (Read-Only Default)

Makes Jira read-only by default on the MCP path.

Direction
ingress
Rego package
jira.ingress.role_gate_writes
App
jira
Bundles
atlassiansoc2gdpr-ccpa
Published
Minimum gateway
1.0.0b24
Schema version
1.0.0
Checksum
sha256:986e5a061ed2a667d4950e82b24909c491ea57219ebde0aa32696c54acde1ebc

jiraatlassianrole-gate-writesaccess-controlleast-privilegeingresssoc2gdpr-ccpa

What this policy does

Direction: ingress (tool_pre_invoke) Default: deny writes unless the caller is in the writer group; allow reads for everyone Package: jira.ingress.role_gate_writes

What it does

Makes Jira read-only by default on the MCP path. Every Jira write tool — creating an issue, editing an issue, transitioning an issue, commenting on an issue, logging work against an issue, and creating an issue link — is denied unless the caller's IdP groups claim contains the placeholder group jira-writers. Read and search tools (getJiraIssue, searchJiraIssuesUsingJql, and the project/issue-type metadata lookups) pass for everyone.

This is the per-app least-privilege baseline (family PF-12). It complements the project-scoped deny-write-sensitive-projects policy: where that one keeps specific projects unwritable by anyone, this one makes mutation the exception org-wide rather than the default. A prompt-injected agent operating as an ordinary read-only user cannot create, edit, transition, comment on, log work against, or link any issue — in any project — because it is not in the writer group. (Community servers ship additional write tools outside these six operation classes; those are intentionally out of this baseline's scope — see Known limitations.)

The check runs at ingress, before the call reaches the Jira MCP server, so a denied write never executes and has no side effects.

Compliance alignment

This policy instantiates least-privilege write-gating (family PF-12) on Jira's write path, and supports alignment with:

  • SOC 2 CC6.1 — supports logical access security over protected assets: Jira issues cannot be mutated over the agent channel without an explicit role grant. CC6.3 — supports role-based access and least privilege: write capability is tied to a live IdP group, so removing the group in the IdP removes write access on the caller's next request.
  • HIPAA §164.308(a)(4) — supports information access management for Jira tenants that track PHI-adjacent work: write authorization is role-scoped. §164.312(a)(1) — supports technical access control with per-call identity taken from the caller's JWT.
  • GDPR Art. 25 — supports data protection by design/default on the agent channel: the default posture is read-only. Art. 29 / Art. 32(4) — supports processing only on the controller's instructions: an unauthorized principal cannot alter personal data held in Jira through the agent.

Tool name matching

The DTwo gateway prefixes tool names with the configured MCP server name (e.g. atlassian-jira-mcp-createjiraissue), and that prefix is not standardized. All matching is therefore case-insensitive and by suffix on lower(input.resource.name), covering both Jira MCP dialects at once:

Operation Official Rovo suffix (camelCase, lowercased) Community suffix (snake_case)
Create issue createjiraissue jira_create_issue
Edit / update issue editjiraissue jira_update_issue
Transition issue transitionjiraissue jira_transition_issue
Comment on issue addcommenttojiraissue jira_add_comment
Log work addworklogtojiraissue jira_add_worklog
Create issue link createissuelink jira_create_issue_link

These six operations are the complete write_jira tool group of the official Atlassian Rovo server (which ships no delete tools at all), plus the community sooperset/mcp-atlassian equivalents of the same operations. Three community-only tools are also gated because they are variants of the same operation classes — leaving any of them open would let a non-writer perform a gated operation under a different name:

  • jira_batch_create_issues — issue creation in bulk;
  • jira_edit_comment — rewrites an existing comment (the comment surface the single-op suffixes gate);
  • jira_link_to_epic — links an issue to an epic (the linking surface the issue-link suffixes gate).

Read and search tools carry none of these suffixes and pass. The tool name is taken from the PARC field input.resource.name, falling back to the legacy alias input.payload.name when the PARC field is missing, null, or not a string — so a degenerate tool hook cannot present a write as a read. Verify the exact names your gateway emits with the dump-input debug technique before relying on this in production, and extend write_suffixes in policy.md if your server exposes a write tool under a different name.

Argument shape

The decision uses only the tool name (input.resource.name, with a legacy input.payload.name fallback) and the caller's identity (input.subject.claims.groups). Tool arguments are not inspected, so the policy cannot be bypassed by unusual argument keys, nesting, or encodings — and it behaves identically whether or not a tool's argument schema is documented.

Identity

Group membership is read fail-closed via object.get(object.get(object.get(input, "subject", {}), "claims", {}), "groups", []): a missing subject, missing claims, a missing groups claim, or a groups claim that is not an array all mean "not a writer", and every write is denied. The is_array guard is load-bearing — some group in caller_groups iterates the values of an object, so a groups claim shaped as {"role": "jira-writers"} would otherwise match and fail open; requiring an array keeps every non-array shape (object, string, number) fail-closed. Reads are unaffected by identity.

Examples

Allowed — read tool, no identity required

{
  "input": {
    "action": "tool_pre_invoke",
    "resource": { "name": "atlassian-getjiraissue", "type": "tool" },
    "payload": { "name": "atlassian-getjiraissue", "args": { "issueIdOrKey": "DEV-7" } }
  }
}

allow = true, no reason.

Allowed — write tool, caller in the writer group

{
  "input": {
    "action": "tool_pre_invoke",
    "resource": { "name": "atlassian-createjiraissue", "type": "tool" },
    "subject": { "sub": "auth0|alice", "claims": { "groups": ["jira-writers"] } },
    "payload": {
      "name": "atlassian-createjiraissue",
      "args": { "projectKey": "DEV", "summary": "New task" }
    }
  }
}

allow = true, no reason.

Denied — write tool, caller not in the writer group

{
  "input": {
    "action": "tool_pre_invoke",
    "resource": { "name": "atlassian-editjiraissue", "type": "tool" },
    "subject": { "sub": "auth0|bob", "claims": { "groups": ["engineering"] } },
    "payload": {
      "name": "atlassian-editjiraissue",
      "args": { "issueIdOrKey": "DEV-7", "fields": { "summary": "changed" } }
    }
  }
}

allow = false, reason = "Jira write tools are restricted to members of the 'jira-writers' group ...".

Composition

This policy is the Jira least-privilege baseline; it gates who may write, not what they may write. Useful companions in the same atlassian bundle:

Known limitations

  • Group names are placeholders — replace jira-writers with your IdP's group name at import time. The policy expects groups to be an array claim in the caller's JWT; if your IdP emits roles under a different or namespaced claim (e.g. https://acme.com/groups or roles), update caller_groups in policy.md.
  • Reads are open to everyone. searchJiraIssuesUsingJql and getJiraIssue (especially with fields:["*all"]) remain a broad egress channel — a read-only agent can still trawl issues across projects the user can see. Pair with a read/search fence (deny-view-search-sensitive-projects) and/or egress redaction if your Jira tenant holds regulated content.
  • Closed write set — community write tools outside the gated operation classes are not gated. The suffix list is the complete official Rovo write_jira group, its community equivalents, and the three community-only variants of the same operation classes (jira_batch_create_issues, jira_edit_comment, jira_link_to_epic). The community sooperset/mcp-atlassian server ships additional write and destructive tools that carry none of the gated suffixes and therefore pass as "reads": e.g. jira_delete_issue, jira_remove_issue_link, jira_add_watcher, jira_create_remote_issue_link (attaches an external URL, a different operation from the gated issue-to-issue createissuelink), jira_update_proforma_form_answers (updates issue-attached ProForma form answers — issue-adjacent data this baseline does not treat as an issue edit), and the sprint/version tools. This is intentional scope for the baseline; gate those with the destructive-op / sensitive-project companions, or add their suffixes to write_suffixes. The jira_delete_issue, jira_create_remote_issue_link, and jira_update_proforma_form_answers residuals are each covered by a documenting test case.
  • Official non-write_jira write groups are not gated. The official Rovo server also exposes write tools in permission groups outside write_jira — JSM Ops (e.g. updateJsmOpsAlert) and Compass (e.g. createCompassComponent); the landscape note lists these groups for completeness and notes JSM/Bitbucket tools run in API-token mode. They are not issue writes and are outside this baseline's scope, but if your tenant exposes them, gate them separately or extend write_suffixes. Covered by a documenting test case (updatejsmopsalert passes).
  • Confluence is out of scope. This is a Jira-only policy. Confluence writes (createConfluencePage, updateConfluencePage, comments) are handled by the separate confluence app policies.
  • groups-claim spoofing is out of the gateway's hands. The policy trusts the IdP-asserted groups claim; if a caller can mint tokens with arbitrary claims, that is an IdP/JWT-validation problem, not a policy one.

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

# Deny-by-default: reads are explicitly allowed below; every write requires
# membership in the writer group.
default allow := false

# Placeholder IdP group permitted to perform Jira writes.
# Replace "jira-writers" with your IdP's group name at import time.
writer_group := "jira-writers"

# Lowercased tool name. The gateway prefixes tool names with the configured MCP
# server name (e.g. `atlassian-jira-mcp-createjiraissue`), so matching below is
# case-insensitive and suffix-based to stay portable across naming conventions.
#
# PARC-first with a legacy fallback (red-team hardening): `resource.name` is the
# unified PARC field, but if a tool hook ever arrived with it missing, null, or
# non-string while the legacy alias `payload.name` still identified the tool,
# the write would otherwise look like a read and fail OPEN. The fallback keeps
# the gate on the legacy alias too. If neither field names the tool, there is no
# tool name to gate on and the non-write branch applies.
resource_name := object.get(object.get(input, "resource", {}), "name", "")

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

has_resource_name if {
    is_string(resource_name)
    resource_name != ""
}

tool_name := lower(resource_name) if {
    has_resource_name
}

tool_name := lower(payload_name) if {
    not has_resource_name
    is_string(payload_name)
}

# --- Write-tool detection ---
# The complete official Atlassian Rovo `write_jira` group (camelCase names,
# lowercased) plus the community sooperset/mcp-atlassian equivalents of the same
# six operations, plus the community-only variants of those same operation
# classes (batch create, comment rewrite, epic linking). Matched by suffix so
# any gateway server-name prefix still matches. endswith is exact at the tail,
# so read tools whose names merely contain "jiraissue" (getJiraIssue,
# getTransitionsForJiraIssue, getJiraIssueRemoteIssueLinks, ...) do not match
# any of these full suffixes.
write_suffixes := [
    # create issue
    "createjiraissue", # official Rovo
    "jira_create_issue", # community
    "jira_batch_create_issues", # community batch-create — same create operation in bulk
    # edit / update issue
    "editjiraissue", # official Rovo
    "jira_update_issue", # community
    # transition issue
    "transitionjiraissue", # official Rovo
    "jira_transition_issue", # community
    # comment on issue
    "addcommenttojiraissue", # official Rovo
    "jira_add_comment", # community
    "jira_edit_comment", # community — rewrites an existing comment; same comment surface
    # log work
    "addworklogtojiraissue", # official Rovo
    "jira_add_worklog", # community
    # create issue link
    "createissuelink", # official Rovo
    "jira_create_issue_link", # community
    "jira_link_to_epic", # community — links an issue to an epic; same linking surface
]

is_write_tool if {
    some suffix in write_suffixes
    endswith(tool_name, suffix)
}

# --- Identity (fail closed) ---
# Missing subject, missing claims, a missing groups claim, or a groups claim that
# is not an array all yield "not a writer" — writes then deny.
caller_groups := object.get(object.get(object.get(input, "subject", {}), "claims", {}), "groups", [])

# The is_array guard is load-bearing: `some group in caller_groups` iterates the
# *values* of an object, so a groups claim shaped as {"role": "jira-writers"}
# would otherwise match and fail OPEN. Requiring an array keeps every non-array
# shape (object, string, number) fail-closed, as the Identity section promises.
caller_is_writer if {
    is_array(caller_groups)
    some group in caller_groups
    group == writer_group
}

# --- Decision ---

# Reads (and anything that is not one of the six gated write tools) pass for
# everyone.
allow if {
    not is_write_tool
}

# Writes pass only for members of the writer group.
allow if {
    is_write_tool
    caller_is_writer
}

reasons contains msg if {
    is_write_tool
    not caller_is_writer
    msg := sprintf("Jira write tools are restricted to members of the '%s' group — this account has read-only Jira access through the gateway. Ask your identity admin to add you to '%s', or hand this write (create, edit, transition, comment, worklog, or issue link) to a teammate with Jira write access. If this tool is actually read-only, contact your InfoSec team to update the policy.", [writer_group, writer_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)