diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7d22ad0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/.settings +/.vscode +/target +/.classpath +/.project \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a716fd8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2018, ASERVO Software GmbH +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of ASERVO Software GmbH nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README.md b/README.md index 195f0dc..36ea031 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,280 @@ -# jira-rest-extender +[![OCTOPUS CodeWare](https://www.tiburon.su/files/logo_octo_text_600_153.png)](https://www.tiburon.su) +REST API Extender for JIRA +================ + +REST API for automated JIRA configuration with URMS + +Related Documentation +--------------------- + +* [Atlassian REST API design guidelines version 1](https://developer.atlassian.com/server/framework/atlassian-sdk/atlassian-rest-api-design-guidelines-version-1/) + +Resources +--------- + +All resources produce JSON (media type: `application/json`) results. + +### Settings + +Access important JIRA settings like the title, the base url, the mode +etc. + +* #### `GET /rest/extender/1/settings` + + Get JIRA application settings. + + __Responses__ + + ![Status 200][status-200] + + ```javascript + { + "baseurl": "http://localhost:2990/jira", + "mode": "private", + "title": "Your Company JIRA" + } + ``` + + ![Status 401][status-401] + + Returned if the current user is not authenticated. + + ![Status 403][status-403] + + Returned if the current user is not an administrator. + +* #### `PUT /rest/extender/1/settings` + + Set JIRA application settings. + + __Request Body__ + + Media type: `application/json` + + Content: Settings, for example: + + ```javascript + { + "baseurl": "http://localhost:2990/jira", + "mode": "private", + "title": "Your Company JIRA" + } + ``` + + __Request Parameters__ + + None. + + __Responses__ + + ![Status 200][status-200] + + Returned if request could be executed without major exceptions. + + The response will contain a list of errors that occurred while setting + some specific values such as a string that was too long, for example: + + ``` + { + "errorMessages": [ + "The length of the application title must not exceed 255 characters" + ], + "errors": {} + } + ``` + + ![Status 401][status-401] + + Returned if the current user is not authenticated. + + ![Status 403][status-403] + + Returned if the current user is not an administrator. + +### Licenses + +The JIRA license API is a bit weird and needs to be well understood. +Just like in the web interface, different license keys can theoretically +be set for each application (JIRA Core, JIRA Software, etc.). However, +the entered license key is always set for all applications for which it +is valid. + +For example: + +1. JIRA Core and JIRA Software each have their own license key. If a +license key is now entered (in the web interface or via the REST API) +with which both applications can be licensed, this license key is stored +for both applications. + +2. JIRA Core and JIRA Software use a common license key. If a license +key is now entered (in the web interface or via the REST API) with which +only JIRA Core can be licensed, the license key is also only stored for +JIRA Core. + +So again, an entered license key is always stored for all applications +for which it is valid. The web interface might suggest that you can +select the desired application, but this is not true. + +* #### `GET /rest/extender/1/licenses` + + Get the license keys together with the application keys of the + applications using this license key. + + __Responses__ + + ![Status 200][status-200] + + ```javascript + { + "licenses": [ + { + "key": "AAA...", + "applicationKeys": [ + "jira-software" + ] + }, + { + "key": "AAA...", + "applicationKeys": [ + "jira-core", + "jira-servicedesk" + ] + } + ] + } + ``` + + ![Status 401][status-401] + + Returned if the current user is not authenticated. + + ![Status 403][status-403] + + Returned if the current user is not an administrator. + +* #### `PUT /rest/extender/1/licenses` + + Set a license by its license key. + + __Request Body__ + + Media type: `text/plain` + + Content: License key, for example: + + ``` + AAA... + ``` + + __Request Parameters__ + + | parameter | type | description | + | ----------- | --------- | ------------------------------------------------------------------------------ | + | `clear` | _boolean_ | Clear all licenses before setting the new license, optional, defaults to false | + + __Responses__ + + ![Status 200][status-200] + + ```javascript + { + "key": "AAA...", + "applicationKeys": [ + "jira-core", + "jira-servicedesk", + "jira-software" + ] + } + ``` + + ![Status 401][status-401] + + Returned if the current user is not authenticated. + + ![Status 403][status-403] + + Returned if the current user is not an administrator. + +### Syncronise Crowd User Directory + +* #### `GET /rest/extender/1/directory` + + Get info about User Directories. + + __Responses__ + + ![Status 200][status-200] + + ```javascript + [ + { + "id": 1, + "name": "Jira Internal Directory", + "active": true, + "createdDate": 1362038271308, + "updatedDate": 1362038271308, + "lowerName": "jira internal directory", + "description": "Jira default internal directory", + "type": "INTERNAL", + "implementationClass": "com.atlassian.crowd.directory.InternalDirectory", + "lowerImplementationClass": "com.atlassian.crowd.directory.internaldirectory", + "allowedOperations":["CREATE_GROUP", "CREATE_ROLE", "CREATE_USER", "DELETE_GROUP", "DELETE_ROLE", "DELETE_USER", "UPDATE_GROUP",…], + "attributes":{ + "user_encryption_method": "atlassian-security" + }, + "empty": false, + "keys":["user_encryption_method"], + "encryptionType": "atlassian-security" + } + ] + ``` + + ![Status 401][status-401] + + Returned if the current user is not authenticated. + + ![Status 403][status-403] + + Returned if the current user is not an administrator. + +* #### `PUT /rest/extender/1/directory` + + Start sycronyse User Directory. + + __Request Parameters__ + + | parameter | type | description | + | --------- | ------ | ----------------------------------------------------- | + | `id` | _long_ | id of the User Directory, which should be syncronised | + + __Responses__ + + ![Status 200][status-200] + + Returned if request could be executed without major exceptions. + + The response will contain a list of errors that occurred while setting + some specific values such as a string that was too long, for example: + + ``` + { + "message": "parameter should not be null!", + "status-code": 500, + "stack-trace": full_stack_trace_of_the_application + } + ``` + ![Status 401][status-401] + + Returned if the current user is not authenticated. + + ![Status 403][status-403] + + Returned if the current user is not an administrator. + + +[status-200]: https://img.shields.io/badge/status-200-brightgreen.svg +[status-400]: https://img.shields.io/badge/status-400-red.svg +[status-401]: https://img.shields.io/badge/status-401-red.svg +[status-403]: https://img.shields.io/badge/status-403-red.svg +[status-404]: https://img.shields.io/badge/status-404-red.svg diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..fadeea6 --- /dev/null +++ b/pom.xml @@ -0,0 +1,533 @@ + + + + 4.0.0 + + su.tiburon.atlassian.jira + rest-extender + 1.1.0 + atlassian-plugin + + REST API extender for JIRA + REST API extender for JIRA. + + + + BSD 3-Clause License + https://opensource.org/licenses/BSD-3-Clause + + + + + Github + https://github.com/tiburon-777/rest-extender/issues + + + + scm:git:git://github.com/tiburon-777/rest-extender.git + scm:git:git@github.com:tiburon-777/rest-extender.git + https://github.com/tiburon-777/rest-extender + + + + OCTOPUS CodeWare + http://www.tiburon.su/ + + + + + Andrey Ivanov + ya@tiburon.su + OCTOPUS CodeWare + https://www.tiburon.su + + + + + + 1.8 + 1.8 + UTF-8 + UTF-8 + + + 8.0.1 + + 7.5.0 + 8.0.2 + 8.0.2 + 6.2.0 + 3.1.0 + 5.0.1 + 2.0.0 + 2.1.5 + 2.10.2-rc04 + 2.15.3 + + + 1.7.9 + 2.4 + 1.1.1 + 2.1 + + + 4.12 + 1.10.19 + 0.8.1 + 4.3.0 + + + + + + + com.atlassian.jira + jira-api + ${jira.version} + provided + + + jta + jta + + + + + + com.atlassian.jira + jira-core + ${jira.version} + provided + + + jndi + jndi + + + jta + jta + + + + + + com.atlassian.jira.plugins + project-templates-api + ${atlassian.project.templates.version} + provided + + + + com.atlassian.jira + jira-rest-api + ${jira.version} + provided + + + + com.atlassian.plugins.rest + atlassian-rest-common + ${atlassian.plugins.rest.version} + provided + + + + com.atlassian.plugins.rest + atlassian-rest-doclet + ${atlassian.plugins.rest.version} + provided + + + + com.atlassian.plugin + atlassian-spring-scanner-annotation + ${atlassian.spring.scanner.version} + provided + + + + com.atlassian.templaterenderer + atlassian-template-renderer-api + 3.0.0 + provided + + + + com.atlassian.sal + sal-api + ${atlassian.sal.version} + provided + + + + com.atlassian.crowd + crowd-api + ${atlassian.crowdapi.version} + provided + + + + com.atlassian.upm + plugin-license-storage-lib + ${upm.license.compatibility.version} + + + + com.atlassian.upm + plugin-license-storage-plugin + ${upm.license.compatibility.version} + provided + + + + com.atlassian.upm + licensing-api + ${upm.license.compatibility.version} + provided + + + + com.atlassian.upm + upm-api + ${upm.license.compatibility.version} + provided + + + + + + org.slf4j + slf4j-api + ${slf4j.version} + provided + + + + javax.ws.rs + jsr311-api + ${javax.ws.version} + provided + + + + javax.xml.bind + jaxb-api + ${javax.xml.version} + provided + + + + + + junit + junit + ${junit.version} + test + + + + org.mockito + mockito-core + ${mockito.version} + test + + + + com.atlassian.jira + jira-tests + ${jira.version} + test + + + + + + + com.atlassian.maven.plugins + jira-maven-plugin + ${atlassian.amps.version} + true + + ${jira.version} + false + false + false + + true + true + false + + false + + + + jira-software + ${jira.version} + + + + + com.atlassian.labs.plugins + quickreload + ${atlassian.quick.reload.version} + + + + ${project.groupId}.${project.artifactId} + * + + org.springframework.*;resolution:="optional", + org.eclipse.gemini.*;resolution:="optional", + sun.misc;resolution:=optional, + * + + + + + + crowd + crowd + 3.1.3 + + + confluence + confluence + 6.8.0 + + + + + + + com.atlassian.plugin + atlassian-spring-scanner-maven-plugin + ${atlassian.spring.scanner.version} + + + + atlassian-spring-scanner + + process-classes + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + prepare-agent + + prepare-agent + + + + report + test + + report + + + + + + + org.eluder.coveralls + coveralls-maven-plugin + ${coveralls.version} + + + + + + + sonatype-nexus-snapshots + Sonatype Nexus snapshot repository + https://oss.sonatype.org/content/repositories/snapshots + + + + sonatype-nexus-staging + Sonatype Nexus release staging repository + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + + release + + true + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + sign-artifacts + deploy + + sign + + + + + ${gpg.keyname} + ${gpg.passphrase} + ${gpg.executable} + + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.8.2 + + + true + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.8 + true + + + default-deploy + deploy + + deploy + + + + + sonatype-nexus-staging + https://oss.sonatype.org/ + true + + + + + + + + sources + + true + + + + + org.apache.maven.plugins + maven-source-plugin + 3.0.1 + + + attach-sources + install + + jar-no-fork + + + + + + + + + + javadoc + + true + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.0.1 + + + attach-javadocs + install + + jar + + + + + + + + + + + + atlassian-public + https://packages.atlassian.com/maven/repository/public + + true + never + warn + + + true + warn + + + + + sonatype-oss + https://oss.sonatype.org/content/groups/public + + true + never + warn + + + true + warn + + + + + + + atlassian-public + https://packages.atlassian.com/maven/repository/public + + true + never + warn + + + true + warn + + + + + sonatype-oss + https://oss.sonatype.org/content/groups/public + + true + never + warn + + + true + warn + + + + + diff --git a/src/main/java/su/tiburon/atlassian/jira/JiraApplicationHelper.java b/src/main/java/su/tiburon/atlassian/jira/JiraApplicationHelper.java new file mode 100644 index 0000000..8993c67 --- /dev/null +++ b/src/main/java/su/tiburon/atlassian/jira/JiraApplicationHelper.java @@ -0,0 +1,160 @@ +package su.tiburon.atlassian.jira; + +import com.atlassian.jira.bc.license.JiraLicenseService; +import com.atlassian.jira.bc.license.JiraLicenseService.ValidationResult; +import com.atlassian.jira.config.properties.APKeys; +import com.atlassian.jira.config.properties.ApplicationProperties; +import com.atlassian.jira.license.JiraLicenseManager; +import com.atlassian.jira.license.LicenseDetails; +import com.atlassian.jira.util.UrlValidator; +import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; +import com.google.common.collect.Lists; +import com.opensymphony.util.TextUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; +import com.atlassian.crowd.embedded.api.CrowdDirectoryService; +import com.atlassian.crowd.embedded.api.Directory; + +import javax.annotation.Nonnull; +import javax.inject.Inject; +import java.util.Collection; +import java.util.List; + +@Component +public class JiraApplicationHelper { + + @ComponentImport + private final ApplicationProperties applicationProperties; + + private final JiraI18nHelper i18nHelper; + + @ComponentImport + private final JiraLicenseManager licenseManager; + + @ComponentImport + private final JiraLicenseService licenseService; + + @ComponentImport + private final CrowdDirectoryService crowdDirectoryService; + + /** + * Constructor. + * + * @param applicationProperties injected {@link ApplicationProperties} + * @param i18nHelper injected {@link JiraI18nHelper} + * @param licenseManager injected {@link JiraLicenseManager} + * @param licenseService injected {@link JiraLicenseService} + * @param crowdDirectoryService injected {@link CrowdDirectoryService} + */ + @Inject + public JiraApplicationHelper( + final ApplicationProperties applicationProperties, + final JiraI18nHelper i18nHelper, + final JiraLicenseManager licenseManager, + final JiraLicenseService licenseService, + final CrowdDirectoryService crowdDirectoryService) { + + this.applicationProperties = applicationProperties; + this.i18nHelper = i18nHelper; + this.licenseManager = licenseManager; + this.licenseService = licenseService; + this.crowdDirectoryService = crowdDirectoryService; + } + + + public void syncDir(@Nonnull final Long dirId ) { + crowdDirectoryService.synchroniseDirectory(dirId); + } + + public List getDir() { + return crowdDirectoryService.findAllDirectories(); + } + + public String getBaseUrl() { + return applicationProperties.getString(APKeys.JIRA_BASEURL); + } + + public void setBaseUrl( + @Nonnull final String baseUrl) { + + if (!UrlValidator.isValid(baseUrl)) { + throw new IllegalArgumentException(i18nHelper.getText("admin.errors.you.must.set.a.valid.base.url")); + } + + applicationProperties.setString(APKeys.JIRA_BASEURL, baseUrl); + } + + public String getMode() { + return applicationProperties.getString(APKeys.JIRA_MODE); + } + + public void setMode( + @Nonnull final String mode) { + + if (!mode.equalsIgnoreCase("public") && !mode.equalsIgnoreCase("private")) { + throw new IllegalArgumentException("Invalid mode"); + } + + if (mode.equalsIgnoreCase("public") && hasExternalUserManagement()) { + throw new IllegalArgumentException(i18nHelper.getText("admin.errors.invalid.mode.externalUM.combination")); + } + + applicationProperties.setString(APKeys.JIRA_MODE, mode); + } + + public String getTitle() { + return applicationProperties.getString(APKeys.JIRA_TITLE); + } + + public void setTitle( + @Nonnull final String title) { + + if (!TextUtils.stringSet(title)) { + throw new IllegalArgumentException(i18nHelper.getText("admin.errors.you.must.set.an.application.title")); + } + + if (StringUtils.length(title) > 255) { + throw new IllegalArgumentException(i18nHelper.getText("admin.errors.invalid.length.of.an.application.title")); + } + + applicationProperties.setString(APKeys.JIRA_TITLE, title); + } + + private boolean hasExternalUserManagement() { + return applicationProperties.getOption(APKeys.JIRA_OPTION_USER_EXTERNALMGT); + } + + /** + * Get all licenses. + * + * @return licenses details + */ + public Collection getLicenses() { + return Lists.newArrayList(licenseManager.getLicenses()); + } + + /** + * Set a new license key and clear all licenses before if wanted. + * + * @param key the license key + * @param clear whether to remove all licenses before setting the new license + * @return license details + */ + public LicenseDetails setLicense( + final String key, + boolean clear) { + + final ValidationResult validationResult = licenseService.validate(i18nHelper.getI18nHelper(), key); + + if (validationResult.getErrorCollection().hasAnyErrors()) { + throw new IllegalArgumentException("Specified license was invalid."); + } + + final String licenseString = validationResult.getLicenseString(); + + return clear + ? licenseManager.clearAndSetLicenseNoEvent(licenseString) + : licenseManager.setLicenseNoEvent(licenseString); + } + +} diff --git a/src/main/java/su/tiburon/atlassian/jira/JiraI18nHelper.java b/src/main/java/su/tiburon/atlassian/jira/JiraI18nHelper.java new file mode 100644 index 0000000..fa76961 --- /dev/null +++ b/src/main/java/su/tiburon/atlassian/jira/JiraI18nHelper.java @@ -0,0 +1,59 @@ +package su.tiburon.atlassian.jira; + +import com.atlassian.jira.user.preferences.PreferenceKeys; +import com.atlassian.jira.util.I18nHelper; +import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; +import com.opensymphony.module.propertyset.PropertySet; +import org.springframework.stereotype.Component; + +import javax.inject.Inject; +import java.util.Locale; + +@Component +public class JiraI18nHelper { + + @ComponentImport + private final I18nHelper.BeanFactory i18nBeanFactory; + + private final JiraUserHelper userHelper; + + /** + * Constructor. + * + * @param i18nBeanFactory injected {@link I18nHelper.BeanFactory} + * @param userHelper injected {@link JiraUserHelper} + */ + @Inject + public JiraI18nHelper( + final I18nHelper.BeanFactory i18nBeanFactory, + final JiraUserHelper userHelper) { + + this.i18nBeanFactory = i18nBeanFactory; + this.userHelper = userHelper; + } + + public String getText( + final String key) { + + return getI18nHelper().getText(key); + } + + public I18nHelper getI18nHelper() { + return i18nBeanFactory.getInstance(getLocale()); + } + + Locale getLocale() { + final PropertySet userProperties = userHelper.getUserProperties(); + + if (userProperties != null) { + final String locale = userProperties.getString(PreferenceKeys.USER_LOCALE); + + if (locale != null) { + return new Locale(locale); + } + } + + return Locale.getDefault(); + } + +} diff --git a/src/main/java/su/tiburon/atlassian/jira/JiraUserHelper.java b/src/main/java/su/tiburon/atlassian/jira/JiraUserHelper.java new file mode 100644 index 0000000..ce01c97 --- /dev/null +++ b/src/main/java/su/tiburon/atlassian/jira/JiraUserHelper.java @@ -0,0 +1,75 @@ +package su.tiburon.atlassian.jira; + +import com.atlassian.jira.security.JiraAuthenticationContext; +import com.atlassian.jira.user.ApplicationUser; +import com.atlassian.jira.user.UserPropertyManager; +import com.atlassian.jira.user.preferences.UserPreferencesManager; +import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; +import com.opensymphony.module.propertyset.PropertySet; +import org.springframework.stereotype.Component; + +import javax.annotation.Nullable; +import javax.inject.Inject; + +@Component +public class JiraUserHelper { + + @ComponentImport + private final JiraAuthenticationContext authenticationContext; + + @ComponentImport + private final UserPropertyManager userPropertyManager; + + /** + * Constructor. + * + * @param authenticationContext injected {@link JiraAuthenticationContext} + * @param userPropertyManager injected {@link UserPreferencesManager} + */ + @Inject + public JiraUserHelper( + final JiraAuthenticationContext authenticationContext, + final UserPropertyManager userPropertyManager) { + + this.authenticationContext = authenticationContext; + this.userPropertyManager = userPropertyManager; + } + + /** + * Get logged in user. + * + * @return user + */ + @Nullable + public ApplicationUser getLoggedInUser() { + return authenticationContext.getLoggedInUser(); + } + + /** + * Get the property set of the logged in user. + * + * @return property set + */ + @Nullable + public PropertySet getUserProperties() { + return getUserProperties(getLoggedInUser()); + } + + /** + * Get the property set of the given user. + * + * @param user the application user + * @return property set + */ + @Nullable + public PropertySet getUserProperties( + @Nullable final ApplicationUser user) { + + if (user != null) { + return userPropertyManager.getPropertySet(user); + } + + return null; + } + +} diff --git a/src/main/java/su/tiburon/atlassian/jira/JiraWebAuthenticationHelper.java b/src/main/java/su/tiburon/atlassian/jira/JiraWebAuthenticationHelper.java new file mode 100644 index 0000000..d146235 --- /dev/null +++ b/src/main/java/su/tiburon/atlassian/jira/JiraWebAuthenticationHelper.java @@ -0,0 +1,48 @@ +package su.tiburon.atlassian.jira; + +import com.atlassian.jira.security.GlobalPermissionManager; +import com.atlassian.jira.user.ApplicationUser; +import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; +import org.springframework.stereotype.Component; + +import javax.inject.Inject; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; + +import static com.atlassian.jira.permission.GlobalPermissionKey.SYSTEM_ADMIN; + +@Component +public class JiraWebAuthenticationHelper { + + @ComponentImport + private final GlobalPermissionManager globalPermissionManager; + + private final JiraUserHelper userHelper; + + /** + * Constructor. + * + * @param userHelper the injected {@link JiraUserHelper} + * @param globalPermissionManager the injected {@link GlobalPermissionManager} + */ + @Inject + public JiraWebAuthenticationHelper( + final GlobalPermissionManager globalPermissionManager, + final JiraUserHelper userHelper) { + + this.globalPermissionManager = globalPermissionManager; + this.userHelper = userHelper; + } + + public void mustBeSysAdmin() { + final ApplicationUser user = userHelper.getLoggedInUser(); + if (user == null) { + throw new WebApplicationException(Response.Status.UNAUTHORIZED); + } + + final boolean isSysAdmin = globalPermissionManager.hasPermission(SYSTEM_ADMIN, user); + if (!isSysAdmin) { + throw new WebApplicationException(Response.Status.FORBIDDEN); + } + } +} diff --git a/src/main/java/su/tiburon/atlassian/jira/bean/LicenseBean.java b/src/main/java/su/tiburon/atlassian/jira/bean/LicenseBean.java new file mode 100644 index 0000000..1de2f62 --- /dev/null +++ b/src/main/java/su/tiburon/atlassian/jira/bean/LicenseBean.java @@ -0,0 +1,75 @@ +package su.tiburon.atlassian.jira.bean; + +import com.atlassian.application.api.ApplicationKey; +import com.atlassian.jira.license.LicenseDetails; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Collection; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Bean for {@link com.atlassian.jira.license.LicenseDetails} results in REST responses. + */ +@XmlRootElement(name = "license") +public class LicenseBean { + + @XmlElement + private final String key; + + @XmlElement + private final Collection applicationKeys; + + private LicenseBean( + final String key, + final Collection applicationKeys) { + + this.key = key; + this.applicationKeys = applicationKeys; + } + + public String getKey() { + return key; + } + + public Collection getApplicationKeys() { + return applicationKeys.stream() + .map(ApplicationKey::valueOf) + .collect(Collectors.toSet()); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof LicenseBean) { + LicenseBean other = (LicenseBean) obj; + + return Objects.equals(key, other.key) + && Objects.equals(applicationKeys, other.applicationKeys); + } + + return false; + } + + @Override + public int hashCode() { + return Objects.hash(key, applicationKeys); + } + + /** + * Factory method for creating a bean from {@link LicenseDetails}. + * + * @param licenseDetail the license details + */ + public static LicenseBean from( + final LicenseDetails licenseDetail) { + + final String key = licenseDetail.getLicenseString(); + final Collection applicationKeys = licenseDetail.getLicensedApplications().getKeys().stream() + .map(ApplicationKey::value) + .collect(Collectors.toSet()); + + return new LicenseBean(key, applicationKeys); + } + +} \ No newline at end of file diff --git a/src/main/java/su/tiburon/atlassian/jira/bean/LicensesBean.java b/src/main/java/su/tiburon/atlassian/jira/bean/LicensesBean.java new file mode 100644 index 0000000..439face --- /dev/null +++ b/src/main/java/su/tiburon/atlassian/jira/bean/LicensesBean.java @@ -0,0 +1,44 @@ +package su.tiburon.atlassian.jira.bean; + +import com.atlassian.jira.license.LicenseDetails; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Collection; +import java.util.stream.Collectors; + +/** + * Bean for collection of {@link LicenseDetails} results in REST responses. + */ +@XmlRootElement(name = "licenses") +public class LicensesBean { + + @XmlElement + private final Collection licenses; + + private LicensesBean( + final Collection licenses) { + + this.licenses = licenses; + } + + public Collection getLicenses() { + return licenses; + } + + /** + * Factory method for creating a bean from a collection of {@link LicenseDetails}. + * + * @param licenseDetails the license details + */ + public static LicensesBean from( + final Collection licenseDetails) { + + final Collection licenses = licenseDetails.stream() + .map(LicenseBean::from) + .collect(Collectors.toList()); + + return new LicensesBean(licenses); + } + +} \ No newline at end of file diff --git a/src/main/java/su/tiburon/atlassian/jira/bean/SettingsBean.java b/src/main/java/su/tiburon/atlassian/jira/bean/SettingsBean.java new file mode 100644 index 0000000..1f072cd --- /dev/null +++ b/src/main/java/su/tiburon/atlassian/jira/bean/SettingsBean.java @@ -0,0 +1,62 @@ +package su.tiburon.atlassian.jira.bean; + +import su.tiburon.atlassian.jira.JiraApplicationHelper; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Bean for setting results in REST responses. + */ +@XmlRootElement(name = "settings") +public class SettingsBean { + + @XmlElement + private final String baseurl; + + @XmlElement + private final String mode; + + @XmlElement + private final String title; + + /** + * The default constructor is needed for JSON request deserialization. + */ + public SettingsBean() { + this.baseurl = null; + this.mode = null; + this.title = null; + } + + public SettingsBean( + final String baseUrl, + final String mode, + final String title) { + + this.baseurl = baseUrl; + this.mode = mode; + this.title = title; + } + + public SettingsBean( + final JiraApplicationHelper applicationHelper) { + + this.baseurl = applicationHelper.getBaseUrl(); + this.mode = applicationHelper.getMode(); + this.title = applicationHelper.getTitle(); + } + + public String getBaseUrl() { + return baseurl; + } + + public String getTitle() { + return title; + } + + public String getMode() { + return mode; + } + +} diff --git a/src/main/java/su/tiburon/atlassian/jira/rest/DirectoriesResource.java b/src/main/java/su/tiburon/atlassian/jira/rest/DirectoriesResource.java new file mode 100644 index 0000000..2d5a073 --- /dev/null +++ b/src/main/java/su/tiburon/atlassian/jira/rest/DirectoriesResource.java @@ -0,0 +1,89 @@ +package su.tiburon.atlassian.jira.rest; + +import com.atlassian.jira.rest.api.util.ErrorCollection; +import com.atlassian.plugins.rest.common.security.AnonymousAllowed; +import su.tiburon.atlassian.jira.JiraApplicationHelper; +import su.tiburon.atlassian.jira.JiraWebAuthenticationHelper; +import org.springframework.stereotype.Component; +import com.atlassian.crowd.embedded.api.Directory; +import org.codehaus.jackson.map.ObjectMapper; + +import java.util.List; +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.QueryParam; +import javax.ws.rs.WebApplicationException; + +/** + * Settings resource to get the licenses. + */ +@Path("/directory") +@AnonymousAllowed +@Produces(MediaType.APPLICATION_JSON) +@Component +public class DirectoriesResource { + + private final JiraApplicationHelper applicationHelper; + + private final JiraWebAuthenticationHelper webAuthenticationHelper; + + /** + * Constructor. + * + * @param applicationHelper the injected {@link JiraApplicationHelper} + * @param webAuthenticationHelper the injected {@link JiraWebAuthenticationHelper} + */ + @Inject + public DirectoriesResource( + final JiraApplicationHelper applicationHelper, + final JiraWebAuthenticationHelper webAuthenticationHelper) { + + this.applicationHelper = applicationHelper; + this.webAuthenticationHelper = webAuthenticationHelper; + } + + @PUT + @Consumes(MediaType.APPLICATION_JSON) + public Response syncDir(@QueryParam("id") Long dirId) throws WebApplicationException { + + webAuthenticationHelper.mustBeSysAdmin(); + + final ErrorCollection errorCollection = ErrorCollection.of(); + + if (dirId != null) { + try { + applicationHelper.syncDir(dirId); + } catch (Exception e) { + errorCollection.addErrorMessage(e.getMessage()); + } + } else { + throw new WebApplicationException(Response.Status.BAD_REQUEST); + } + if (errorCollection.hasAnyErrors()) { + return Response.ok(errorCollection).build(); + } else { + return Response.ok(null).build(); + } + } + + @GET + public Response getDir() { + webAuthenticationHelper.mustBeSysAdmin(); + + ObjectMapper mapper = new ObjectMapper(); + final List directories = applicationHelper.getDir(); + String jsn = ""; + try { + jsn = mapper.writeValueAsString(directories); + } catch (Exception e) { + e.printStackTrace(); + } + return Response.ok(jsn).build(); + } +} \ No newline at end of file diff --git a/src/main/java/su/tiburon/atlassian/jira/rest/LicensesResource.java b/src/main/java/su/tiburon/atlassian/jira/rest/LicensesResource.java new file mode 100644 index 0000000..8f2c90d --- /dev/null +++ b/src/main/java/su/tiburon/atlassian/jira/rest/LicensesResource.java @@ -0,0 +1,76 @@ +package su.tiburon.atlassian.jira.rest; + +import com.atlassian.jira.license.LicenseDetails; +import com.atlassian.plugins.rest.common.security.AnonymousAllowed; +import su.tiburon.atlassian.jira.JiraApplicationHelper; +import su.tiburon.atlassian.jira.JiraWebAuthenticationHelper; +import su.tiburon.atlassian.jira.bean.LicenseBean; +import su.tiburon.atlassian.jira.bean.LicensesBean; +import org.springframework.stereotype.Component; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.Collection; + +/** + * Licenses resource to get the licenses. + */ +@Path("/licenses") +@AnonymousAllowed +@Produces(MediaType.APPLICATION_JSON) +@Component +public class LicensesResource { + + private final JiraApplicationHelper applicationHelper; + + private final JiraWebAuthenticationHelper webAuthenticationHelper; + + /** + * Constructor. + * + * @param applicationHelper the injected {@link JiraApplicationHelper} + * @param webAuthenticationHelper the injected {@link JiraWebAuthenticationHelper} + */ + @Inject + public LicensesResource( + final JiraApplicationHelper applicationHelper, + final JiraWebAuthenticationHelper webAuthenticationHelper) { + + this.applicationHelper = applicationHelper; + this.webAuthenticationHelper = webAuthenticationHelper; + } + + @GET + public Response getLicenses() { + webAuthenticationHelper.mustBeSysAdmin(); + + final Collection licenseDetails = applicationHelper.getLicenses(); + return Response.ok(LicensesBean.from(licenseDetails)).build(); + } + + @PUT + @Consumes(MediaType.TEXT_PLAIN) + public Response setLicense( + @QueryParam("clear") @DefaultValue("false") boolean clear, + final String licenseKey) throws WebApplicationException { + + webAuthenticationHelper.mustBeSysAdmin(); + + if (licenseKey == null) { + throw new WebApplicationException(Response.Status.BAD_REQUEST); + } + + final LicenseDetails licenseDetail = applicationHelper.setLicense(licenseKey, clear); + return Response.ok(LicenseBean.from(licenseDetail)).build(); + } + +} diff --git a/src/main/java/su/tiburon/atlassian/jira/rest/SettingsResource.java b/src/main/java/su/tiburon/atlassian/jira/rest/SettingsResource.java new file mode 100644 index 0000000..801cc95 --- /dev/null +++ b/src/main/java/su/tiburon/atlassian/jira/rest/SettingsResource.java @@ -0,0 +1,91 @@ +package su.tiburon.atlassian.jira.rest; + +import com.atlassian.jira.rest.api.util.ErrorCollection; +import com.atlassian.plugins.rest.common.security.AnonymousAllowed; +import su.tiburon.atlassian.jira.JiraApplicationHelper; +import su.tiburon.atlassian.jira.JiraWebAuthenticationHelper; +import su.tiburon.atlassian.jira.bean.SettingsBean; +import org.springframework.stereotype.Component; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +/** + * Settings resource to get the licenses. + */ +@Path("/settings") +@AnonymousAllowed +@Produces(MediaType.APPLICATION_JSON) +@Component +public class SettingsResource { + + private final JiraApplicationHelper applicationHelper; + + private final JiraWebAuthenticationHelper webAuthenticationHelper; + + /** + * Constructor. + * + * @param applicationHelper the injected {@link JiraApplicationHelper} + * @param webAuthenticationHelper the injected {@link JiraWebAuthenticationHelper} + */ + @Inject + public SettingsResource( + final JiraApplicationHelper applicationHelper, + final JiraWebAuthenticationHelper webAuthenticationHelper) { + + this.applicationHelper = applicationHelper; + this.webAuthenticationHelper = webAuthenticationHelper; + } + + @GET + public Response getSettings() { + webAuthenticationHelper.mustBeSysAdmin(); + + final SettingsBean settingsBean = new SettingsBean(applicationHelper); + return Response.ok(settingsBean).build(); + } + + @PUT + @Consumes(MediaType.APPLICATION_JSON) + public Response setSettings( + final SettingsBean settingsBean) { + + webAuthenticationHelper.mustBeSysAdmin(); + + final ErrorCollection errorCollection = ErrorCollection.of(); + + if (settingsBean.getBaseUrl() != null) { + try { + applicationHelper.setBaseUrl(settingsBean.getBaseUrl()); + } catch (Exception e) { + errorCollection.addErrorMessage(e.getMessage()); + } + } + + if (settingsBean.getMode() != null) { + try { + applicationHelper.setMode(settingsBean.getMode()); + } catch (Exception e) { + errorCollection.addErrorMessage(e.getMessage()); + } + } + + if (settingsBean.getTitle() != null) { + try { + applicationHelper.setTitle(settingsBean.getTitle()); + } catch (Exception e) { + errorCollection.addErrorMessage(e.getMessage()); + } + } + + return Response.ok(errorCollection).build(); + } + +} diff --git a/src/main/resources/META-INF/spring/spring-scanner.xml b/src/main/resources/META-INF/spring/spring-scanner.xml new file mode 100644 index 0000000..d91305f --- /dev/null +++ b/src/main/resources/META-INF/spring/spring-scanner.xml @@ -0,0 +1,10 @@ + + + + diff --git a/src/main/resources/atlassian-plugin.xml b/src/main/resources/atlassian-plugin.xml new file mode 100644 index 0000000..6f2f630 --- /dev/null +++ b/src/main/resources/atlassian-plugin.xml @@ -0,0 +1,21 @@ + + + ${project.description} + ${project.version} + + images/logo_octo_375_293.png + images/logo_octo_375_293.png + + + + + + com.atlassian.auiplugin:ajs + com.atlassian.auiplugin:aui-experimental-expander + + atl.general + + + + + diff --git a/src/main/resources/images/logo_octo_375_293.png b/src/main/resources/images/logo_octo_375_293.png new file mode 100644 index 0000000..543fe87 Binary files /dev/null and b/src/main/resources/images/logo_octo_375_293.png differ