When running Confluence, your technical team may find itself in a position where it needs to access data via external apps. This can be a typical problem for advanced users of Atlassian tools, who have engineering teams wishing to pull data into other services or applications. For example, such teams may wish to aggregate content with certain tags and make it available to internal Intranet search tools. In this post we will be demonstrating how technical users of Confluence can address this problem.
The easiest way of retrieving this data is via the Confluence REST API. However, each API call needs to either have the credentials of a user, with access to the data, or needs an Admin role. This can be a headache to manage and maintain, especially when passwords change, and also risks data being missed or incorrect data being exposed if the wrong user account is used.
In addition to the functionality provided in the out-of-the-box API, you may wish to complete more sophisticated search filtering tasks, or lock down exactly what users can do. In this latter example, restricting users ability to modify and delete content via API calls can prevent the accidental corruption of content. In both these cases, you will need a third-party tool or plugin to achieve this result.
Thankfully, this is where one particular plugin, ScriptRunner, comes in handy. ScriptRunner allows you to develop functionality via Groovy scripts that tap into the underlying Confluence API.
You can find the list of Java Packages available to you, and thus your scripts, within the Confluence plugin documentation.
A recent client engagement at Modus led us to the situation where we needed to allow a custom in-house application to access Confluence, but it needed to assume the identity of specific users. Itโs exactly these types of requirements that ScriptRunner is particularly suitable for.
This article will look at how you can secure the Confluence REST API and build custom endpoints via ScriptRunner. These endpoints then allow an external authenticated application to assume the identity of a specific user so all queries run will return only the data that user can see. This avoids having to store multiple user credentials in the custom application, and gives us only one set of credentials we have to worry about protecting and maintaining.
The steps we will be performing to achieve the above goals are as follows:
- Setting up ScriptRunner.
- Building an Authentication endpoint.
- Building a Query endpoint.
- Testing our endpoints via Postman.
- Locking down the API.
- Discussing further security considerations.
So letโs get started!
Setting up ScriptRunner
Our first task is going to be to set up the ScriptRunner plugin in Confluence. Note that youโll need to have access to an Admin role in order to do this. The ScriptRunner plugin can be obtained from the Atlassian marketplace. The installation documentation for all versions of Confluence can be found here.
Once installed you should now be able to see a number of tabs with various sets of features that ScriptRunner offers. For the purposes of this article we are interested in the REST Endpoints tab. Take some time to go through the documentation so you have a better understanding of this API.
Itโs now time to take some security considerations on board. Before we write any code, you will need to create a new user in the Confluence Internal directory (serviceuser
) via the Admin user. You should give this account limited access to anything in Confluence, and add it to a separate user group (service-users
). The sole purpose of this account is to be able to hit the Authentication endpoint we will be creating. This endpoint will be locked down to only allow code execution by users in the custom group we have created.
Building Custom Endpoints
In order to develop a custom endpoint you will need to also familiarize yourself with Groovy.
Thankfully, Groovy allows us to import the Java libraries used by Confluence, so most things you can do with a custom Java plugin, you can do with a Groovy script.
A typical endpoint follows this format:
@BaseScript CustomEndpointDelegate delegate myEndpoint( httpMethod: "GET", groups: ["service-users"] ) { MultivaluedMap queryParams, String body -> return Response.ok(new JsonBuilder([hello: "world"]).toString()).build() }
In this block of code we define the endpoint name (myEndpoint
), the type of HTTP method it accepts (GET
), the groups that can execute the endpoint (service-users
) and then the rest of the body of the method. Finally the endpoint has a hard coded response, which returns an object with a single key/value pair.
To learn more about how ScriptRunner constructs endpoints you can review the documentation at the ScriptRunner website.
We are going to build two endpoints. The first is the Authentication endpoint. This uses the credentials that we created in the previous step. Confluence will use basic auth to handle authenticating an existing user.
For more information on Basic authentication in Confluence, please refer to the developer documentation.
Authentication Endpoint
The purpose of the authentication endpoint will be to log in as the service user we created, and then impersonate a user of choice in Confluence. For the purposes of this demo, we will create a new user (testuser
) and hard code it into the script. Once you are confident this is working as expected, you can replace the hard-coded user with a variable, and pass in any user you wish via the request.
This demo also uses a GET
request, but this can also be modified to use POST
instead.
We will start by creating a new endpoint in ScriptRunner for our authAndImpersonate
endpoint as follows:
- Navigate to Confluence Administration
- On the left navigation find the ScriptRunner menu and select the REST Endpoints tab
- Select Add New Item, and then select the Custom endpoint option to add your own. You will then be presented with the following screen:
To the inline script section add the following Groovy code.
import com.atlassian.seraph.auth.DefaultAuthenticator import com.atlassian.confluence.user.UserAccessor import com.onresolve.scriptrunner.runner.rest.common.ServletRequestThreadLocal import com.atlassian.confluence.user.AuthenticatedUserThreadLocal import com.atlassian.sal.api.component.ComponentLocator import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate import groovy.json.JsonBuilder import groovy.transform.BaseScript import javax.ws.rs.core.MultivaluedMap import javax.ws.rs.core.Response import org.codehaus.jackson.map.ObjectMapper @BaseScript CustomEndpointDelegate delegate authAndImpersonate( httpMethod: "GET", groups: ["service-users"] ) { MultivaluedMap queryParams, String body -> def userAccessor = ComponentLocator.getComponent(UserAccessor) def targetUser = userAccessor.getUserByName("testuser") ServletRequestThreadLocal.get().getSession().setAttribute(DefaultAuthenticator.LOGGED_IN_KEY, targetUser) return Response.ok(new JsonBuilder([user: targetUser]).toString()).build() }
We will now look briefly at what this code is doing.
First we have created a GET
endpoint, accessible to the service-users
group, called authAndImpersonate
.
Within the body of the method we import the UserAccessor
interface, and can then reference it via the userAccessor
variable.
Following this, we select our target user by passing in their username (testuser
) as a string. This returns a user object.
The user object is then used to create a new session for that user on the subsequent line. This will generate a new session and the session cookie can then be returned in the response.
Finally, we return said response and include the targetUser
variable in the object to show it was executed successfully.
Save the endpoint. It will now be available for you to click on, which launches a new tab in your browser.
With the testuser
, create a new space called myspace
. This will be used in the next ScriptRunner endpoint which queries for data. To this space add a page with sample content and add a new label called test
.
We are now ready to add a Query endpoint which can return data for this new user from the space that was added.
Query Endpoint
Our next endpoint does the bulk of the work. Using the cookie that was returned from the authAndImpersonate
endpoint we can now execute a query against Confluence as if we were logged in as the user impersonated in the Authentication endpoint.
Add a new endpoint via ScriptRunner, and add the following code to it:
import com.atlassian.seraph.auth.DefaultAuthenticator import com.atlassian.confluence.user.UserAccessor import com.atlassian.confluence.user.AuthenticatedUserThreadLocal import com.atlassian.confluence.spaces.SpaceManager import com.atlassian.sal.api.component.ComponentLocator import com.atlassian.confluence.search.v2.SearchManager import com.atlassian.confluence.search.v2.searchfilter.SiteSearchPermissionsSearchFilter import com.atlassian.confluence.search.v2.ContentSearch import com.atlassian.confluence.search.v2.query.* import com.atlassian.confluence.search.v2.sort.ModifiedSort import com.atlassian.confluence.search.v2.SearchSort import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate import groovy.json.JsonBuilder import groovy.transform.BaseScript import javax.ws.rs.core.MultivaluedMap import javax.ws.rs.core.Response import org.codehaus.jackson.map.ObjectMapper @BaseScript CustomEndpointDelegate delegate getData( httpMethod: "GET", groups: ["confluence-administrators", "confluence-users"] ) { MultivaluedMap queryParams, String body -> def userAccessor = ComponentLocator.getComponent(UserAccessor) def spaceManager = ComponentLocator.getComponent(SpaceManager) def searchManager = ComponentLocator.getComponent(SearchManager) def query = BooleanQuery.andQuery(new LabelQuery("test"), new InSpaceQuery("myspace")); def sort = new ModifiedSort(SearchSort.Order.DESCENDING); // latest modified content first def searchFilter = SiteSearchPermissionsSearchFilter.getInstance(); def searchContent = new ContentSearch(query, sort, searchFilter, 0, 10); def searchresult = searchManager.search(searchContent) return Response.ok(new JsonBuilder([results: searchresult.getAll()]).toString()).build() }
As with the previous endpoint we have the initial method declaration which names the endpoint and provides a list of groups that can access it.
Within the body of the method we start by defining three variables. Each of these: userAccessor
, spaceManager
, and searchManager
imports functionality from Confluence. This functionality allows us to construct a query to return data from the Confluence database.
Following these three declarations, we then construct the query itself and store it in the query variable. The query is performing a boolean and
operation looking for content with the label test
AND located within the new space we created called myspace
.
Our next variable declaration (def sort
) is responsible for holding the sort information that will be performed against the query results. In this instance, we will be returning the results in descending order with latest modified content being listed first.
Next, we need to provide a mechanism to ensure that the only content our user has access to is returned from the endpoint. This is handled by adding a search filter that can use the site permissions functionality and stored in the variable searchFilter
.
Now that the basic query, sorting, and filtering is constructed we can create a new ContentSearch
, and then execute this via the searchManager
. The results of this search are then stored in our searchresults
variable.
The data returned from our two new endpoints will be a JSON object. You can change the endpoint code to be either a GET
request or POST
, or expand the query to encompass whatever you require in the response. The options for how you configure the endpoint are very flexible and should meet most needs. For further information on customizations please refer to the Search API document located on the Confluence website.
Our basic querying endpoint is now complete so go ahead and save it. Like the authAndImpersonate
endpoint, it can now be accessed from the ScriptRunner REST Endpoints tab.
We are going to explore testing the endpoint functionality now via a third party tool called Postman.
Consume the data
Postman is an application that allows you to easily test REST API endpoints and can be downloaded from the following URL:
Once installed, you can now make REST requests to Confluence through an easy to configure GUI. Postman also caches the session cookie returned from endpoints as well without you needing to do anything else.
Letโs start with adding in a GET request to the authAndImpersonate
endpoint. The URL will look something like this, depending on your Data Center installation.
https:///rest/scriptrunner/latest/custom/authAndImpersonate
The screenshot below demonstrates how you add this:
You will need to add the authentication option for your new request as Basic Auth and include the credentials for the serviceuser
user you created.
This can be done by changing the Authorization
mode under the Params
option.
To the username field, add the new admin account (serviceuser
) and also include the password you assigned to it.
We can now hit the new endpoint to get data back via the Send
button. On executing the authAndImpersonate
endpoint, you should see something like this:
{ "user": { "backingUser": { "active": true, "lowerName": "testuser", "directoryId": 292913, "fullName": "testuser", "emailAddress": "testuser@example.com", "email": "testuser@example.com", "name": "testuser", "displayName": "testuser" }, "lowerName": "testuser", "key": { "stringValue": "2a9fc5c867064d670169071e83e60131" }, "fullName": "testuser", "email": "testuser@example.com", "name": "testuser" } }
This shows we successfully accessed authAndImpersonate
and have returned a session for the user we hard coded called testuser
. This will be stored as a cookie inside Postman.
Next create a new tab in Postman, and add in the getData
API endpoint you created previously.
The cookie associated with the authenticated userโs (testuser
) active session can be included when calling this endpoint. ScriptRunner will return a JSON object that uses the following structure (note: we have removed the middle section of the endpoint response for brevity):
{ "results": [ { "displayTitle": "First page in Test space", "handle": { "className": "com.atlassian.confluence.pages.Page", "id": 3602488 }, "lastUpdateDescription": "", "ownerTitle": null, "spaceName": "test user", "creatorUser": { "backingUser": { "active": true, "lowerName": "testuser", "directoryId": 294513, "fullName": "test user", "emailAddress": "testuser@example.com", "email": "testuser@example.com", "name": "testuser", "displayName": "test user" }, "lowerName": "testuser", "key": { "stringValue": "2c3fe4c863deb2ac0168deb9b17d0110" }, "fullName": "test user", "email": "testuser@example.com", "name": "testuser" }, ... "homePage": false, "sanitisedContent": "This is the first page in test space \n\n\n\n\n\n\n test" } ] }
The contents returned will depend on what you have configured in your system including what the user you impersonated can view.
And there we have it! An example of how to log into the system as special service user, switch to an existing Confluence user, query and safely filter data, and finally return a JSON object with said data.
Now that we have our endpoints working, letโs lock down the REST API.
Locking down the REST API
Leaving the RESTful API available to users may be useful, but also runs some risks. If users leave their credentials on their laptop they could unwittingly be exposed.
There is also the risk of a user accidentally executing a command that deletes data without realizing they did it. Forcing users through the GUI where there are more visual checks in place, may make sense to your organization.
Therefore you may wish to lock down the API and only make the ScriptRunner endpoint available. If you go this route, and make the endpoint available to all users, the worst that can happen is they can execute the Query endpoint as themselves and return data. This prevents arbitrary queries being executed and, assuming you are not writing data, reduces risks.
Depending on the tools you use in your environment, the method to block or re-route specific URL paths will be different. Please refer to the documentation for your web server or load balancer for the specifics.
However regardless of technology you will need to prevent users from accessing the following URL path structure:
https:///rest/api/*
Be sure to allow traffic to the ScriptRunner path.
https:///rest/scriptrunner/*
With this now configured, your system should only allow traffic to the new ScriptRunner endpoints.
Letโs wrap up with some final security considerations.
Security considerations
Our final look at this custom functionality is to review security considerations. Weโve discussed locking down the API and how to create a secure user, but is there anything else we should take into account?
- The service userโs credentials should be securely stored in the application consuming the data from Confluence.
- Do not leave the credentials in clear text in CURL requests, and ensure the machine executing the requests is also following security best practices.
- Additionally consider injecting the service userโs credentials into the request at execution time using a secure practice, and ensure the connection is over HTTPS.
- Periodically change the password, as if this account is compromised, a user hitting the Authentication endpoint could assume the role of another admin, who has far more permissive settings.
- When implementing this functionality you should consider whether you really need to write data. Locking down the query to read only is much safer. While it wouldnโt prevent an unauthorized user who gains access to the endpoint with the correct credentials from accessing data, it can prevent corruption and deletion of data by accident.
We should note that, there is an additional risk here which a malicious actor could try to leverage. An unauthorized individual who retrieves the cookie of a user from the Authentication endpoint, can then assume the session of that user and gain access to the Confluence front end, and subsequently do damage. Therefore, always make sure the service userโs credentials used to access the endpoint are kept as securely as possible as discussed above, and the application is not publicly accessible via the web.
Conclusion
In this post we have demonstrated how Confluence can be communicated with from third party apps. This allows other teams at your organization to aggregate data and share it with non-Atlassian applications.
As seen, you can built new REST endpoints into Confluence via the ScriptRunner plugin. These endpoints allow other applications on your network to query and consume data from Confluence in JSON format.
To handle filtering data, we demonstrated how the endpoints can be configured to allow the application to assume the role of another user in the system. This impersonation means that query results will only return what that user can see and nothing else. Using this mechanism and the Authentication endpoint allows dynamically querying without having to store all the users credentials in the application accessing Confluence. Instead we only have to store the values for a single special case service user.
This simple proof of concept should pave the way for you to create your own custom endpoints. These will be written in Groovy and implement the Java libraries Confluence makes available for developers.
Modus supports clients in all stages of the Atlassian lifecycle, from licensing, to enabling, to scaling, to training. Learn more about what we can do with Atlassian and talk to an expert on our partner page. We also have unique experience leveraging our AWS partnership to design, migrate, and deploy Atlassian instances in the AWS cloud.
Andy Dennis
Related Posts
-
Ship It: Release Management in Jira and Confluence
Ship it! Planning, managing and communicating releases is often done as an afterthought. You release…
-
How To Be A Confluence and Jira Requirements Hero
Manually creating user stories with Atlassianโs Jira from product requirements written in Confluence takes effort…