HTTPS SSH

Play Scala Module for Atlassian Connect

Description

This is a Play module to help develop Atlassian Connect add-ons with Play Framework in Scala.
Here are a few of the useful features it brings to add-on developers:

Add-on descriptor placeholders

This module no longer provides an add-on descriptor template as in the original version that uses XML.
You just need to create the atlassian-connect.json file under the /app directory.
You can have a few placeholders in the atlassian-connect.json file that this module will fill in for you:

  • ${addonKey} will be replaced with what you define for key ac.key in the configuration.
  • ${addonName} will be replaced with what you define for key ac.name in the configuration.
  • ${addonVersion} will be replaced with what you define for key ac.version in the configuration.
  • ${localBaseUrl} will be replaced with what you define for key application.baseUrl in the configuration.
    Please note that this should be different in each deployment environment, and it's highly recommend that you use the
    https protocol in production. If not defined, http://localhost:9000 is assumed.

You should also define the registration webhook to the path /installed if you are relying on the default ac.routes,
so that installation events of the add-on in host application are automatically handled.
If you would rather use a different path, define a POST method for it your registration webhook path and map it to
com.atlassian.connect.playscala.controllers.AcController.registration() or your own registration controller methods
if you need to use your own implementation.

Here is a sample of atlassian-connect.json file that you can build on top of it:

{
    "key": "${addonKey}",
    "name": "${addonName}",
    "version": "${addonVersion}",
    "description": "An Add-on that performs awesome magics!",
    "vendor": {
        "name": "ACME",
        "url": "http://www.acme.com"
    },
    "baseUrl": "${localBaseUrl}",
    "authentication": {
        "type": "jwt"
    },
    "enableLicensing": false,
    "lifecycle": {
        "installed": "/installed"
    },
    "modules": {
        "webPanels": [{
            "key" : "awesome-addon-webpanel-1",
            "name": {
                "value": "Awesomeness can be found here!"
            },
            "url": "/awesome?issue_key={issue.key}",
            "location": "atl.jira.view.issue.right.context"
        }]
    },
    "scopes": ["READ"]
}

Validates incoming JWT request

You can ensure incoming requests are valid and coming from a known trusted host application, by wrapping your block of
code as a function that requires a Token and passing it to the jwtValidated or jwtValidatedAsync method,
which are available by mixing in the ActionJwtValidator trait. This also...

Enables multi-tenancy

The block of code wrapped in jwtValidated or jwtValidatedAsync takes a Token parameter, which allow you to identify
the host application the request is coming from, and also the user on whose behalf the request is made, if there is one.

For multi-tenancy, the important thing is to identify the key of the host application available from the AcHostModel
that can be retrieved from the Token, and of course keep track of the current user, if a user has logged into the host
application.

Make calls back to the host application

Play comes with a nice library to make HTTP requests to any host or URL. This module provides a shortcut to make HTTP
requests back to the host application, using that same API. Simply start your calls with AcHostWS#uri instead of WS#url.
This gives you:

  • relative URL handling and user identification. It knows about the current host application and the current user, if
    there is one, you're working with in the context of the current request, as the Token that keeps track of the current
    host and user is passed in implicitly. Passing in an absolute URL will result in an IllegalArgumentException.
  • default timeout. You never know what might be going on the network, never make an HTTP request without a timeout.

If you need to make JWT-signed calls back to the host application, mix in JwtSignedAcHostWS and execute your calls with
signedGet() and signedPost() etc instead of get() and post().

Using the product REST API

Certain REST URLs may require additional permissions that should be added to your atlassian-plugin.json file.

Jira Permissions

Confluence Permissions

For example, to view details of a specific jira issue.

AcHostWS.url(s"/rest/api/2/issue/${issueKey}").signedGet();

You also need to add the required scopes to your atlassian-connect.json file:

    "scopes": ["READ"]

Easy integration of AUI

Include AUI easily in your HTML pages using the template provided by the modules @ac.aui.styles() and @ac.aui.scripts().
You can even choose the version you'd like to use @ac.aui.styles("5.2-m1") and @ac.aui.scripts("5.2-m1")
(and make sure to use the same version in each). For the best results, put @ac.aui.styles() in the head of your HTML
and @ac.aui.scripts() at the end of the body (but before your own scripts).

Getting started

Create your Scala Play application

You should find everything you need on the Play website. Once you have your Play application up and running, go to
the next step:

Your Atlassian Connect Key and Name

Add the following to conf/application.conf:

ac.key=your-ac-key
ac.name=your-ac-name
application.baseUrl=http://localhost:9000

Note that application.baseUrl should be changed to match the port you are using in local development environment, and
you should have a production configuration that overwrites it with the base URL of your atlassian-connect add-on app.

Add Atlassian's public maven repository in your build.sbt file

This is called a resolver in SBT world. SBT is the build tool, based on Scala, used by Play.

resolvers ++= Seq(
  Resolver.defaultLocal,
  "atlassian-proxy-public" at "https://m2proxy.atlassian.com/content/groups/public/",
  "atlassian-maven-public" at "https://maven.atlassian.com/content/groups/public/",
  Classpaths.typesafeReleases,
  DefaultMavenRepository,
  Resolver.sonatypeRepo("releases"),
  Resolver.mavenLocal
)

Note that we actually also add our local maven repository for good measure and ease of use.

Add the module as a dependency in your build.sbt file

val acPlayScalaVersion = "<version>"

val appDependencies = Seq(
  "com.atlassian.connect" %% "ac-play-scala" % acPlayScalaVersion,
  "com.atlassian.connect" %% "ac-play-scala" % acPlayScalaVersion classifier "assets", // required from 0.4.0 or above
  // your other dependencies go here,
)

libraryDependencies ++= appDependencies

Where <version> is the current version of this module.

Add the module's routes to your conf/routes configuration

Comment the default application index and add the module's routes:

# Home page
# GET     /                           controllers.Application.index()
->      /                                   ac.Routes

This will ensure that any routes that are not handled by your application are delegated to this helper module.

Configure the database

If you haven't already configured a database, in your conf/application.conf setup the default database. Here is an example
using a local PostgresSQL installation:

db.default.driver=org.postgresql.Driver
db.default.url="jdbc:postgresql:my-database"
db.default.user=my-user
db.default.password=my-password
db.default.partitionCount=1
db.default.maxConnectionsPerPartition=5
db.default.minConnectionsPerPartition=1
db.default.acquireIncrement=1
db.default.acquireRetryAttempts=1
db.default.acquireRetryDelay=5 seconds

Alternatively, you can configure a H2 database, either in-memory mode or not:

db.default.driver=org.h2.Driver
db.default.url="jdbc:h2:mem:play"
db.default.user=sa
db.default.password=""

Note that any data saved the H2 database running with in-memory mode will lost when Play restarts. You can change the
database url to open a database in a specified directory:

db.default.url="jdbc:h2:file:h2/play"

You can even tell H2 to mimic a particular database by adding a parameter to the database url, such as:

db.default.url="jdbc:h2:mem:play;MODE=PostgreSQL"

Note that the Play framework comes with H2 and the PostgresSQL driver is already a dependency of this module, so you don't
need to add a dependency for either.

This module uses Slick for persistence of the AcHostModel. You also need to define the Slick driver for the database you choose:

db.default.slick.driver=scala.slick.driver.PostgresDriver

or

db.default.slick.driver=scala.slick.driver.H2Driver

For other SQL database you choose, look up Slick documentation to find out the name of the driver.

To create the ac_host database table, define a Global object that extends AcGlobalSettings, then the table will
be created if it does not exist.

Alternatively, you could create it using Play's database evolutions.

In your code where you need to access the ac_host database table, such as extending/mixing in AcController, ActionJwtValidator
or PageTokenValidator, you will need to mix in SlickDbConfiguration.

You can read more about some of those topics on the Play website:

Using NoSQL database

If you are using NoSQL database, you will need to implement DbConfiguration that references your own implementation of
AcHostModelStore.

Then you will need to mix in your DbConfiguration implementation wherever you would have to mix in SlickDbConfiguration
mentioned above. Alternatively, you can mix in DefaultDbConfiguration, which will discover the implementation at runtime
by looking up the provider.com.atlassian.connect.playscala.store.DbConfiguration configuration item, or default to
SlickDbConfiguration if the configuration item is not defined.

You can have a look at Extensions for Atlassian Connect Play Scala Module
and see if there is an extension that already implements DbConfiguration of the database you are using.

You can also check out the nosql-demo branch of Who's Looking Connect Scala to see how to
use the RedisAcHostStore extension.

You're done with the database configuration.

Reload

Now you're ready to reload your application. If you're running the Play console you will need
to run reload for the new dependencies, resolvers, etc. to take effect.
Then you can refresh the home page of your application.

If all went well, you should now see the welcome page of the Atlassian Connect Play Module:

The Atlassian Connect Play Module home page

A note on Security

Most requests to the remote Play application will be properly signed with JWT headers coming from the Atlassian application.
This Play module will take care of authenticating these requests. However any subsequent requests within the original page will require
more work to authenticate remotely.

For example a remote admin page may include a horizontal navbar including links to various other remote admin pages. When a user clicks
on any of these links they will load within the iframe without any additional JWT headers being sent to the remote server. To overcome
this, this module provides a secure token mechanism. If you use @ac.page to decorate your pages all links, forms and ajax requests will
automatically be decorated with this secure token. If @ac.page is not used any requests will have to be decorated manually. This can be
done by adding the following request parameters:

?acpt=<SECURE_TOKEN>

For ajax requests one can also add the following header to the request:

X-acpt:<SECURE_TOKEN>

The secure token must be passed in as a parameter to @ac.page, which means that your pages will also need to define it
as a parameter, typically as an implicit parameter so that it can be passed to @ac.page implicitly.

On the server side to verify that an action in your Play controller is being called with a valid token, you can simply
wrap your block of code as a function that takes a Token and pass it to the PageTokenValidated method, which is
available by mixing in the PageTokenValidator trait. Tokens contain a timestamp and will time out once they are older
than 15 minutes (configurable via ac.token.expiry.secs in application.conf).
Any response from a PageTokenValidated block will contain a fresh token in the 'X-acpt' response header.
If @ac.page is used, this will trigger tokens to be refreshed client-side automatically; however if @ac.page is not
used this may have to be done manually.

For AJAX call, you can invoke a withTokenHeader to return a refreshed token
to the AJAX call client:

PageTokenValidated {implicit token =>
  ...
  Ok(result).withTokenHeader
}

Sample Add-ons

Here are some Atlassian Connect add-ons built with Play Scala Module: