This is an extension of a previous post that I did, on returning all of the users in a Jira Cloud instance.

The request was for a script that returns all users in a Jira Cloud instance who have been inactive for over 30 days. Unfortunately, only part of this request is doable.  The Jira Cloud REST API does not currently provide any way of returning login or access timestamp information.  The most it will tell you is whether a user account is active or inactive. The only way to get access information is to export the Jira userbase through the Atlassian portal.

So the script below simply returns all Jira users in a Cloud instance who have an inactive account.   If/when the day comes that Atlassian adds login information to the API, I’ll update this post.

 import groovy.json.JsonSlurper

def sb = []
def page = 0

def lastPage = false

while (lastPage == false) {
  //run this loop until we detect the last page of results

  def x = 0
  //Every time the loop runs, set x to 0. We use x to count the number of users returned in this batch of results

  def getUsers = get("/rest/api/2/user/search?query=&maxResults=200&startAt=" + 

A long long time ago, I posted a complaint to this blog about how I had no idea what a Jira SearchContext was, and how it was very frustrating to try to figure it out.

Yesterday I realized that I had never really made any strides toward fixing my lack of knowledge around the search functionality of Jira.  I’m not talking JQL, I’m talking the actual Search Service waayyy down in the guts of Jira.

I set out wanting to update the permissions for a list of filters on Jira Server, based on the name of the server.  The list of filter names came from a migration we were doing from Jira Server to Jira Cloud. JCMA returned a list of the names of forty filters that were publicly accessible.  My task was to go in and update those permissions.  As I was going to have to repeat this process multiple times, it seemed an excellent candidate for a scripted solution.

As it turns out, it is extremely difficult to search for filters by name.   You can search for filters by their ID without issue, but searching by name is essentially not possible.

My next idea was to simply wipe

Introduction

So you or your organization have decided to purchase, or at least trial, ScriptRunner. Now what?

If you’re going to learn how to use ScriptRunner, I would very strongly advise that you set it up in a test environment.  This allows you to learn to use the tool, and to not have to worry about making mistakes or deleting production data.

In order to use ScriptRunner, you’ll need to be an admin on the Jira system in question.   As well, I’ll do my best to explain the Groovy code that we’re using, but it would be of great benefit for you to read some primary learning materials on the subject.   Some of what I’ll say will assume a basic knowledge of object-oriented programming principles.   Please note that Groovy is case-sensitive, but is agnostic to whitespace.

All of the testing and learning we’re going to be doing is done in the ScriptRunner Script Console.   You can think of this as sort of a window into your Jira system, in which Groovy code can be run.  The Script Console is accessed by going to your Jira System Setings > Manage Apps > ScriptRunner > Script Console.  ScriptRunner has many

I am entirely self-taught when it comes to ScriptRunner and Groovy.   Everything I’ve learned has been through trial and error, and Googling different permutations of words until I find a solution.

A great deal of the information out there assumes that you already know how to work with ScriptRunner, Groovy, and Jira or Confluence.   I found this to be terrifically frustrating when I first started, as I did not have the requisite knowledge to make use of the information that I was finding.  I didn’t have the skills to put it into context, never mind making use of it in the specific use case to which I was trying to apply it.

For that reason, I’m going back to the beginning.  I’m starting an ongoing series of blog posts about how to get started with ScriptRunner for both Jira and Confluence.  You need to learn to walk before you can run, so for that reason I am calling this series the ScriptWalker series.

Not only will this hopefully be a resource for persons just starting out with ScriptRunner, but it will also force me to be sure that I can teach what I’m doing. In the end, that will make

 

Adaptavist has a tool called Microscope that Jira admins can use to look into the specifics of various aspects of the system, including workflows. If you’re looking to examine an instance, I recommend using Microscope rather than writing your own script.

However, it was requested that I look into workflows in a very specific way: the requester needed a tool that would take the name of a group and search all of the workflows for any references to that group.  In effect, they were searching for dependencies on that group within the workflows.

This script does not do that, but this is the basis upon which that script is built.  This script takes a workflow and returns the validator conditions for all of the transitions.  This could easily be adjusted to return the conditions for the triggers, etc.

 

 import com.atlassian.jira.workflow.WorkflowManager
import com.atlassian.jira.component.ComponentAccessor

def workflowManager = ComponentAccessor.getComponent(WorkflowManager)
def wf = workflowManager.getWorkflow("<workflow name>")
//wf is the workflow itself

def statuses = wf.getLinkedStatusObjects()
//statuses are the statuses available to that workflow

statuses.each {
  status ->
    //For each status associated with the workflow

    def relatedActions = wf.getLinkedStep(status).getActions()
  //relatedActions is the set of actions (transitions) associated with each status
  //log.warn("For the status " 

This was actually an interesting problem to solve.  Atlassian don’t seem to want anyone returning all of the users in an Jira instance through the API.   There’s supposedly a method for doing this, but it doesn’t work if you’re running the script through a Connect app like ScriptRunner.  This is another method that only works with an external script, as was the case with managing Cloud Confluence Space permissions.

Instead what we do is run an empty query through the user search. However, this presents its own set of challenges, as the body is only returned in a raw format. That is, instead of returning JSON, the HTTP request is returned as a byte stream. 

So after running the query, we turn it into text and then use the JSON Slurper to turn it into a JSON object with which we can work.

Despite the strangeness of the raw response, pagination, startAt, and maxResults still work, and are necessary to get all of the results.   Additionally, there is no flag in the HTTP response that pertains to the last page of results, such as “lastPage”. Therefor we must determine the final page of results ourselves.

The script starts by asserting

I spend a fair amount of time writing listeners with ScriptRunner, so that Jira will do various things for me based on certain criteria. For example, I write a lot of listeners that listen for when an issue is updated, and take action accordingly.

Until today, however, I had never thought how I might leverage the system to determine what kind of update had triggered the event.  I always just wrote the criteria in by hand, so that the listener would ignore anything I didn’t want eliciting a reaction.

The listener I was working on today got so complex, had so many nested IF statements and conditions, that it occurred to me to search for a better way.

As it turns out, the event object contains a lot of information, including which field’s change triggered the listener.  

In my own example, I was looking at the labels field.  I wanted the script to send a Slack message if the issue had been updated, but only if the labels had changed in a certain way.

The line of code required to check the type of update event doesn’t even require an import:

 def change = event?.getChangeLog()?.getRelated("ChildChangeItem")?.find {it.field == "labels"}

if (change) 

In my previous post I explored how to access the Confluence Cloud Space Permissions API endpoint. 

This Python script extends that, and gives a user a permission set in all Spaces in Confluence. This could be useful if you wanted to give one person Administrative rights on all Spaces in Confluence, for example.

Note that the user must first have READ/SPACE permission before any other permissions can be granted.

 from requests.models import Response
import requests
import json
  
headers = {
    'Authorization': 'Basic <Base-64 encoded username and password>',
    'Content-Type': 'application/json',
    'Accept': 'application/json',
}
  
userID = '<user ID (not name)>'
  
  
url='https://<url>.atlassian.net/wiki/rest/api/space/'
resp = requests.get(url, headers=headers)
data = json.loads(resp.text)
  
  
for lines in data["results"]:
  url="https://<url>.atlassian.net/wiki/rest/api/space/"+lines["key"] + '/permission'
  
  dictionary = {"subject":{"type":"user","identifier": userID},"operation":{"key":"read","target":"space"},"_links":{}}
  
  data = data=json.dumps(dictionary)
    
  try:
      response = requests.post(url=url, headers=headers, data=data)
      print(response.content)
  except:
    print("Could not add permissions to Space " + lines["key"])   

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
import com.atlassian.crowd.manager.directory.DirectoryManager

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 =