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:
deny-write-sensitive-projects— even for authorized writers, keeps designated projects (HR, LEGAL, SEC) unwritable.deny-view-search-sensitive-projects— read/search fence for the same projects (this policy leaves reads open).redact-sensitive-info— egress redaction of PII/secrets in returned issue content.
Known limitations
- Group names are placeholders — replace
jira-writerswith your IdP's group name at import time. The policy expectsgroupsto 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/groupsorroles), updatecaller_groupsinpolicy.md. - Reads are open to everyone.
searchJiraIssuesUsingJqlandgetJiraIssue(especially withfields:["*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_jiragroup, 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 communitysooperset/mcp-atlassianserver 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-issuecreateissuelink),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 towrite_suffixes. Thejira_delete_issue,jira_create_remote_issue_link, andjira_update_proforma_form_answersresiduals are each covered by a documenting test case. - Official non-
write_jirawrite groups are not gated. The official Rovo server also exposes write tools in permission groups outsidewrite_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 extendwrite_suffixes. Covered by a documenting test case (updatejsmopsalertpasses). - Confluence is out of scope. This is a Jira-only policy. Confluence writes
(
createConfluencePage,updateConfluencePage, comments) are handled by the separateconfluenceapp policies. groups-claim spoofing is out of the gateway's hands. The policy trusts the IdP-assertedgroupsclaim; 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)
Related policies
Airtable: Redact PII in Record Reads
Scans the responses of the Airtable record-read tools — the calls that return row fields values — and rewrites high-confidence PII shapes to a fixed…
Asana: Redact PII in Task & Comment Reads
On the Asana MCP read path, this transform scans the free-text business fields that ride back in task, comment/story, and status-update responses — notes,…
BigQuery: Redact PII in Query Results
Scans the content returned by BigQuery's result-returning tools and rewrites high-confidence PII shapes to fixed, non-recoverable redaction tokens before the…
Block Agent Email to External Recipients
Blocks agent-initiated Microsoft 365 email sends when any recipient address falls outside a corporate-domain allowlist.
Block BigQuery Exfiltration and Cross-Project Writes
Inspects the raw GoogleSQL string carried by BigQuery SQL tools and denies any statement that moves data out of the tenant's own project — even when the call…
bigqueryguard-warehouse-exportingresssqlexfiltrationsoc2pci-dssgdpr-ccpa
Block Bulk Export & External Staging (Snowflake)
Blocks Snowflake SQL-execution tool calls whose query text moves whole tables off the Snowflake perimeter — bulk export to cloud storage or a stage, and…
snowflakeguard-warehouse-sqlexportexfiltrationingresssoc2pci-dssgdpr-ccpa