There’s a great deal of information on the internet about managing Confluence Space permissions with scripts, and how there’s no REST endpoint for it, and how it’s basically impossible.

This is incorrect.

There’s also a lot of information about using the JSONRPC or XMLRPC APIs to accomplish this.   These APIs are only available on Server/DC. In the Cloud they effectively don’t exist, so this is yet more misinformation.

So why all the confusion?

There’s a lot of outdated information out there that floats around and doesn’t disappear even after it stops being correct or relevant. This is one of the major struggles I had when I started learning how to write scripts to interact with Jira and Confluence.    Much of the information used to be relevant, but five or six or ten years later it only serves to distract people looking for a solution. That’s one of the major reasons I started this blog in the first place.

Specific to this instance, another reason for confusion is that the documentation for the REST API does outline an endpoint for Confluence Space permission management, but it includes some very strict limitations that could easily be misinterpreted.

The limitation is this: the

This script examines the membership of every group in Confluence.  It returns information in three groups:

  • The Confluence user directories
  • Information about each group in Confluence
  • Information about each user in Confluence

The output looks like the blob below.  As noted, the information is divided into three sections. Formatting is provided by the script:


Directory Info

Directory name: Confluence Internal Directory

Group Info

confluence-administrators has 1 members. 1 of those members are active and 0 of those members are inactive
confluence-users has 2 members. 1 of those members are active and 1 of those members are inactive

User Info

Group: confluence-administrators. Username: admin. User account is active. User is in directory: Confluence Internal Directory
Group: confluence-users. Username: admin. User account is active. User is in directory: Confluence Internal Directory
Group: confluence-users. Username: slaurin. User account is inactive. User is in directory: Confluence Internal Directory



And here is the code:

 import com.atlassian.user.GroupManager
import com.atlassian.confluence.user.DisabledUserManager

def disUse = ComponentLocator.getComponent(DisabledUserManager)

def userAccessor = ComponentLocator.getComponent(UserAccessor)
def groupManager = ComponentLocator.getComponent(GroupManager)

def directoryManager = ComponentLocator.getComponent(DirectoryManager)

def activeUsers = 0
def inactiveUsers = 0

def groups = groupManager.getGroups()
def groupInfoStringBuffer = ["<h1>Group Info</h1>"]
def userInfoStringBuffer = 

The Jira Cloud Migration Assistant tool (JCMA) will only migrate some types of custom fields.   The custom fields that it cannot migrate must be recreated on the Cloud side, or otherwise mitigated in some way.

I wrote a small tool that proactively identifies any custom field in a Jira instance that JCMA will not be able to migrate.

It’s important to be proactive, especially when it comes to migrations. Every unexpected error or issue results in lost time, and a delayed migration.

Here’s the code:

 import com.atlassian.jira.component.ComponentAccessor
def migrateableFieldTypes = [
def sb = []
def customFields = ComponentAccessor.getCustomFieldManager().getCustomFieldObjects()
//Get all of the custom fields in the system
customFields.each{ customField -> 
//For each custom field
//If the custom field type does not match one of the items in the array of migrateable field types
sb.add(customField.getFieldName() + " | " +":")[1] + "<br>")
//Note that custom field
return sb.toString().replace(",", "") 

When migrating data from Jira Server/DC to Jira Cloud, JCMA does not like tickets that have no assignee, or which have tickets with an assignee that has an inactive user status.

This script checks a list of issues, and replaces any that have a missing or inactive assignee.

The script comments should pretty well explain what’s happening with the script. By default it’s set up to check all of the issues in a given project. However, by commenting out one line and uncommenting another, a specific list of issues across any number of projects can be fed to the script.

 import com.atlassian.jira.component.ComponentAccessor
def issueManager = ComponentAccessor.getIssueManager()
def issueService = ComponentAccessor.getIssueService()
def userManager = ComponentAccessor.getUserManager()
def projectManager = ComponentAccessor.getProjectManager()

final replacementUser = "<username>"
//Define the username that will be used to replace the current assignee

final projName = "<project name>"
//Define the project to be checked

def project = projectManager.getProjectObjByName(projName).id
//Declare a project ID object, using the name of the project

def issues = ComponentAccessor.issueManager.getIssueIdsForProject(project)
//Get all the issues in the project

//final issues = ["<issues>"]
//Uncomment this line and comment out the previous one to feed the script a specific list of issues

issues.each {
  targetIssue ->
    //For each issue 


Please note: this solution was originally posted by Peter-Dave Sheehan on the Atlassian Forums. I’m just explaining how I use it.


Sometimes when I’m trying to solve a problem with Jira, the internal Java libraries just aren’t sufficient. They’re often not documented, or they’re opaque. 

It’s often far easier to turn to the REST API to get work done, but that’s a little more tricky on Jira DC or Server than it is on Cloud.  On Jira Cloud, a REST call could be as simple as:

 def result = get("/rest/api/2/issue/<issue key>")
.header('Content-Type', 'application/json')

       return field


However this won’t work on Server/DC.  Instead we need a REST framework upon which to build our script.

The Framework

This piece of code uses the currently logged in user to authenticate against the Jira REST API.  It then makes a GET call to the designated API endpoint URL.

This code can easily be changed to a POST or a PUT simply by uncommenting the payload statement and the setRequestBody statement, then changing the MethodType from GET to POST/PUT

The script returns a JSON blob. With point notation, we can then easily access its individual attributes, and

The Problem

One of the challenges that I encountered this week was the need to include Advanced Roadmaps plans in a Jira DC to Cloud migration.  As you may be aware, JCMA gives you the option to either migrate ALL plans, or none of them.  There is no facility for selectively adding plans.  This is a problem because the client instance has 1200 Roadmaps plans, and trying to add that many plans to a migration causes JCMA to crash.

I set out this week to build the foundations of what I’m calling the Roadmaps Insight Tool.  The first version was intended to simply list every Roadmaps plan in an instance, and list each of its data sources (project, board, or filter). 

The resulting dataset is useful in a number of ways. First, it gives transparency to a part of Jira that is otherwise quite opaque.

Second, it indicates which data sources on each plan are invalid; typically this is because the referenced data source no longer exists.  A Jira administrator wanting to do a cleanup of the Roadmaps Plans could easily base that cleanup on this information.

Third, in the case of this particular client it allows us to

You can easily remove all permissions from a Confluence DC Space, or a Confluence Cloud Space.  Confluence Server, though? You’re out of luck.

Imagine you migrated from Confluence Cloud to Confluence Server, and you wanted to remove all permissions on a Space (except  for maybe “View Space”).  That’s a whole lot of manually clicking, unless you script it.  You’re going to need ScriptRunner for this.

The script below takes two inputs: a Space key, and a username.  It needs the username of someone on the Space with Admin access, because Confluence will not let you remove EVERYONE  with admin access from the Space.

Someone gets left behind.


Okay so it takes those two pieces of information as variables.  It then makes use of two arrays. The first array is a prescribed selection of the permissions you’d like removed from the Space. Want to let everyone keep the View Space permission type? Take it out of the List!
The second array is generated by the script. It’s a list of every username and group name with some kind of permission on the Space.

We then nest two loops, and iterate through the permission types and usernames.  For each permission type,


It’s possible to connect to a Jira instance using Python, and it’s possible to connect to AWS Comprehend using Python. Therefor, it is possible to marry the two, and use Python to assess the sentiment of Jira issues. There are two caveats when it comes to using this script:

  1. The script assumes you can authenticate against Jira with Basic Web Authentication. If your organization uses Single Sign On, this script would need to be amended. 
  2. The script assumes you’re working with Jira Server or Datacenter.  If you’re using Jira Cloud the approach would be different, but I’m planning to do a post about that in the near future.

The authentication method below is not mine. I have linked to the Stack Overflow page where I found it, in the script comments.


The Script

The script starts with three imports. We need the Jira library, logging, and the AWS library (boto3).  You’ll likely need to a PIP install of Jira and boto3, if you’ve not used them before.

After the imports we’re defining client, which we use to interact with the AWS API.  Remember to change your region to whichever region is appropriate for you, in addition

As part of my grad school course work, I had half a dozen XML files with content that needed to be analyzed for sentiment.   AWS Comprehend is a service that analyzes text in a number of ways, and one of those is sentiment analysis.

My options were to either cut and paste the content of 400 comments from these XML files, or come up with a programmatic solution.  Naturally, I chose the latter.

The XML file is formatted like so:


          <post id="123456">
            <userid>user id</userid>
            <subject>Post title</subject>
            <message> Message content </message> 


What I needed to get at was the message element of each post, as well as the post id.

The script imports BeautifulSoup to work with the XML, and boto3, to work with AWS.    We next define a string buffer, because we need to store the results of the analysis somehow.

Next we define the client, which tells AWS everything it needs to know.  Tell it the service you’re after, the AWS region, and the tokens you’d use to authenticate against AWS.

After that we provide a list of XML files that the script needs to parse, and tell it to

I found myself needing to examine issues that came through our Jira Service Desk workflow, to determine if they had a form attached. If they didn’t have a form attached, i.e. someone created the issue manually instead of through the Service Desk Portal, the workflow would be instructed to handle them in a certain way.

This turned out to be surprisingly difficult.  There’s no easily accessed attribute associated with issues in Jira that indicates whether or not they have a form attached.

In the end, I determined that it was possible to examine issues in this way by applying some JQL to them.

 import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.jql.parser.JqlQueryParser
import com.atlassian.jira.web.bean.PagerFilter

def jqlQueryParser = ComponentAccessor.getComponent(JqlQueryParser)
def issueManager = ComponentAccessor.getIssueManager()
def searchService = ComponentLocator.getComponent(SearchService)
def user = ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()

def issue = issueManager.getIssueObject("<issue key>")
//Define an issue against which the script will be run

def queryParser = ComponentAccessor.getComponent(JqlQueryParser)
def query = queryParser.parseQuery('issueFormsVersion > 1 and key = '+ issue.key)
//Define the query parameters

def search =, query, PagerFilter.getUnlimitedFilter())
//This gives us a list of issues that match the query

if(search.results.size() > 0){
    //Do something here
    //If any results were found, it means that the issue had a form attached