Eight Confluence actions had _scopes that didn't match what the Atlassian Confluence API actually accepts. Each fix applies the corrected scope set from QA's "Doc remarks" column in [QA TESTING] Action To Scope Mapping.xlsx → DONEConfluence, cross-checked independently against the Confluence v1 (swagger.v3.json) and v2 (openapi-v2.json) OpenAPI specs.
| Action | Endpoint | Bug | Fix |
|---|---|---|---|
cql_search | GET /wiki/rest/api/search | Required both classic search:confluence AND granular read:content-details:confluence (separate any_of clauses) when docs say either alone is sufficient | Collapse to one any_of listing both as alternatives |
delete_space | DELETE /wiki/rest/api/space/{spaceKey} | Had only delete:space:confluence — insufficient. Docs require classic write:confluence-space OR granular pair (read:content.metadata:confluence AND delete:space:confluence) | Distribute the DNF "(A) ∨ (B∧C)" into Mercury's CNF: two clauses, each pairing the classic with one granular |
get_audit_logs | GET /wiki/rest/api/audit | Required both read:audit-log:confluence AND write:audit-log:confluence | Drop the write clause; endpoint only needs read |
get_content_properties_for_page (v2) | GET /pages/{page-id}/properties | Required read:confluence-props AND write:confluence-props — neither is a real scope for this v2 endpoint | Replace with the documented read:page:confluence |
get_content_restrictions | GET /wiki/rest/api/content/{id}/restriction | Listed three scopes including a non-existent read:content.restriction:confluence | Drop the extra; keep the documented classic + granular pair |
search_content | GET /spaces and GET /pages (v2) | Used read:page:confluence only, but action calls /spaces too. Spreadsheet listed the v1 endpoint /wiki/rest/api/content/search but the code was migrated to v2 — initial fix in this PR followed the sheet's wrong endpoint and was corrected in a follow-up commit | Multi-clause: read:space:confluence (for /spaces) and read:page:confluence (for /pages) |
search_users | GET /wiki/rest/api/search/user | Used read:confluence-user | Replace with read:content-details:confluence |
update_space_property (v2) | PUT /spaces/{space-id}/properties/{property-id} | Required read:space.property:confluence AND write:space.property:confluence (neither is a real scope) | Replace with read:space:confluence OR write:space:confluence |
security block from Atlassian's OpenAPI spec. The classic scope listed there matches what the QA Doc Remarks said, in every case.MERCURY_AUTOLOAD=true. Mercury's validate_scopes accepts every new _scopes block.ruff format + ruff check pass on all files.tests/test_attrs/test_action_scopes.py all 5 tests pass.Beyond the 8 sheet-flagged mismatches, an automated cross-endpoint sweep across all 62 confluence actions surfaced 7 additional likely bugs the spreadsheet missed. These are NOT touched in this PR (deferred to QA's review), but documenting them here so they can be added to a future audit pass.
7 actions have _scopes requiring users to hold both read:X and write:X (declared as two separate any_of clauses combined with all_of). The OpenAPI v2 spec for each of these endpoints lists both scopes inside a single oAuthDefinitions array, which per OpenAPI semantics means either one alone authorizes the call. So the natural choice for a CREATE / DELETE op (granting only write:X) currently FAILS Mercury's gate even though Atlassian would happily accept the call.
Example side-by-side for create_space_property.py:
// Atlassian v2 spec — POST /spaces/{space-id}/properties
{
"security": [
{"basicAuth": []},
{"oAuthDefinitions": ["read:space:confluence", "write:space:confluence"]}
]
}
// → either read:space OR write:space alone authorizes the call
// Mercury current
{
"all_of": [
{"any_of": ["read:space:confluence"]},
{"any_of": ["write:space:confluence"]}
]
}
// → user must hold BOTH; user with only write:space is rejected at gate
The full list of 7:
| Action | Endpoint | Spec accepts |
|---|---|---|
create_blogpost_property.py | POST /blogposts/{blogpost-id}/properties | read:page:confluence OR write:page:confluence |
create_content_property_for_whiteboard.py | POST /whiteboards/{id}/properties | read:whiteboard:confluence OR write:whiteboard:confluence |
create_space_property.py | POST /spaces/{space-id}/properties | read:space:confluence OR write:space:confluence |
delete_blogpost_property.py | DELETE /blogposts/{blogpost-id}/properties/{property-id} | read:page:confluence OR write:page:confluence |
delete_content_property_for_page_by_id.py | DELETE /pages/{page-id}/properties/{property-id} | read:page:confluence OR write:page:confluence |
delete_content_property_for_whiteboard_by_id.py | DELETE /whiteboards/{whiteboard-id}/properties/{property-id} | read:whiteboard:confluence OR write:whiteboard:confluence |
delete_space_property.py | DELETE /spaces/{space-id}/properties/{property-id} | read:space:confluence OR write:space:confluence |
In each case, the proposed fix is to collapse the two any_of clauses into one, listing both scopes as alternatives — the same shape this PR applied for update_space_property.py. Same one-line diff per file.
QA's per-row verdict logic looks at one endpoint at a time and compares Mercury's _scopes against the row's spec_scopes column. For these 7, Mercury's flat scope list ({read:X, write:X}) does overlap with the spec's accepted set (also {read:X, write:X}), so per-endpoint comparison reports "match." The bug is in Mercury's clause structure, which the per-row verdict can't see. The cross-endpoint sweep catches it by inspecting the structure: multi-clause all_of requires every clause, but the spec only requires any one scope.
Recommendation: for any sheet pass going forward, also flag rows where Mercury has multiple all_of clauses and the row's endpoint accepts a single any_of-style scope set.
delete_space end-to-end: a user holding only delete:space:confluence (without the granular read:content.metadata:confluence companion) should now be blocked by Mercury's gate. Previously they passed Mercury and 403'd at runtime.get_content_properties_for_page works after the fix — the previous scope set (read:confluence-props / write:confluence-props) wasn't authorizing anything real, so any users who got it working before were probably granting unrelated broader scopes.🤖 Generated with Claude Code