Connect to Atlassian Via App Links
Application Links exist to handle authentication when making calls between applications.
App Link Configuration
App links can be configured in one of three ways
OAuth
Trusted applications
Basic authentication
However only the first of these is currently recommended by Atlassian, and is the kind we will focus on.
Note that all of the examples provided below also work with Trusted applications.
OAuth comes in two flavors: OAuth authentication and OAuth with impersonation. Impersonation should only be used where the sets of users on both apps are the same.
If you can enable OAuth with impersonation you should do so, as it allows you to make requests from code on behalf of the user that invoked the function (eg event, workflow function etc), without any interaction on their behalf.
The following image shows the outbound configuration the source app, and the inbound configuration for the target app, when two-legged oauth with impersonation is enabled.
Configuration with 2 legged-oauth
If you create a new reciprocal app link, and check the box confirming that the set of users are the same on both apps, you will get the configuration above.
Making Requests as the Current User
If you wish to make a remote REST request on behalf of the current user, you can do so using com.atlassian.applinks.api.ApplicationLink.createAuthenticatedRequestFactory()
. The following example calls a Jira endpoint that returns details of the user making the request, so is useful for experimenting.
This example should work calling Jira from Bitbucket Server, Confluence, and Bamboo. To call another app you should change the endpoint, as this one exists only in Jira. Note also that in Jira you will get the ApplicationLinkService
using ComponentAccessor
rather than ComponentLocator
.
import com.atlassian.applinks.api.ApplicationLinkResponseHandler import com.atlassian.applinks.api.ApplicationLinkService import com.atlassian.applinks.api.application.jira.JiraApplicationType import com.atlassian.sal.api.component.ComponentLocator import com.atlassian.sal.api.net.Response import com.atlassian.sal.api.net.ResponseException import groovy.json.JsonSlurper import static com.atlassian.sal.api.net.Request.MethodType.GET def appLinkService = ComponentLocator.getComponent(ApplicationLinkService) def appLink = appLinkService.getPrimaryApplicationLink(JiraApplicationType) // <1> def applicationLinkRequestFactory = appLink.createAuthenticatedRequestFactory() // <2> def request = applicationLinkRequestFactory.createRequest(GET, "/rest/auth/1/session") def handler = new ApplicationLinkResponseHandler<Map>() { @Override Map credentialsRequired(Response response) throws ResponseException { null } @Override Map handle(Response response) throws ResponseException { assert response.statusCode == 200 new JsonSlurper().parseText(response.getResponseBodyAsString()) as Map // <3> } } def sessionDetails = request.execute(handler) // <4> log.debug("Making the request as: " + sessionDetails["name"])
Line 13: Get the primary Jira application link - if you have multiple, see Two-legged OAuth Without Impersonation.
Line 15: Get the OAuth provider.
Line 28: The response is JSON, so convert it to a Map.
Line 32: Execute the request.
You should see a log message like:
2016-05-05 10:15:34 [http-bio-8080-exec-10] DEBUG c.o.s.runner.ScriptRunnerImpl - Making the request as: admin
Making Requests as Another User
That’s great, but let’s say you want you want to create a new page in Confluence on a Jira transition. If the user executing the Jira transition does not have permission to create a page, this will fail. You may well want this to happen, indeed it’s probably the safest course. But let’s say you want to create a Confluence space on a transition, perhaps as part of a provisioning process. If this is the case you will need to temporarily impersonate a user on the target system that has the create page or create space permission etc.
How you do that is unfortunately not standardized across applications, so what follows are examples for each application supported by ScriptRunner:
It’s advisable to create a user called for example: deployment
, which has admin rights. You can create a password which can’t be used, so you will know whenever you see this user in the audit log, that these changes are the results of your integrations.
From Bitbucket Server
import com.atlassian.bitbucket.user.SecurityService import com.atlassian.bitbucket.user.UserService import com.atlassian.bitbucket.util.Operation import com.atlassian.sal.api.component.ComponentLocator import com.atlassian.sal.api.user.UserManager def userManager = ComponentLocator.getComponent(UserManager) def securityService = ComponentLocator.getComponent(SecurityService) def adminUser = ComponentLocator.getComponent(UserService).getUserByName("deployer") assert adminUser: 'make sure an admin user called "deployer" exists in all instances' def securityContext = securityService.impersonating(adminUser, "Creating a space on behalf of ${userManager.getRemoteUser().fullName}") securityContext.call(new Operation<Void, RuntimeException>() { @Override Void perform() throws Throwable { assert userManager.getRemoteUser().username == "deployer" // make REST requests here null } })
From Confluence
import com.atlassian.confluence.user.AuthenticatedUserImpersonator import com.atlassian.confluence.user.AuthenticatedUserThreadLocal import com.atlassian.confluence.user.UserAccessor import com.atlassian.sal.api.component.ComponentLocator def userAccessor = ComponentLocator.getComponent(UserAccessor) def adminUser = userAccessor.getUserByName("deployer") assert adminUser: 'make sure an admin user called "deployer" exists in all instances' AuthenticatedUserImpersonator.REQUEST_AGNOSTIC.asUser({ assert AuthenticatedUserThreadLocal.get().name == "deployer" // make REST requests here }, adminUser)
From Jira
import com.atlassian.jira.component.ComponentAccessor def jiraAuthenticationContext = ComponentAccessor.getJiraAuthenticationContext() def originalUser = jiraAuthenticationContext.getLoggedInUser() def userManager = ComponentAccessor.getUserManager() def adminUser = userManager.getUserByName("deployer") assert adminUser: 'make sure an admin user called "deployer" exists in all instances' try { jiraAuthenticationContext.setLoggedInUser(adminUser) // make REST requests here } finally { jiraAuthenticationContext.setLoggedInUser(originalUser) }
Two-legged OAuth Without Impersonation
If you have different sets of users between your apps, you cannot use two-legged authentication with impersonation. The problem here is that user jbloggs on Jira may actually refer to a different jbloggs on Confluence. Atlassian expresses this in their documentation as "users must have the same password", but I believe they mean that the user for any given ID should be the same person.
If you just have some a subset of users in one app that all exist in another app you should be OK using 2-legged with impersonation.
If not, your alternatives are:
specify a user on the target app that all remote requests via this link will execute as. However, as you can’t specify a user with admin rights this may not be useful
prompt the end-user to do the login dance, which will allow them to grant the source app to make requests to the target on their behalf.
This code sample focuses on doing the dance.
import com.atlassian.applinks.api.ApplicationLinkRequest import com.atlassian.applinks.api.ApplicationLinkService import com.atlassian.applinks.api.CredentialsRequiredException import com.atlassian.applinks.api.application.jira.JiraApplicationType import com.atlassian.sal.api.component.ComponentLocator import static com.atlassian.sal.api.net.Request.MethodType.GET def appLinkService = ComponentLocator.getComponent(ApplicationLinkService) def appLink = appLinkService.getPrimaryApplicationLink(JiraApplicationType) def applicationLinkRequestFactory = appLink.createAuthenticatedRequestFactory() ApplicationLinkRequest request try { request = applicationLinkRequestFactory.createRequest(GET, "/rest/auth/1/session") } catch (CredentialsRequiredException e) { log.warn("Your permission is required to make this request, go to: ${e.authorisationURI}") return } // we have the permission, use the request object...
Once the user has clicked the link which takes them to the page below, the code will proceed beyond the catch
block.
The problem you may encounter is that there may not be any easy way to present this URL to the user in the web UI. In this case, you could just fail the transition or whatever, or in Jira, add a message into the UI:
import com.onresolve.scriptrunner.runner.util.UserMessageUtil // get requestUrl as above UserMessageUtil.warning("Please authenticate to Confluence " + "via ${requestUrl} before executing this transition.")