You can define REST endpoints in ScriptRunner, for example to:

Adding a REST Endpoint

Navigate to Admin → REST Endpoints.

Click a heading to add a handler. Choose Custom endpoint to add your own endpoint.

REST endpoints are configured programatically. There is currently one sample defined (click Expand Examples), which we’ll discuss in-depth:

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

@BaseScript CustomEndpointDelegate delegate (1)

doSomething( (2)
    httpMethod: "GET", groups: ["confluence-administrators"] (3)
) { MultivaluedMap queryParams, String body -> (4)
    return Response.ok(new JsonBuilder([abc: 42]).toString()).build() (5)
}
1 - this line makes methods in your script recognisable as endpoints, and is required
2 - the name of the REST endpoint, which forms part of the URL, in this case doSomething
3 - configuration of the endpoint, in this case which HTTP verb to handle, and what groups to allow
4 - parameters which are provided to your method body
5 - the body of your method, where you will return a javax.ws.rs.core.Response object

Once this is added to the list of configured endpoints, either as an inline script, or by copying it to a file and adding as a script file, you should be able to test the endpoint by visiting in your browser:

<confluence_base_url>/rest/scriptrunner/latest/custom/doSomething

Alternatively using a command line utility:

>curl -u admin:admin http://localhost:8080/confluence/rest/scriptrunner/latest/custom/doSomething
{"abc":42}

If you are using a file, you can try changing the response, though you may need to hit the "Scan" button on the REST Endpoints page before calls to the endpoint return the new response. See the section on script root scanning, below.

admin:admin corresponds to a username and password.

Configuration

The general format of a method defining a REST endpoint is:

methodName (Map configuration, Closure closure)

For the configuration only the following options are supported:

httpMethod

One of GET, POST, PUT, DELETE

groups

One or more groups. If the requesting user is in any of these groups the request will be allowed

Note that either or both of these can be omitted. If you omit the groups attribute, the endpoint will be available to unauthenticated users.

The closure can take these parameters:

MultivaluedMap queryParams

corresponds to the URL parameters

String content

The body of the request, for POST and PUT etc

HttpServletRequest request

The request object. You can use this to get the requesting user for instance.

You can use any of these forms for your closure:

something() { MultivaluedMap queryParams ->
something() { MultivaluedMap queryParams, String body ->
something() { MultivaluedMap queryParams, String body, HttpServletRequest request ->

depending on what you need access to.

Access request URL

Sometimes you may need to use the URL path after your method name, for instance in the following call, you want to retrieve the /foo/bar:

<base_url>/rest/scriptrunner/latest/custom/doSomething/foo/bar

To get this, use the 3-param form of the closure definition, and call the getAdditionalPath method from the base class:

doSomething() { MultivaluedMap queryParams, String body, HttpServletRequest request ->

    def extraPath = getAdditionalPath(request)
    // extraPath will contain /foo/bar when called as above
}
In previous versions, an extraPath variable was injected into the scripts, but this is not thread-safe - use the above method instead.

Examples

Create User

This example demonstrates plugging a gap in the official REST API. As at the time of writing, there is no way to create a new User.

import bucket.user.UserAccessor
import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.user.GroupManager
import com.atlassian.user.impl.DefaultUser
import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.json.JsonBuilder
import groovy.transform.BaseScript
import org.codehaus.jackson.map.ObjectMapper

import javax.servlet.http.HttpServletRequest
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response

import static com.atlassian.user.security.password.Credential.unencrypted

@BaseScript CustomEndpointDelegate delegate

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

user(
        httpMethod: "POST", groups: ["confluence-administrators"]
) { MultivaluedMap queryParams, String body ->

    def mapper = new ObjectMapper()
    def user = mapper.readValue(body, Map)
    assert user.username // must provide username
    assert user.fullname // must provide fullname
    assert user.email // must provide email
    assert user.group // must provide group
    assert user.password // must provide group

    try {
        def newUser = new DefaultUser(user.username, user.fullname, user.email)
        // password ought to be encrypted but this is just an example
        userAccessor.createUser(newUser, unencrypted(user.password as String))
        userAccessor.addMembership(user.group, user.username)

    } catch (e) {
        return Response.serverError().entity([error: e.message]).build()
    }

    return Response.created(new URI("/$user.username")).build()
}

Most of this code is involved with validating the JSON and query params that are passed to it, that is ensuring that all required fields for a user are present. We use the appropriate method on the Response class to send the right status code, eg 500 (server error) if the user already exists, and 201 (created) if we create the user.

To test it you could use

curl -v -X POST -H "Content-type: text/json" -u admin:admin --data "@user.json" \
    <confluence_base_url>/rest/scriptrunner/latest/custom/user

where user.json is a text file containing:

{
  "username": "newuser",
  "fullname": "New User",
  "email": "newuser@example.com",
  "group": "confluence-users",
  "password": "newuser"
}

Also note that you can have multiple methods with the same name in the same file, which is useful to do simple CRUD REST APIs, eg:

POST /user - creates a user
PUT /user - updates a user
DELETE /user - deletes a user
GET /user - gets a user

Get A User

In the previous example we created a user. This example demonstrates how we can retrieve that user.

user(
        httpMethod: "GET", groups: ["confluence-administrators"]
) { MultivaluedMap queryParams, String body, HttpServletRequest request ->

    // validate we have username as a url parameter
    def extraPath = getAdditionalPath(request)
    assert extraPath =~ "^/[a-zA-Z]+"
    def username = extraPath.split("/").last()

    def user = userAccessor.getUser(username)
    // user must exist in Confluence
    if (!user) {
        return Response.serverError().entity([error: "User $username does not exist"]).build()
    }

    def userResponse = [
            username: user.name,
            fullname: user.fullName,
            email: user.email,
            groups: groupManager.getGroups(user)*.name
    ]

    return Response.ok(new JsonBuilder(userResponse).toString()).build()
}

Note that when we created the previous user we got the following header in the response

Location: <confluence_base_url>/rest/scriptrunner/latest/custom/user/newuser

We can then use the GET user endpoint in the same script, using that location to retrieve the user from.

To retrieve the user you could use the following command

curl -X GET -u admin:admin <confluence_base_url>/rest/scriptrunner/latest/custom/user/newuser

As before we use the appropriate method on the Response class to send the right status code, eg 500 (server error) if the user does not exist, and 200 (ok) if we retrieve the user.

Create Project Pages

This Custom REST Endpoint provides functionality that allows you to create complex page structures automatically within Confluence. There are several different ways in which you can utilise this. These include, but are not limited to:

  • Using a custom button (Script Fragment > Custom Web Item) to allow users to trigger the action.

  • Calling the endpoint from another Atlassian application or 3rd party system, allowing you to create page structures within Confluence. In this example we’ll be using the custom Script Fragment to demonstrate the functionality.

This example is provided as a starting point to implement your own custom solution and not as a copy paste solution. Security concerns should be evaluated as part of implementing your own version of this endpoint, here are a few:

  • Consider which groups should have access to your rest endpoint and restrict as required.

  • Check to ensure users have the correct Confluence permissions for the work being done by your endpoint. In its current state the endpoint checks to ensure the calling user is in the confluence-administrators or confluence-users group. Additionally it checks to ensure the calling user has view permissions on the parent page.

import com.atlassian.confluence.core.BodyContent
import com.atlassian.confluence.core.BodyType
import com.atlassian.confluence.core.DefaultSaveContext
import com.atlassian.confluence.pages.DuplicateDataRuntimeException
import com.atlassian.confluence.pages.Page
import com.atlassian.confluence.pages.PageManager
import com.atlassian.confluence.pages.templates.PageTemplate
import com.atlassian.confluence.pages.templates.PageTemplateManager
import com.atlassian.confluence.security.Permission
import com.atlassian.confluence.security.PermissionManager
import com.atlassian.confluence.spaces.Space
import com.atlassian.confluence.spaces.SpaceManager
import com.atlassian.confluence.user.AuthenticatedUserThreadLocal
import com.atlassian.sal.api.component.ComponentLocator
import com.onresolve.scriptrunner.canned.confluence.utils.PermissionDeniedException
import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
import groovy.json.JsonOutput
import groovy.transform.BaseScript
import groovy.transform.Field

import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.core.Response

@Field SpaceManager spaceManager = ComponentLocator.getComponent(SpaceManager)
@Field PageManager pageManager = ComponentLocator.getComponent(PageManager)
@Field PermissionManager permissionManager = ComponentLocator.getComponent(PermissionManager)
@Field PageTemplateManager pageTemplateManager = ComponentLocator.getComponent(PageTemplateManager)

// The specified setup - Specify the page titles and hierarchy here
// This example setup contains one page with 2 child pages
// The template value takes the id of the template you want to use
def spec = [
    [
        title           : "Planning",
        templateName    : "template1",
        templateSpaceKey: "PS",
        page            : [
            [
                title           : "Review",
                templateName    : "template2",
                templateSpaceKey: "global-template"
            ],
            [
                title           : "Social",
                templateName    : "template1",
                templateSpaceKey: "PS"
            ]
        ]
    ]
] as List<Map>

@BaseScript CustomEndpointDelegate delegate
createProjectPages(httpMethod: "GET", groups: ["confluence-administrators", "confluence-users"]) { MultivaluedMap queryParams, String body ->
    // This is the space key specified in the custom script fragment
    def spaceKey = queryParams.getFirst("spaceKey") as String
    // This is the parent page id specified in the custom script fragment
    // The first page created in this script will use the page associated with the parent page id as its parent
    def parentPageId = queryParams.getFirst("parentPageId") as Long
    def mainPageTitle = spec.get(0).get("title")
    // Flag shown to user on successful or failed creation of new project structure pages
    def flag = [
        type : 'success',
        title: "Pages created",
        close: 'auto',
        body : "Refresh this page to see the newly created page (${mainPageTitle}) and its children in the page tree"
    ]
    try {
        createPages(spaceKey, parentPageId, spec)
    } catch (IllegalStateException | DuplicateDataRuntimeException | PermissionDeniedException e) {
        log.error("There was a problem trying to create the project structure", e)

        flag = [
            type : 'failure',
            title: "An error occurred",
            close: 'manual',
            body : "There was an error trying to create project structure pages"
        ]
    }
    Response.ok(JsonOutput.toJson(flag)).build()
}

/**
 * Create the desired page structure
 *
 * @param spaceKey The Key of the space to add pages for.
 * @param parentPageId The page id of the parent page.
 * @param spec The specification for the pages to be created.
 */
void createPages(String spaceKey, Long parentPageId, List<Map> spec) throws IllegalStateException, PermissionDeniedException, Exception {
    def space = spaceManager.getSpace(spaceKey) as Space
    def parentPage = pageManager.getPage(parentPageId) as Page ?: spaceManager.getSpace(spaceKey).getHomePage()
    if (!parentPage) {
        throw new IllegalStateException("The specified parent page for new project structure pages does not exist")
    }
    if (!userHasPageViewPermission(parentPage)) {
        throw new PermissionDeniedException("User does not have the required permission to create child pages on page with id ${parentPage.getId()}")
    }
    spec.each { pageSpec ->
        createPage(parentPage, space, pageSpec as Map)
    }
}

/**
 * Check if the user clicking the fragment button has the relevant permission to create child pages.
 * @param parentPage
 * @return user permission view status
 */
boolean userHasPageViewPermission(Page parentPage) {
    def user = AuthenticatedUserThreadLocal.get()
    return permissionManager.hasPermission(user, Permission.VIEW, parentPage)
}

/**
 * Create a page using the given page specification (pageSpec). This spec dictates the title of the page, the template
 * to be used to populate it (if any) and if required, the specification of any child pages.
 *
 * @param parentPage The parent page for the page we're about to create.
 * @param space The space that the page should be created in.
 * @param pageSpec The specification for the pages to be created.
 */
void createPage(Page parentPage, Space space, Map pageSpec) throws IllegalStateException {
    def testPageTitle = pageSpec.title as String
    def templateName = pageSpec.templateName as String
    def templateSpaceKey = pageSpec.templateSpaceKey as String
    String content = getTemplateContent(templateName, templateSpaceKey)
    def createdPage = createBasicPage(space, testPageTitle, content)
    linkPages(parentPage, createdPage)

    // Save this page
    pageManager.saveContentEntity(createdPage, DefaultSaveContext.SUPPRESS_NOTIFICATIONS)

    if (!pageManager.getPage(space.getKey(), createdPage.getTitle())) {
        throw new IllegalStateException("Unable to create page ${testPageTitle}")
    } else {
        log.debug("Created page ${testPageTitle} successfully")
    }

    // Build the children
    if (pageSpec.page) {
        pageSpec.page.each { childPageSpec ->
            createPage(createdPage, space, childPageSpec as Map)
        }
    }
}

/**
 * Get the content from the template to populate the page with.
 *
 * @param templateName The name of the template we wish to use.
 * @param templateSpace The space associated with the template we wish to use.
 * @return The template body content.
 */
String getTemplateContent(String templateName, String templateSpaceKey) {
    PageTemplate pageTemplate = templateSpaceKey == "global-template" ?
        pageTemplateManager.getGlobalPageTemplate(templateName) :
        pageTemplateManager.getPageTemplate(templateName, spaceManager.getSpace(templateSpaceKey))
    if (pageTemplate) {
        return pageTemplate.getContent()
    } else {
        throw new IllegalStateException("Unable to retrieve specified template")
    }
}

/**
 * Create a basic page. This is not linked in any hierarchy.
 *
 * @param space The space that this page belongs to.
 * @param title The title of the page we are creating.
 * @param content The content of the page we are creating.
 *
 * @return The create page object.
 */
Page createBasicPage(Space space, String title, String content) {
    def page = new Page()
    def bodyContent = new BodyContent(page, content, BodyType.XHTML)
    page.setVersion(1)
    page.setSpace(space)
    page.setTitle(title)
    page.setBodyContent(bodyContent)
    page.setCreator(AuthenticatedUserThreadLocal.get())

    return page
}

/**
 * Link a parent and a child page together. This method creates a bi-directional relationship between the two pages.
 *
 * @param parent The parent page that we wish to link.
 * @param child The child page that we wish to link.
 */
void linkPages(Page parent, Page child) {
    // Set the parent page on the child
    child.setParentPage(parent)
    // Set the child page on the parent
    parent.addChild(child)
    // Set the ancestors on the child page
    def ancestors = []
    def parentPageAncestors = parent.getAncestors() as List
    if (parentPageAncestors) {
        ancestors.addAll(parentPageAncestors)
    }
    ancestors.add(parent)
    child.setAncestors(ancestors)
}

The custom fragment will define where the button is located and who has visibility of it.

project structure fragment

The condition in the custom script fragment above states that the button will be shown when the space key equals 'PS' The link value should have this format: '<base_url>/rest/scriptrunner/latest/custom/createProjectPages?spaceKey=PS&parentPageId=851970'.

In this case the space key is 'PS' and the parent page has an id of '851970'. You should change the spaceKey and parentPageId values to suite your needs.

Add or remove query parameters as required for your use case.

Limitations: Currently with this implementation we pass the space key via a query parameter. This means that you will need a custom fragment for each space if you want the project structure created in the same space as the button. A possible improvement would be to have the fragment button in a single space, when the fragment button is clicked a new space with the defined page structure is created.

For how-to questions please ask on Atlassian Answers where there is a very active community. Adaptavist staff are also likely to respond there.

Ask a question about ScriptRunner for JIRA, for for Bitbucket Server, or for Confluence.