Applicable: | Jira 7.0.0 and later. |
Level of experience: | Advanced. You should have completed at least one intermediate tutorial before working through this tutorial. See the list of developer tutorials. |
Time estimate: | It should take you approximately 1 hour to complete this tutorial. |
A JQL functions is a powerful tool that enhances Jira's searching functionality. Developers can create their own JQL Functions using JQL function plugin module.
This tutorial shows implementation of a new JQL function called roleMembers
.
This function returns the users that are members of a particular Jira project role.
The first argument
is the name of the role whose members we try to find. It is compulsory. Any other arguments
name the projects whose roles should be checked. When no project is specified, all projects that the
searcher can see are queried. For example, a call to roleMembers(trole, tproj)
will find all the
users in the role trole
for the project tproj
. On the other hand, a call to roleMembers('testrole')
returns all the users in the role testrole
across all projects that the searcher can see.
About these instructions
You can use any supported combination of operating system and IDE to construct this app. These instructions were written using macOS Sierra and IntelliJ IDEA 2017.3. If you use another combination, you should use the equivalent operations for your specific environment.
This tutorial was last tested with Jira 7.10.0 using Atlassian Plugin SDK 6.3.10.
To complete this tutorial, you need to know the following:
We encourage you to work through this tutorial. If you want to skip ahead or check your work when you are finished, you can find the app source code on Atlassian Bitbucket.
To clone the repository, run the following command:
1 2git clone https://bitbucket.org/atlassian_tutorial/role-members-jql-function
Alternatively, you can download the source as a ZIP archive.
In this step, you'll use an atlas
command to generate stub code for your app. The atlas
commands are part of the
Atlassian Plugin SDK and automate much of the work of app development for you.
Set up the Atlassian Plugin SDK and build a project if you did not do that yet.
Open a Terminal and navigate to the directory where you want to keep your app code.
To create an app skeleton, run the following command:
1 2atlas-create-jira-plugin
To identify your app, enter the following information.
group-id |
|
artifact-id |
|
version |
|
package |
|
Navigate to the project directory created in the previous step.
1 2cd role-members-jql-function-tutorial/
Delete the test directories.
Setting up testing for your app isn't part of this tutorial. To delete the generated test skeleton, run the following commands:
1 2rm -rf ./src/test/java rm -rf ./src/test/resources/
Delete the unneeded Java class files.
1 2rm -rf ./src/main/java/com/example/plugins/tutorial/*
Import project in your favorite IDE.
It is a good idea to familiarize yourself with the project configuration file known as the POM (that is, Project Object Model definition file). The POM declares your app's dependencies, build settings, and metadata (information about your app).
Modify the POM as follows:
Navigate to the role-members-jql-function-tutorial
directory created by the SDK.
Open the pom.xml
file.
Add your company or organization name and your website URL to the organization
element:
1 2<organization> <name>Example Company</name> <url>http://www.example.com/</url> </organization>
Update the name
element to something more readable:
1 2<name>Role members JQL function</name>
This is the name of your app that will appear on the Manage Add-ons page in the Jira administration console.
Update the description
element:
1 2<description>This plugin demonstrates how to add a jql function to Atlassian Jira</description>
Save the pom.xml
file.
After you get familiar with JQL function plugin module reference, do the following steps:
Navigate to src/main/resources/
and open the atlassian-plugin.xml
file.
Add the jql-function
module as a child of atlassian-plugin
.
1 2<jql-function key="role-members" i18n-name-key="rolefunc.name" name="Role Members Function" class="com.example.plugins.tutorial.RoleFunction"> <description key="rolefunc.description">JQL function to return the members of a particular role</description> <fname>roleMembers</fname> <list>true</list> </jql-function>
class
attribute identifies handler implementation class com.example.plugins.tutorial.RoleFunction
.fname
element specifies the name of the function that users use in their JQL queries.list
element specifies that our function returns a list of values.Save the file.
When you created the app, the SDK generated an i18n resources file for you. This is where UI text comes from. Add a UI text string to it as follows:
Navigate to src/main/resources
and open the role-members-jql-function-tutorial.properties
resource file.
Add the following property:
1 2rolefunc.bad.num.arguments={0} function takes one or more arguments rolefunc.role.not.exist={0} role does not exist rolefunc.project.not.exist=Project with id or key {0} does not exist rolefunc.name=Role Members function
Save the file.
JqlFunction
implementationNow let's create the JQL function that was referenced in the app descriptor. We're going to make it simple to start with, and build on this class as we go.
Navigate to src/main/java/com/example/plugins/tutorial/
and create a new file named RoleFunction.java
.
Add the following code to the file:
1 2package com.example.plugins.tutorial; import com.atlassian.jira.JiraDataType; import com.atlassian.jira.JiraDataTypes; import com.atlassian.jira.jql.operand.QueryLiteral; import com.atlassian.jira.jql.query.QueryCreationContext; import com.atlassian.jira.plugin.jql.function.AbstractJqlFunction; import com.atlassian.jira.user.ApplicationUser; import com.atlassian.jira.util.MessageSet; import com.atlassian.jira.util.MessageSetImpl; import com.atlassian.plugin.spring.scanner.annotation.component.Scanned; import com.atlassian.query.clause.TerminalClause; import com.atlassian.query.operand.FunctionOperand; import javax.annotation.Nonnull; import java.util.Arrays; import java.util.List; @Scanned public class RoleFunction extends AbstractJqlFunction { @Nonnull @Override public MessageSet validate(ApplicationUser applicationUser, @Nonnull FunctionOperand functionOperand, @Nonnull TerminalClause terminalClause) { return new MessageSetImpl(); } @Nonnull @Override public List<QueryLiteral> getValues(@Nonnull QueryCreationContext queryCreationContext, @Nonnull FunctionOperand functionOperand, @Nonnull TerminalClause terminalClause) { return Collections.emptyList(); } @Override public int getMinimumNumberOfExpectedArguments() { return 1; } @Nonnull @Override public JiraDataType getDataType() { return JiraDataTypes.USER; } }
So far, the initial JQL function code doesn't do a lot. But it forms a good foundation for building upon and it gives us a chance to reflect on some concepts. Notice the methods in the class:
getMinimumNumberOfExpectedArguments()
basically returns the smallest number of arguments
that the function may accept. The value returned from this method must be consistent across method invocations.
We need at least 1 argument that is project role.getDataType()
method is called to determine the type of data the function returns. The
value tells Jira which JQL clauses the function can be expected to work with. In our case we
want to return users.validate()
and getValues()
methods are called to validate JQL function arguments append
get result respectively. Keep in mind that those functions can't return null
.Save the file.
RoleFunction
validationBefore you run query, you need to be sure that arguments passed by user are valid.
In this step, in validate
method, you will check that project role and projects exist. Since you will use
ProjectService,
projects that are not visible for user won't be found.
In the same RoleFunction.java
file, update validate()
method with the following:
1 2@Nonnull @Override public MessageSet validate(ApplicationUser applicationUser, @Nonnull FunctionOperand functionOperand, @Nonnull TerminalClause terminalClause) { MessageSet messages = new MessageSetImpl(); final List<String> arguments = functionOperand.getArgs(); //Make sure we have the correct number of arguments. if (arguments.isEmpty()) { messages.addErrorMessage(getI18n().getText("rolefunc.bad.num.arguments", functionOperand.getName())); return messages; } //Make sure the role is valid. final String requestedRole = arguments.get(0); ProjectRole role = projectRoleManager.getProjectRole(requestedRole); if (role == null) { messages.addErrorMessage(getI18n().getText("rolefunc.role.not.exist", requestedRole)); return messages; } //Make sure the project arguments are valid if provided. if (arguments.size() > 1) { for (String project : arguments.subList(1, arguments.size())) { ProjectService.GetProjectResult result = getProjectResult(project); if (!result.isValid()) { result.getErrorCollection().getErrorMessages().forEach(messages::addErrorMessage); return messages; } } } return messages; }
In the snippet above, we check that:
If validation fails, we add error message to MessageSet
. Validation is only successful if there is
no error message.
To make validation method work, inject ProjectRoleManager
and
ProjectService
using Atlassian Spring Scanner
and implement getProjectResult()
method.
Dependency injection is shown in this example:
1 2@JiraImport private final ProjectRoleManager projectRoleManager; @JiraImport private final ProjectService projectService; public RoleFunction(ProjectRoleManager projectRoleManager, ProjectService projectService) { this.projectRoleManager = projectRoleManager; this.projectService = projectService; }
The getProjectResult()
has to be able to get project by project key or project ID. Here is a simple
implementation:
1 2private ProjectService.GetProjectResult getProjectResult(String project){ ProjectService.GetProjectResult result = null; try { result = projectService.getProjectById(Long.parseLong(project)); } catch (NumberFormatException e){ result = projectService.getProjectByKey(project); } return result; }
Save the file.
getValues
methodIf function passed validation, getValues
method will be invoked. Replace the stub code with following:
1 2@Nonnull @Override public List<QueryLiteral> getValues(@Nonnull QueryCreationContext queryCreationContext, @Nonnull FunctionOperand functionOperand, @Nonnull TerminalClause terminalClause) { final List<String> arguments = functionOperand.getArgs(); //Can't do anything when no argument is specified. This is an error so return empty list. if (arguments.isEmpty()) { return Collections.emptyList(); } final ProjectRole projectRole = projectRoleManager.getProjectRole(arguments.get(0)); //Role not in system, then do nothing. if (projectRole == null) { return Collections.emptyList(); } final Set<ApplicationUser> users = new HashSet<>(); //Projects are specified, then look at those projects. if (arguments.size() > 1) { for (String project : arguments.subList(1, arguments.size())) { ProjectService.GetProjectResult result = getProjectResult(project); users.addAll(projectRoleManager.getProjectRoleActors(projectRole, result.getProject()).getApplicationUsers()); } } else { ServiceOutcome<List<Project>> result = projectService.getAllProjects(queryCreationContext.getApplicationUser()); for (Project project: result.getReturnedValue()) { users.addAll(projectRoleManager.getProjectRoleActors(projectRole, project).getApplicationUsers()); } } //Convert all the users to query literals. final List<QueryLiteral> literals = new ArrayList<>(); for (ApplicationUser user : users) { literals.add(new QueryLiteral(functionOperand, user.getKey())); } return literals; }
We use ProjectRoleManager
to get ProjectRoleActor
s for specified project and project role, and
then convert them to ApplicationUser
.
Save the file.
Let's start Jira and see what we've got so far.
Open a Terminal and navigate to the app root directory where the pom.xml
is located.
Run the following SDK command:
1 2atlas-run
This command downloads and starts Jira with your app installed.
Open the Jira instance in a browser and log in with the default admin/admin.
Create new project, issues, and roles.
Perform a JQl query, that is, assignee IN roleMembers(Administrators, TEST)
Create a new user and make sure that the user doesn't have permissions to browse project.
Log in with new user and try to run the same query. You should see an error in result. Here our validate()
function works.
Enter a name for your handler (it can be anything because we won't save it this time), and then click Next.
Notice the configuration form for this handler.
From here, you can keep Jira running while you continue development of the app. To reload your app, use QuickReload. It reinstalls your app behind the scenes as you work.
To use QuickReload, follow these steps:
atlas-package
command.To make the function truly production ready, your RoleFunction
also needs to implement
ClauseSanitisingJqlFunction.
A saved JQL search (filter) can be shared with multiple users. While this functionality is very useful,
it also allows information to be leaked. For example, let's say you have a filter that contains
assignee in roleMembers(Administrators, Proj)
and you share the filter with Janice who cannot see Proj
.
The search will not return any results, however, Janice will know that a project called Proj
exists even
though she does not have permission to see it. A JQL function that can expose sensitive information should also
implement the optional ClauseSanitisingJqlFunction
interface.
The roleMembers
function should check that the passed user can see all the project arguments. If all
the passed projects are visible, the we can simply return the FunctionOperand
as passed. On the other hand,
if some of the projects are not visible then the sanitizer should return a new FunctionOperand
that
has any names replaced with project IDs. This is not a perfect solution as we still leak the fact that there
exists a project that the searcher cannot see, however, we no longer leak the project name.
To implement the ClauseSanitisingJqlFunction
, use the following code:
1 2public FunctionOperand sanitiseOperand(final ApplicationUser user, @Nonnull final FunctionOperand functionOperand) { final List<String> arguments = functionOperand.getArgs(); //We only sanitise projects, so just return the original function if there are no projects. if (arguments.size() <= 1) { return functionOperand; } boolean argChanged = false; final List<String> newArgs = new ArrayList<>(arguments.size()); newArgs.add(arguments.get(0)); for (final String argument : arguments.subList(1, arguments.size())) { final Project project = projectManager.getProjectObjByKey(argument); if (project != null && !permissionManager.hasPermission(ProjectPermissions.BROWSE_PROJECTS, project, user)) { newArgs.add(project.getId().toString()); argChanged = true; } else { newArgs.add(argument); } } if (argChanged) { return new FunctionOperand(functionOperand.getName(), newArgs); } else { return functionOperand; } }
The ProjectManager
that we used to get projects doesn't perform permission check and returns all
projects that match query. However we use PermissionManager
to check if user has Browse
permission
and replace project name with ID. Use constructor injection for ProjectManager
and PermissionManager
was it shown previously.
Save the file.
Go back to browser and log in to Jira as admin.
Run a JQL query as previously, save the filter, and then share it with logged-in-users
.
Log in as your test user without permissions.
Find filter you just created and run it.
Sanitizer replaces project key with project ID.
The roleMembers
function should probably perform some permissions checks on the project role argument.
If such a check was implemented, then the sanitizer would probably have to change to reflect any logic here.
Congratulations, that's it!
Have a treat!
Rate this page: