Best practice for calling a REST Endpoint from Project Automation ScriptRunner Script

Terry Robison April 6, 2022

I have developed a REST Endpoint to connect to LDAP/AD to determine a users Group (memberOf), and eventually add the user to a group if needed.

I am planning to call this REST Endpoint from a ScriptRunner groovy script initiated by a JIRA Automation trigger (Issue Created/Run Script). What is the best way to call the REST Endpoint?

TIA

1 answer

1 accepted

0 votes
Answer accepted
Ram Kumar Aravindakshan _Adaptavist_
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
April 13, 2022

Hi @Terry Robison

I suggest that you first try and invoke the REST Endpoint from the ScriptRunner console.

If you can get the result you want, you can use that same code in Project Automation.

Below is the sample REST Endpoint code I have tested with:-

import com.atlassian.jira.component.ComponentAccessor
import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.json.JsonBuilder
import groovy.transform.BaseScript
import groovyx.net.http.HttpResponseDecorator
import groovyx.net.http.RESTClient
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response

@BaseScript CustomEndpointDelegate delegate
getVersions { MultivaluedMap queryParams ->
def applicationProperties = ComponentAccessor.applicationProperties
def hostUrl = applicationProperties.getString('jira.baseurl')
def username = 'admin'
def password = 'q'
def projectKey = 'BT'

final def headers = ['Authorization': "Basic ${"${username}:${password}".bytes.encodeBase64()}", 'Accept': 'application/json'] as Map

def http = new RESTClient(hostUrl)
http.setHeaders(headers)

def resp = http.get(path: "/rest/api/2/project/${projectKey}/versions") as HttpResponseDecorator

if (resp.status != 200) {
log.warn 'Commander did not respond with 200'
}

def row = resp.data['name']

Response.ok(new JsonBuilder(row).toPrettyString()).build()
}

And below is the sample ScriptRunner console code I have tested with:-

import com.atlassian.jira.component.ComponentAccessor
import groovy.json.JsonSlurper

def projectManager = ComponentAccessor.projectManager

def versionManager = ComponentAccessor.versionManager

def baseUrl = 'http://localhost:9091'

final def projectKey = 'MOCK'

def project = projectManager.getProjectObjByKey(projectKey)

def hostUrl = "${baseUrl}/rest/scriptrunner/latest/custom/getVersions"

def response = hostUrl.toURL().text

def json = new JsonSlurper().parseText(response)

def artifacts = json.collect().sort()


artifacts.each {
versionManager.createVersion(it.toString(), null, null, null, project.id, null, false)
}

Please note that the sample working codes above are not 100% exact to your environment. Hence, you will need to make the required modifications.

After setting up the REST Endpoint using the first code sample, I tried to invoke it using the ScriptRunner console using the second code sample.

Once I got the code to work, I configured a Project Automation rule. Below is a print screen of my Project Automation configuration:-

automation_config.png

If you notice, I have used the same code that I have tested in the Script Console, and it can return the expected result.

I hope this helps to answer your question. :)

Thank you and Kind regards,

Ram

Terry Robison April 13, 2022

Hi Ram,

Thank you for the response, it has moved me much more forward, however, I am getting an HTTP 403 response. 

I am not sure how to add the authentication when utilizing the toURL() call?

I attempted to user the format: http://user:password@jirahost.com/queryStrnig

I logged the URL I am calling and that URL responds with a successful JSON payload from my browser..

 

ScriptRunner Console:

import com.atlassian.jira.component.ComponentAccessor
import groovy.json.JsonSlurper

def prot = 'https://'
def baseUrl = 'my.jirahost.com'
def username = 'my.user'
def pass = 'my.password'
def qString = '/rest/scriptrunner/latest/custom/getLicStat?user=my.user'
def Url = "${prot}${username}:${pass}@${baseUrl}${qString}"
log.warn("URL: " + Url)
def response = Url.toURL().text

def json = new JsonSlurper().parseText(response)

Response: 

java.io.IOException: Server returned HTTP response code: 403 for URL: https://my.user:my.pass@jirastagedc.it.keysight.com/rest/scriptrunner/latest/custom/getLicStat?user=my.user at Script483.run(Script483.groovy:11)

null

LOG:

2022-04-13 14:45:22,165 WARN [runner.ScriptBindingsManager]: URL: https://my.user:my.pass@my.jirahost.com/rest/scriptrunner/latest/custom/getLicStat?user=my.user
2022-04-13 14:45:22,242 ERROR [common.UserScriptEndpoint]: *************************************************************************************
2022-04-13 14:45:22,243 ERROR [common.UserScriptEndpoint]: Script console script failed: java.io.IOException: Server returned HTTP response code: 403 for URL: https://my.user:my.pass@my.jirahost.com/rest/scriptrunner/latest/custom/getLicStat?user=user at Script483.run(Script483.groovy:11)

Successful response from browser:

Must be authenticated in browser.

{"hasJiraLicense":true,"hasConfluenceLicense":true,"hasBitbucketLicense":true}
Ram Kumar Aravindakshan _Adaptavist_
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
April 13, 2022

Hi @Terry Robison

When you invoke the REST Endpoint you have created via the ScriptRunner Console or Project Automation, you do not need to add parameters such as username, password, or port. Your REST Endpoint configuration will handle this.

If you look at the example I have provided in my previous comment; I am not using any username or password, or port when invoking the REST Endpoint, i.e.:-

import com.atlassian.jira.component.ComponentAccessor
import groovy.json.JsonSlurper

def projectManager = ComponentAccessor.projectManager

def versionManager = ComponentAccessor.versionManager

def baseUrl = 'http://localhost:9091'

final def projectKey = 'MOCK'

def project = projectManager.getProjectObjByKey(projectKey)

def hostUrl = "${baseUrl}/rest/scriptrunner/latest/custom/getVersions"

def response = hostUrl.toURL().text

def json = new JsonSlurper().parseText(response)

def artifacts = json.collect().sort()


artifacts.each {
versionManager.createVersion(it.toString(), null, null, null, project.id, null, false)
}

I'm just invoking the REST Endpoint's URL. You may need to add additional URL parameters in some instances, like the Issue key.

In your case, your code should be something like:-

import com.atlassian.jira.component.ComponentAccessor
import groovy.json.JsonSlurper

def baseUrl = 'https://my.jirahost.com'

def username = 'my.user"

def hostURL = "${baseUrl}/rest/scriptrunner/latest/custom/getLicStat?user=${username}"

log.warn "URL: ${hostURL}"

def response = hostURL.toURL().text

def json = new JsonSlurper().parseText(response)

log.warn "=====>>>> ${json}"

I hope this helps to answer your question. :)

Thank you and Kind regards,

Ram

Terry Robison April 14, 2022

Hi Ram,

Yes, I tried calling my endpoint without using credentials and I got the 403 error, and that is why I tried adding user:pass credentials. It does make sense now that the endpoint handle authentication. However, my endpoint is more like the sample code used in the Atlassian Documentation. 

It is not clear to me how the endpoint is authenticating, other that restricting execution to jira administrators.

The endpoint does work from a browser on my client or Postman. In Postman I do provide Basic auth credentials. Without authentication I get a 401 Unauthorized as a response.

I am using this decorator as provided in the example in the Atlassian documentation:

@BaseScript CustomEndpointDelegate delegate
getLicStat( httpMethod: "GET", groups: ["jira-administrators"] )
{ queryParams, body, HttpServletRequest request ->

 

Here is the endpoint code, with user and company details obfuscated.

import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import com.onresolve.scriptrunner.ldap.LdapUtil
import org.springframework.LdapDataEntry
import org.springframework.ldap.core.support.AbstractContextMapper
import org.springframework.ldap.query.LdapQueryBuilder
import org.springframework.ldap.query.SearchScope

import javax.naming.directory.Attributes
import javax.naming.directory.Attribute
import javax.naming.NamingEnumeration

import groovy.json.JsonBuilder
import groovy.json.JsonSlurper
import groovy.json.JsonOutput
import groovy.transform.BaseScript

import javax.ws.rs.core.Response

import javax.servlet.http.HttpServletRequest

/**
* Name:
* Description:
*
* History
* Date POC Remarks
* -------- ---------------- ---------------------------------------------------------------------------
* 03/01/22 xxxxxxxx Initial Coding
*
*
**/

@BaseScript CustomEndpointDelegate delegate
getLicStat( httpMethod: "GET", groups: ["jira-administrators"] )
{ queryParams, body, HttpServletRequest request ->

final JIRA = "CN=groupx,CN=Users,DC=AD,DC=domain,DC=COM"
final CONFLUENCE = "CN=groupy,CN=Users,DC=AD,DC=domain,DC=COM"
final BITBUCKET = "CN=groupz,CN=Users,DC=AD,DC=domain,DC=COM"

def user = request.getParameter("user")

// LDAP Filter that looks for User object to lookup in LDAP
final ldapQueryFilter = "(&(sAMAccountName=${user}))"
final resourcePoolName = 'testLdapResource'

try {
LdapUtil.withTemplate(resourcePoolName) { ldap ->
// Create the LDAP query
def query = LdapQueryBuilder.query()
.searchScope(SearchScope.SUBTREE)
.filter(ldapQueryFilter)

Attributes attrs;

// Execute the search; Get Attributes from result
ldap.search(query, {
LdapDataEntry entry ->
attrs = entry.getAttributes()
} as AbstractContextMapper<Object>)

// Get the memberOf attribute from the Attributes NaminingEnumeration result
def attr = attrs.get("memberOf")

// Check for license groups
def hasJira = attr.contains(JIRA)
def hasConf = attr.contains(CONFLUENCE)
def hasBB = attr.contains(BITBUCKET)

return Response.ok(new JsonBuilder([hasJiraLicense:hasJira, hasConfluenceLicense:hasConf, hasBitbucketLicense:hasBB]).toString()).build()

}

} catch (Exception e) {
return Response.ok(new JsonBuilder([User:"Not Found"]).toString(), "JSON").build()
}

}
Ram Kumar Aravindakshan _Adaptavist_
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
April 15, 2022

Hi @Terry Robison

The main problem appears to be in your REST Endpoint code. You are not doing any authentication on it, resulting in the 400 error message.

If you view the sample REST Endpoint code that I had shared earlier, i.e. 

import com.atlassian.jira.component.ComponentAccessor
import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.json.JsonBuilder
import groovy.transform.BaseScript
import groovyx.net.http.HttpResponseDecorator
import groovyx.net.http.RESTClient
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response

@BaseScript CustomEndpointDelegate delegate
getVersions { MultivaluedMap queryParams ->
def applicationProperties = ComponentAccessor.applicationProperties
def hostUrl = applicationProperties.getString('jira.baseurl')
def username = 'admin'
def password = 'q'
def projectKey = 'BT'

final def headers = ['Authorization': "Basic ${"${username}:${password}".bytes.encodeBase64()}", 'Accept': 'application/json'] as Map

def http = new RESTClient(hostUrl)
http.setHeaders(headers)

def resp = http.get(path: "/rest/api/2/project/${projectKey}/versions") as HttpResponseDecorator

if (resp.status != 200) {
log.warn 'Commander did not respond with 200'
}

def row = resp.data['name']

Response.ok(new JsonBuilder(row).toPrettyString()).build()
}

You'll notice that I have used this header:-

final def headers = ['Authorization': "Basic ${"${username}:${password}".bytes.encodeBase64()}", 'Accept': 'application/json'] as Map 

and I have set the header to the RESTClient, i.e.

 def http = new RESTClient(hostUrl)
http.setHeaders(headers)

This will implicitly add the authentication details to the REST Endpoint. Hence, when I try even to invoke the REST Endpoint, I can invoke it as:-

http://localhost:9091/rest/scriptrunner/latest/custom/getVersions

without any need for a username or password, and I can get the expected results as shown below:-

rest_results.png

I can then invoke this in the ScriptRunner console or Project Automation, i.e. 

import com.atlassian.jira.component.ComponentAccessor
import groovy.json.JsonSlurper

def projectManager = ComponentAccessor.projectManager

def versionManager = ComponentAccessor.versionManager

def baseUrl = 'http://localhost:9091'

final def projectKey = 'MOCK'

def project = projectManager.getProjectObjByKey(projectKey)

def hostUrl = "${baseUrl}/rest/scriptrunner/latest/custom/getVersions"

def response = hostUrl.toURL().text

def json = new JsonSlurper().parseText(response)

def artifacts = json.collect().sort()


artifacts.each {
versionManager.createVersion(it.toString(), null, null, null, project.id, null, false)
}

and trigger whatever changes I want.

 

I hope this helps to answer your question. :)

Thank you and Kind regards,

Ram

Terry Robison April 18, 2022

Hi Ram,

I appreciate your continued help to figure this out, but in your example you are allowing anyone to call the endpoint (i.e. anonymous), and then setting up a RestClient with auth to call a Jira API.

I am restricting the endpoint to [Jira Administrators]. Therefore I have to be authenticated when making the call to the endpoint.

Thanks for all the effort you put into this, it certainly helped me get to a workable solution. I still have some work to do on it, however, here is the console script that will return a valid response.

import groovy.json.JsonSlurper
import groovyx.net.http.RESTClient
import groovyx.net.http.HttpResponseDecorator
import groovyx.net.http.ContentType
import groovyx.net.http.Method

def http = new RESTClient('https://myhost.com')
http.headers['Authorization'] = 'Basic '+"myuser:mypasswd".getBytes('iso-8859-1').encodeBase64()
http.setContentType(ContentType.JSON)

// Explicitly type the response. get was returning an object
HttpResponseDecorator response
response = (HttpResponseDecorator) http.get(path: "/rest/scriptrunner/latest/custom/myEndpoint", query: ['user': 'myuser'])
response.getData()
Ram Kumar Aravindakshan _Adaptavist_
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
April 19, 2022

Hi @Terry Robison

If you intend to restrict the authentication details in the REST Endpoint, you will instead have to pass the username and password as HTTP Query parameters instead of hard coding it, as shown below:-

import com.atlassian.jira.component.ComponentAccessor
import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.json.JsonBuilder
import groovy.transform.BaseScript
import groovyx.net.http.HttpResponseDecorator
import groovyx.net.http.RESTClient
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response

@BaseScript CustomEndpointDelegate delegate
getVersions { MultivaluedMap queryParams ->
def applicationProperties = ComponentAccessor.applicationProperties
def hostUrl = applicationProperties.getString('jira.baseurl')
def username = queryParams.getFirst('username') // to be invoked by params
def password = queryParams.getFirst('password') // to be invoked by params
def projectKey = '<PROJECT_KEY>'

final def headers = ['Authorization': "Basic ${"${username}:${password}".bytes.encodeBase64()}", 'Accept': 'application/json'] as Map

def http = new RESTClient(hostUrl)
http.setHeaders(headers)

def resp = http.get(path: "/rest/api/2/project/${projectKey}/versions") as HttpResponseDecorator

if (resp.status != 200) {
log.warn 'Commander did not respond with 200'
}

def row = resp.data['name']

Response.ok(new JsonBuilder(row).toPrettyString()).build()
}

As you notice in the example above, instead of setting the username and password on the REST Endpoint, I am passing them as query parameters instead, i.e.:-

 def username = queryParams.getFirst('username')
def password = queryParams.getFirst('password')

Hence, when I invoke the REST Endpoint on the ScriptRunner Console or Automation script, I will need to use:-

def hostUrl = "${baseUrl}/rest/scriptrunner/latest/custom/getVersions?username=${username}&password=${password}"
So this will restrict access only to users who have login details. 
You will have to include the username and password parameters into your REST Endpoint code for this to work.
Below is the updated example for the ScriptRunner console code:-
import com.atlassian.jira.component.ComponentAccessor

import groovy.json.JsonSlurper

def projectManager = ComponentAccessor.projectManager

def versionManager = ComponentAccessor.versionManager

def baseUrl = 'http://localhost:9091'

final def projectKey = 'BT'

final def username = 'admin'

final def password = 'q'

def project = projectManager.getProjectObjByKey(projectKey)

def hostUrl = "${baseUrl}/rest/scriptrunner/latest/custom/getVersions?username=${username}&password=${password}"

def response = hostUrl.toURL().text

def json = new JsonSlurper().parseText(response)

def artifacts = json.collect().sort()

artifacts.each {

versionManager.createVersion(it.toString(), null, null, null, project.id, null, false)

}
Your updated Automation code should be something like this:-
import com.atlassian.jira.component.ComponentAccessor
import groovy.json.JsonSlurper

def baseUrl = 'https://my.jirahost.com'
def username = 'my.user'
def pass = 'my.password'

def qString = "${baseUrl}/rest/scriptrunner/latest/custom/getLicStat?username=${user}&password=${pass}"
log.warn "URL: ${qString}"

def response = qString.toURL().text

def json = new JsonSlurper().parseText(response)

I hope this helps to solve your question. :)

Thank you and Kind regards,

Ram

Like # people like this
Terry Robison April 19, 2022

Thanks Ram!

Suggest an answer

Log in or Sign up to answer
TAGS
AUG Leaders

Atlassian Community Events