From 400c76832d976f1082d841debb63b043b0bb4a86 Mon Sep 17 00:00:00 2001 From: Arthur Neudeck Date: Fri, 11 Jun 2021 11:56:52 +0200 Subject: [PATCH 1/4] Initial commit --- .github/workflows/ci.yml | 16 +- .github/workflows/release.yml | 52 +- .gitignore | 6 + .gitpod | 2 +- CODE_OF_CONDUCT.md | 2 +- CONTRIBUTION.md | 58 +++ LICENSE.md | 61 +++ README.md | 87 +++- docs/CODEOWNERS | 2 +- docs/img/BSmartPullRequestSidePanel.png | Bin 0 -> 7531 bytes docs/img/CodeFreezeConfigPage.png | Bin 0 -> 5495 bytes docs/img/IssueCheckerConfigPage.png | Bin 0 -> 23729 bytes docs/img/JiraAuthorization.png | Bin 0 -> 34102 bytes docs/img/MergeChecks.png | Bin 0 -> 54611 bytes pom.xml | 449 +++++++++++++----- .../IsCodeFreezeHookEnabledCondition.java | 51 ++ .../condition/IsHookEnabledCondition.java | 51 ++ .../hooks/IsAdminCodefreezeMergeCheck.java | 63 +++ .../hooks/IsAfterCodeFreezeHook.java | 69 +++ .../codefreeze/persistence/BranchFreeze.java | 48 ++ .../persistence/CodeFreezeSetting.java | 34 ++ .../persistence/CodeFreezeSettingDao.java | 106 +++++ .../codefreeze/rest/BranchFreezeModel.java | 72 +++ .../codefreeze/rest/CodeFreezeResource.java | 122 +++++ .../codefreeze/servlet/CodeFreezeServlet.java | 206 ++++++++ .../codefreeze/util/FrozenBranchUtil.java | 30 ++ .../bitbucket/common/files/FileUtils.java | 32 ++ .../common/groovy/GroovySandbox.java | 91 ++++ .../groovy/ScriptExecutionException.java | 21 + .../bitbucket/common/groovy/ScriptHelper.java | 54 +++ .../jira/FieldRetrieveHandler.java | 55 +++ .../jira/IssueFieldsRetrieveHandler.java | 35 ++ .../integration/jira/JiraIntegrationUtil.java | 65 +++ .../integration/jira/JiraProxyResource.java | 67 +++ .../jira/SimpleJiraIssueField.java | 40 ++ .../bitbucket/common/json/JsonParser.java | 30 ++ .../common/servlet/AbstractSimpleServlet.java | 45 ++ .../IsIssueCheckerEnabledCondition.java | 57 +++ .../issuechecker/hooks/IsIssueKeyCorrect.java | 173 +++++++ .../persistence/BranchIssueChecker.java | 40 ++ .../persistence/FieldIssueChecker.java | 47 ++ .../persistence/IssueCheckerSetting.java | 61 +++ .../persistence/IssueCheckerSettingDao.java | 147 ++++++ .../persistence/WhiteListGroup.java | 39 ++ .../persistence/WhiteListUser.java | 38 ++ .../rest/BranchIssueCheckerModel.java | 54 +++ .../rest/IssueCheckerResource.java | 63 +++ .../rest/IssueCheckerSettingModel.java | 90 ++++ .../rest/WhiteListGroupModel.java | 52 ++ .../issuechecker/rest/WhiteListUserModel.java | 74 +++ .../servlet/IssueCheckerServlet.java | 213 +++++++++ .../META-INF/spring/plugin-context.xml | 20 + src/main/resources/atlassian-plugin.xml | 148 ++++++ src/main/resources/codefreeze.properties | 14 + src/main/resources/css/codefreeze.css | 10 + src/main/resources/css/icon.css | 14 + .../resources/examples/ParseScript.groovy | 39 ++ src/main/resources/images/icon-codefreeze.svg | 1 + src/main/resources/images/pluginIcon.png | Bin 0 -> 958 bytes src/main/resources/images/pluginLogo.png | Bin 0 -> 3409 bytes src/main/resources/js/addBranchDialog.js | 59 +++ src/main/resources/js/codefreeze.js | 10 + src/main/resources/js/fieldSelector.js | 104 ++++ src/main/resources/js/userSelector.js | 131 +++++ src/main/resources/templates/codefreeze.soy | 131 +++++ src/main/resources/templates/issuechecker.soy | 158 ++++++ .../common/groovy/GroovySandboxTest.java | 55 +++ .../jira/FieldRetrieveHandlerTest.java | 57 +++ .../jiralink/JiraIssueDetailedTest.java | 30 ++ src/test/resources/groovy/ParseScript.groovy | 52 ++ src/test/resources/json/fieldsResponse.json | 36 ++ src/test/resources/json/issueResponse.json | 44 ++ .../json/issueResponseSingleFix.json | 28 ++ 73 files changed, 4251 insertions(+), 160 deletions(-) create mode 100644 CONTRIBUTION.md create mode 100644 LICENSE.md create mode 100644 docs/img/BSmartPullRequestSidePanel.png create mode 100644 docs/img/CodeFreezeConfigPage.png create mode 100644 docs/img/IssueCheckerConfigPage.png create mode 100644 docs/img/JiraAuthorization.png create mode 100644 docs/img/MergeChecks.png create mode 100644 src/main/java/com/baloise/open/bitbucket/codefreeze/condition/IsCodeFreezeHookEnabledCondition.java create mode 100644 src/main/java/com/baloise/open/bitbucket/codefreeze/condition/IsHookEnabledCondition.java create mode 100644 src/main/java/com/baloise/open/bitbucket/codefreeze/hooks/IsAdminCodefreezeMergeCheck.java create mode 100644 src/main/java/com/baloise/open/bitbucket/codefreeze/hooks/IsAfterCodeFreezeHook.java create mode 100644 src/main/java/com/baloise/open/bitbucket/codefreeze/persistence/BranchFreeze.java create mode 100644 src/main/java/com/baloise/open/bitbucket/codefreeze/persistence/CodeFreezeSetting.java create mode 100644 src/main/java/com/baloise/open/bitbucket/codefreeze/persistence/CodeFreezeSettingDao.java create mode 100644 src/main/java/com/baloise/open/bitbucket/codefreeze/rest/BranchFreezeModel.java create mode 100644 src/main/java/com/baloise/open/bitbucket/codefreeze/rest/CodeFreezeResource.java create mode 100644 src/main/java/com/baloise/open/bitbucket/codefreeze/servlet/CodeFreezeServlet.java create mode 100644 src/main/java/com/baloise/open/bitbucket/codefreeze/util/FrozenBranchUtil.java create mode 100644 src/main/java/com/baloise/open/bitbucket/common/files/FileUtils.java create mode 100644 src/main/java/com/baloise/open/bitbucket/common/groovy/GroovySandbox.java create mode 100644 src/main/java/com/baloise/open/bitbucket/common/groovy/ScriptExecutionException.java create mode 100644 src/main/java/com/baloise/open/bitbucket/common/groovy/ScriptHelper.java create mode 100644 src/main/java/com/baloise/open/bitbucket/common/integration/jira/FieldRetrieveHandler.java create mode 100644 src/main/java/com/baloise/open/bitbucket/common/integration/jira/IssueFieldsRetrieveHandler.java create mode 100644 src/main/java/com/baloise/open/bitbucket/common/integration/jira/JiraIntegrationUtil.java create mode 100644 src/main/java/com/baloise/open/bitbucket/common/integration/jira/JiraProxyResource.java create mode 100644 src/main/java/com/baloise/open/bitbucket/common/integration/jira/SimpleJiraIssueField.java create mode 100644 src/main/java/com/baloise/open/bitbucket/common/json/JsonParser.java create mode 100644 src/main/java/com/baloise/open/bitbucket/common/servlet/AbstractSimpleServlet.java create mode 100644 src/main/java/com/baloise/open/bitbucket/issuechecker/condition/IsIssueCheckerEnabledCondition.java create mode 100644 src/main/java/com/baloise/open/bitbucket/issuechecker/hooks/IsIssueKeyCorrect.java create mode 100644 src/main/java/com/baloise/open/bitbucket/issuechecker/persistence/BranchIssueChecker.java create mode 100644 src/main/java/com/baloise/open/bitbucket/issuechecker/persistence/FieldIssueChecker.java create mode 100644 src/main/java/com/baloise/open/bitbucket/issuechecker/persistence/IssueCheckerSetting.java create mode 100644 src/main/java/com/baloise/open/bitbucket/issuechecker/persistence/IssueCheckerSettingDao.java create mode 100644 src/main/java/com/baloise/open/bitbucket/issuechecker/persistence/WhiteListGroup.java create mode 100644 src/main/java/com/baloise/open/bitbucket/issuechecker/persistence/WhiteListUser.java create mode 100644 src/main/java/com/baloise/open/bitbucket/issuechecker/rest/BranchIssueCheckerModel.java create mode 100644 src/main/java/com/baloise/open/bitbucket/issuechecker/rest/IssueCheckerResource.java create mode 100644 src/main/java/com/baloise/open/bitbucket/issuechecker/rest/IssueCheckerSettingModel.java create mode 100644 src/main/java/com/baloise/open/bitbucket/issuechecker/rest/WhiteListGroupModel.java create mode 100644 src/main/java/com/baloise/open/bitbucket/issuechecker/rest/WhiteListUserModel.java create mode 100644 src/main/java/com/baloise/open/bitbucket/issuechecker/servlet/IssueCheckerServlet.java create mode 100644 src/main/resources/META-INF/spring/plugin-context.xml create mode 100644 src/main/resources/atlassian-plugin.xml create mode 100644 src/main/resources/codefreeze.properties create mode 100644 src/main/resources/css/codefreeze.css create mode 100644 src/main/resources/css/icon.css create mode 100644 src/main/resources/examples/ParseScript.groovy create mode 100644 src/main/resources/images/icon-codefreeze.svg create mode 100644 src/main/resources/images/pluginIcon.png create mode 100644 src/main/resources/images/pluginLogo.png create mode 100644 src/main/resources/js/addBranchDialog.js create mode 100644 src/main/resources/js/codefreeze.js create mode 100644 src/main/resources/js/fieldSelector.js create mode 100644 src/main/resources/js/userSelector.js create mode 100644 src/main/resources/templates/codefreeze.soy create mode 100644 src/main/resources/templates/issuechecker.soy create mode 100644 src/test/java/com/baloise/open/bitbucket/common/groovy/GroovySandboxTest.java create mode 100644 src/test/java/com/baloise/open/bitbucket/common/integration/jira/FieldRetrieveHandlerTest.java create mode 100644 src/test/java/com/baloise/open/bitbucket/issuechecker/jiralink/JiraIssueDetailedTest.java create mode 100644 src/test/resources/groovy/ParseScript.groovy create mode 100644 src/test/resources/json/fieldsResponse.json create mode 100644 src/test/resources/json/issueResponse.json create mode 100644 src/test/resources/json/issueResponseSingleFix.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e6748f..4f89854 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,14 +1,20 @@ name: CI -on: [push] +on: + push: + branches: [ master, release ] + pull_request: + branches: [ master ] jobs: build: - runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@master # don't get confused by @master - it is the version of the checkout action. You repo will be checked out with ${{ github.ref }} + - uses: actions/checkout@v2 + - name: Set up Java + uses: actions/setup-java@v1 + with: + java-version: '11' - name: Build with Maven - run: mvn clean verify + run: mvn -B package --file pom.xml \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c271f2b..06e6aa8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,30 +1,42 @@ name: Release on: - push: - # Sequence of patterns matched against refs/tags + create: tags: - - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 + - '*' jobs: build: - name: Create Release runs-on: ubuntu-latest + steps: - - name: Checkout code - uses: actions/checkout@master # don't get confused by @master - it is the version of the checkout action. You repo will be checked out with ${{ github.ref }} - - name: Deploy to Github Package Registry - env: - GITHUB_USERNAME: x-access-token - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: mvn --settings .github/maven-settings.xml deploy - - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + - name: Checkout Source 🛎️ + uses: actions/checkout@v2 + + - name: Set up Java + uses: actions/setup-java@v1 + with: + java-version: '11' + + - name: Build with Maven 🔧 + run: mvn -B package --file pom.xml + + - name: Copy artifacts + run: | + mkdir artifacts + cp target/*.jar artifacts/ + cp target/*.obr artifacts/ + ls artifacts + - name: Archive artifacts + uses: actions/upload-artifact@v2 with: - tag_name: ${{ github.ref }} - release_name: Release ${{ github.ref }} - draft: false - prerelease: false + path: artifacts + + - name: Get tag name + run: echo "TAG_NAME=$(echo ${GITHUB_REF#refs/*/} | tr / -)" >> $GITHUB_ENV + + - name: Release + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 9e227f6..4530d61 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,9 @@ hs_err_pid* /.project /.classpath /bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr diff --git a/.gitpod b/.gitpod index 7d9f2c3..ee99943 100644 --- a/.gitpod +++ b/.gitpod @@ -1,3 +1,3 @@ tasks: - - command: "mvn clean verify" + - command: "mvn clean verify" \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 8bbb649..c088f43 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1 +1 @@ -# [Code of conduct](https://baloise.github.io/open-source/docs/md/guides/governance.html#code-of-conduct) +# [Code of conduct](https://baloise.github.io/open-source/docs/md/guides/governance.html#code-of-conduct) \ No newline at end of file diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md new file mode 100644 index 0000000..a472894 --- /dev/null +++ b/CONTRIBUTION.md @@ -0,0 +1,58 @@ +# Contributing to Codefreeze + +**Thank you for your interest in Codefreeze. Your contributions are highly welcome.** + +There are multiple ways of getting involved: + +- [Report a bug](#report-a-bug) +- [Suggest a feature](#suggest-a-feature) +- [Contribute code](#contribute-code) + +Below are a few guidelines we would like you to follow. +If you need help, please reach out to us by opening an issue. + +## Report a bug +Reporting bugs is one of the best ways to contribute. Before creating a bug report, please check that an [issue](../../issues) reporting the same problem does not already exist. If there is such an issue, you may add your information as a comment. + +To [report a new bug](../../issues/new?labels=bug) you should open an issue that summarizes the bug and set the label to "bug". + +If you want to provide a fix along with your bug report: That is great! In this case please send us a pull request as described in section [Contribute Code](#contribute-code). + +## Suggest a feature +To request a new feature you should [open an issue](../../issues/new?labels=enhancement) and summarize the desired functionality and its use case. Set the issue label to "feature". + +## Contribute code +This is an outline of what the workflow for code contributions looks like + +- Check the list of open [issues](../../issues). Either assign an existing issue to yourself, or + create a new one that you would like work on and discuss your ideas and use cases. + +It is always best to discuss your plans beforehand, to ensure that your contribution is in line with our goals. + +- Fork the repository on GitHub +- Create a topic branch from where you want to base your work. This is usually master. +- Open a new pull request, label it `work in progress` and outline what you will be contributing +- Make commits of logical units. +- Make sure you sign-off on your commits `git commit -s -m "adding X to change Y"` +- Write good commit messages (see below). +- Push your changes to a topic branch in your fork of the repository. +- As you push your changes, update the pull request with new information and tasks as you complete them +- Project maintainers might comment on your work as you progress +- When you are done, remove the `work in progess` label and ping the maintainers for a review +- Your pull request must receive a :thumbsup: from two [MAINTAINERS](docs/CODEOWNERS) + +Thanks for your contributions! + +### Commit messages +Your commit messages ideally can answer two questions: what changed and why. The subject line should feature the “what” and the body of the commit should describe the “why”. + +When creating a pull request, its description should reference the corresponding issue id. + +### Sign your work / Developer certificate of origin +All contributions (including pull requests) must agree to the Developer Certificate of Origin (DCO) version 1.1. This is exactly the same one created and used by the Linux kernel developers and posted on http://developercertificate.org/. This is a developer's certification that he or she has the right to submit the patch for inclusion into the project. Simply submitting a contribution implies this agreement, however, please include a "Signed-off-by" tag in every patch (this tag is a conventional way to confirm that you agree to the DCO) - you can automate this with a [Git hook](https://stackoverflow.com/questions/15015894/git-add-signed-off-by-line-using-format-signoff-not-working) + +``` +git commit -s -m "adding X to change Y" +``` + +**Have fun, and happy hacking!** \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..ad8339a --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,61 @@ +Apache License, Version 2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and You must cause any modified files to carry prominent notices stating that You changed the files; and You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/README.md b/README.md index ff9a9c1..5cc1405 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,80 @@ -# repository-template-java -A template to use when starting a new open source project. +#Codefreeze -## perform a repository wide search and replace for "repository-template-java" and the "target-repo-name" +##Short Documentation -e.g. by using +*Codefreeze* is a basic plugin for [Atlassian Bitbucket](https://www.atlassian.com/software/bitbucket) that allows +you ahead of time set date from which pushing directly to specified branches is no longer possible. -``` -cp -R repository-template-java/ new-name && cd new-name && git config --local --unset remote.origin.url && git config --local --add remote.origin.url git@github.com:baloise/new-name.git && git reset --hard $(git commit-tree FETCH_HEAD^{tree} -m "Initial contribution") && git grep -l 'repository-template-java' | xargs sed -i '' -e 's/repository-template-java/new-name/g' && mvn clean verify && git add -A && git commit -m "Rename from template to new-name" && cd .. -``` -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/bf6fa237dd934970991ecba2c66db23e)](https://app.codacy.com/app/baloise/repository-template-java?utm_source=github.com&utm_medium=referral&utm_content=baloise/repository-template-java&utm_campaign=Badge_Grade_Dashboard) -[![DepShield Badge](https://depshield.sonatype.org/badges/baloise/repository-template-java/depshield.svg)](https://depshield.github.io) -![Build Status](https://github.com/baloise/repository-template-java/workflows/CI/badge.svg) +In order to enable the *Codefreeze* settings page one of two hooks should be enabled on project or repository +level: +- MergeCheck: + - CodeFreezeMergeCheck: allows merges of pull requests only after admin review of the change + - IsIssueKeyCorrect: executes a groovy script for every Jira ticket contained inside the pull request +- Hooks: + - CodeFreezePrePushHook: blocks pushing to specified branches after specified date + - IsIssueKeyCorrect: executes groovy script to evaluate the correctness of a Jira Issue, checks across all + available fields -## the [docs](docs/index.md) +The branch name in the settings will be checked against ref name by "contains" function, which means that the +master will apply restriction to branches such as `/8/1/0/master`, `/master/test` and so on. -## releasing +An example for a groovy script can be found in the file `scr/test/resources/groovy/ParseScript.groovy`. -Run e.g. on master: `mvn -B release:prepare` e.g. via [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io#https://github.com/baloise/repository-template-java) +## Configuration +Settings can be found either on the left side panel of the repository page or inside repository settings. -Subsequently the GitHub action worksflow "create release" will pick up the published tag and release and deploy the artifacts in the Github package registry. +![Pull Request Side Panel](docs/img/BSmartPullRequestSidePanel.png) + +The merge checks page looks like: +![MergeChecks](docs/img/MergeChecks.png) + +The issue checker is configured as follows: +![Issue Cheker Config](docs/img/IssueCheckerConfigPage.png) + +A code freeze can be configured the following way: +![Code Freeze Configuration](docs/img/CodeFreezeConfigPage.png) + +The authorization needs to be defined in JIRA: +![Jira Authorization](docs/img/JiraAuthorization.png) + +# Open Issues +- Reinit the Github task https://github.com/baloise/open-source/issues/117 + +##Hints for Developing Plugins + +Here are the SDK commands you'll use immediately: + +* `atlas-run`: installs this plugin into the product and starts it on localhost +* `atlas-debug`: same as `atlas-run`, but allows a debugger to attach at port 5005 +* `atlas-cli`: after `atlas-run` or `atlas-debug`, opens a Maven command line window: `pi` + re-installs the plugin into the running product instance +* `atlas-help`: prints description for all commands in the SDK + +The full documentation is available at [Introduction to the Atlassian Plugin SDK](https://developer.atlassian.com/display/DOCS/Introduction+to+the+Atlassian+Plugin+SDK) + +An example plugin can be found at https://bitbucket.org/atlassian/bitbucket-server-example-plugins. + + +For locating web elements suffix address with `?web.items&web.panels&web.sections`. + +Follow the link [Best practices for active objects](https://developer.atlassian.com/server/framework/atlassian-sdk/best-practices-for-developing-with-active-objects/) +to obtain further best practices. + +The DB access must be configured using the following JDBC URL: + +`jdbc:h2:file:[pluginfolderpath]\target\bitbucket\home\shared\data\db;AUTO_SERVER=TRUE` + +You can find an AUI soy template at: +* https://bitbucket.org/atlassian/aui/src/auiplugin-5.0-m26-stash/auiplugin/src/main/resources/soy/atlassian/form.soy?fileviewer=file-view-default +* https://developer.atlassian.com/server/bitbucket/reference/soy/branch-selector-field/ + +The API is used with the following URL structure: + +`{host}/{app}/rest/{pathOfRest}/{versionOfRest}/{pathDefinedInClass)` + +Example: + +`localhost:7990/bitbucket/rest/codefreeze/1.0/projects/PROJECT_1/repos/rep_1/branchfreeze` + +##Open Source +This project is open source and follows the [Baloise Open Source Guidelines](https://baloise.github.io/open-source/docs/arc42/). \ No newline at end of file diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index b96df36..cc54e50 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -1,2 +1,2 @@ # see: https://help.github.com/articles/about-codeowners/ -* @baloise/open-source +* @ArthurNeudeck @BenjaminMueller84 \ No newline at end of file diff --git a/docs/img/BSmartPullRequestSidePanel.png b/docs/img/BSmartPullRequestSidePanel.png new file mode 100644 index 0000000000000000000000000000000000000000..d75ab84ac225dfb0e6b1c55fe646ede8cc7fd129 GIT binary patch literal 7531 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 JES5J9`8N{XknR8g literal 0 HcmV?d00001 diff --git a/docs/img/CodeFreezeConfigPage.png b/docs/img/CodeFreezeConfigPage.png new file mode 100644 index 0000000000000000000000000000000000000000..5b1ec635813a2ed0ebb997cc1586c6716c5de072 GIT binary patch literal 5495 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 YvvKE&xLL)ZQT&tru7_NTosUNU4Z}u3djJ3c literal 0 HcmV?d00001 diff --git a/docs/img/JiraAuthorization.png b/docs/img/JiraAuthorization.png new file mode 100644 index 0000000000000000000000000000000000000000..c23257cdebc3f787c108be981a287a7a4238c5d5 GIT binary patch literal 34102 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 b1j{s|h3|BmwTU987U;nKL;HxIo%rtWyHL8; literal 0 HcmV?d00001 diff --git a/docs/img/MergeChecks.png b/docs/img/MergeChecks.png new file mode 100644 index 0000000000000000000000000000000000000000..5c8f37dda4fe99d91027effbf0ccf26afa5674ce GIT binary patch literal 54611 zcmeFZc{r49A3v;BB~g+hR6=FncT)+Kt*nVL6;Wi#&J0t@z9q}p6-D+K`!KREW8WD} zmO+>qX3Q{*@s9g>?z`^i{qOzb{pa_)j-z9o*Ku9vb)MhzbAHdycPsRPzBcO#?h{N* zOsx0r-Z5liI+4W0bdc-VLB^fgGMOdD&jB|>?b}R6Jr{p58i(y}>D^*tDv4&HJw3u` zA9uNH?#9G)s%ihdn|f?^xo>Vv(*5;$cRW2zrt;90+Fi8mX}m9?y?=lDe_tJMqBp<_n4c zvp&n(LBv#%ORvufZ)0n7x%4fNPSG#>TTd0&Jk3j73uM7aa;8Nc+DGAkudIR?bNYLh z?DVww<3d9j{ww*}4-sK^MwzXE;FYppu!L(})6_=dg()MjX;QFhLHeI7qh)>w4gpl~ zPfjW9a9>yimA!!Ha?5NJlErQP=|Jpp_>OIRw5hI`4K>&I?+-FD{kUfTI6=a3Z%8wf zo?KBc3%K*=@}Ae!>n66@8|(i2W_LHyOrL{nWLNiu&z|tgQk_i1uWz%EdsCD=W){4< zWIFS8dbr3kBc?anws+21`o_FenjtD}I!@Y6C{B+Pnkbx#)joRiu#O@xX~l)fkzvV|pTly4f@_>%FJnlc z@^D+s4Xbh!(!7SxO;XoZe7w)SDtruchi=gLuc&&+LXyaapMqF9wY- zh98}s!J>Bb4)MGP^eb5Bz3|lEOikxm(Cc|;zxUWjWg_l4toK~EZ&CWo(is%2)@I_a zMACcz<*$SD`X*;GdPgFQFy`fVBiyjhUA;KOV-e@DM){?^?6uqNUws=FfmuPxv4JCu z#pRWEd@qUuDWG*NR`EtqB@3r5pNg{Q{YshiWfSJExd-HqQPnRgX6=HOzHbXub1a;~ zOTBeUt?t@V{ak^Jo`Zc$mG5L)Ru7naV_h&OfyE$2KS)hZPokhmTKVYXvwzQ+iOIbz z`#BXX4_$UmB3o_$a56H=U4=Z!)^(AvDD&wXGQP26nzy0goE@-6s_8|(py#P&>npC^ zE-vsY0x4usyKe-b2`dSQ^b_mGhb}^%m0on2&V%H{XF0#}mtQf7DDu#o;oC{EYfr4* z83;sAX;6A5sL~N$5(T3=Rgi_i#qN)0UmcjufY(#Ht?(efB9cGgwAWBF1Q_fdvN>j8 zL(OzIz@eNY*LqHnf4yh%&5+~tl&m1^Vz7^M{ zb@%|Umxtou^IvFvMeYHf@16d`S^=Z;IzlZ$Nms=m6k2ls``Z$=)*5$AGqS1#I()yokr63 zh5+w|RmIi%6(l(i$-B!z^xW{3tpk37FY?7|*^Lfj%3tQVU9K_#B|yEY!_PjfA@(Al z^lX22Yp8ixn?|3Ya!j%vxMQ3KNeeEMGWXx=Kl~Icn=|d_ad2i6JL;|ZdkCMJ0L39sWIaWlMM|tw6e75n1|h+Q6R{!MgA?>{Mg^R30$$lXkvm z^lrP_OyHqn_19l(8+PJ!%)Z^Mn-+y){H8L^hQX`MAS(Z^;=3M#iO%j38hvzelwJS7 zU-MKr*zXhtx#4^B*}s>2;W?Fvp2Tu}%TTcUiu_s|RnnURIu_@rd@Oph+X@pi9fV2o zwPCu5xV6jBI4wpdp7Yu$@^k9K$L-?^ucV`SYjy)!yi(dsN+Ge(@RtN6fv6m%6{A8` zW_!tAqr#npCHTw_ z#CZw`w;i7!v!NOZxl3SD6;vwBk95sRbRuJshQC1H92SZvtnI%v;5I!_b*c4|IBZs#p0{7!(I6Q+p&T$Y1dD0Mi$Hk{7p^dN(83F$uLx%r_E?up!PZ>%+uZz@X0 znvHaZ|B|Sy+Z@=~++DQSLl$Wy7xq(1Gs#Vi(FO0=e(SgcQrvtq^e4`Vz!tw3Ekn`O#xpgv$ic15e&;Euov}WLv2Z%chL^`ESy8hez#N%KVbKw?E7^$0Z≠k=~R7(qPs-xl#zcZ5<|o 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 XJEXrFru40WT=lx - - 4.0.0 + ~ Copyright 2021 Baloise Group + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + --> - - Baloise Group - https://www.baloise.com - + + 4.0.0 + + Baloise Group + https://www.baloise.com + - com.baloise.open.repository-template-java - repository-template-java - 0.0.3-SNAPSHOT - jar + com.baloise.open.bitbucket + codefreeze + 1.0.0-SNAPSHOT + atlassian-plugin - - Github Actions - https://github.com/baloise/repository-template-java/actions - + + Github Actions + https://github.com/baloise/codefreeze/actions + + + Github + https://github.com/baloise/codefreeze/issues + + + scm:git:https://github.com/baloise/codefreeze.git + scm:git:https://github.com/baloise/codefreeze.git + https://github.com/baloise/codefreeze.git + - repository-template-java-main - https://github.com/baloise/repository-template-java + codefreeze + This is the Codefreeze plugin for Atlassian Bitbucket Server. + https://github.com/baloise/codefreeze - - https://github.com/baloise/repository-template-java.git - scm:git:https://github.com/baloise/repository-template-java.git - HEAD - - - - - github - GitHub Baloise Apache Maven Packages - https://maven.pkg.github.com/baloise/repository-template-java - - + + UTF-8 + 1.8 + 1.8 - - UTF-8 - 1.8 - 1.8 - - 5.2.0 - 1.2.0 - 1.3 - - - - - org.junit.jupiter - junit-jupiter-api - ${junit.jupiter.version} - test - - - org.junit.jupiter - junit-jupiter-params - ${junit.jupiter.version} - test - - - org.junit.jupiter - junit-jupiter-engine - ${junit.jupiter.version} - test - - - org.hamcrest - hamcrest-library - ${org.hamcrest.version} - test - - - - - - - - maven-surefire-plugin - 2.22.0 - - - org.apache.maven.plugins - maven-release-plugin - 2.5.3 - - v@{project.version} - - - - org.codehaus.mojo - license-maven-plugin - 1.16 - - - add-third-party - - add-third-party - - - - - - - - . - - LICENSE.txt - - - - + 5.2.0 + 1.2.0 + 1.3 + + 6.5.1 + 6.5.1 + 8.0.2 + false + + ${project.groupId}.${project.artifactId} + 2.1.7 + 3.5 + 2.8.0 + 1 + 1.1.1 + 4.12 + 2.0.1 + UTF-8 + 1.8 + 1.8 + + + https://itsec.balgroupit.com/ + com.baloise.open.bitbucket.codefreeze + + + + + + com.atlassian.bitbucket.server + bitbucket-parent + ${bitbucket.version} + pom + import + + + + + + + + com.google.code.gson + gson + ${gson.version} + + + org.kohsuke + groovy-sandbox + 1.27 + + + org.json + json + 20190722 + + + + + com.atlassian.applinks + applinks-api + provided + + + + com.atlassian.bitbucket.server + bitbucket-jira-api + provided + + + + com.atlassian.activeobjects + activeobjects-plugin + provided + + + + com.atlassian.plugins + atlassian-plugins-webresource + 3.7.4 + provided + + + com.atlassian.soy + soy-template-renderer-api + provided + + + + com.atlassian.templaterenderer + atlassian-template-renderer-api + provided + + + com.atlassian.sal + sal-api + provided + + + com.atlassian.bitbucket.server + bitbucket-api + provided + + + com.atlassian.bitbucket.server + bitbucket-spi + provided + + + com.atlassian.bitbucket.server + bitbucket-page-objects + provided + + + javax.servlet + javax.servlet-api + provided + + + org.apache.commons + commons-lang3 + ${commons.lang3.version} + provided + + + com.atlassian.plugin + atlassian-spring-scanner-annotation + ${atlassian.spring.scanner.version} + provided + + + javax.inject + javax.inject + ${javax.inject.version} + provided + + + javax.ws.rs + jsr311-api + ${jsr311.version} + provided + + + org.springframework + spring-context + provided + + + javax.servlet + servlet-api + 2.4 + provided + + + javax.xml.bind + jaxb-api + 2.3.1 + provided + + + com.atlassian.plugins.rest + atlassian-rest-common + 1.0.2 + provided + + + + junit + junit + ${junit.libversion} + test + + + + com.atlassian.plugins + atlassian-plugins-osgi-testrunner + ${plugin.testrunner.version} + test + + + org.apache.wink + wink-client + 1.4 + test + + + org.mockito + mockito-all + 1.8.5 + test + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.8.2 + + + com.atlassian.maven.plugins + bitbucket-maven-plugin + ${amps.version} + true + + + + bitbucket + bitbucket + ${bitbucket.version} + ${bitbucket.data.version} + + + + ${atlassian.plugin.key} + + com.baloise.open.bitbucket.api, + + org.springframework.osgi.*;resolution:="optional", + org.eclipse.gemini.blueprint.*;resolution:="optional", com.thoughtworks.xstream.*; + resolution:="optional", org.apache.commons.cli.*; resolution:="optional", org.apache.ivy.*; + resolution:="optional", org.fusesource.jansi.*; resolution:="optional", com.ibm.uvm.*; + resolution:="optional", * + + + * + + + + + com.atlassian.plugin + atlassian-spring-scanner-maven-plugin + ${atlassian.spring.scanner.version} + + + + atlassian-spring-scanner + + process-classes + + + + + + com.atlassian.plugin + atlassian-spring-scanner-external-jar + + + false + + + + com.sonatype.clm + clm-maven-plugin + 2.7.0-01 + + develop + + + + + + + + + true + warn + + + true + never + warn + + atlassian-public + https://packages.atlassian.com/mvn/maven-external/ + + + + + + + true + warn + + + true + never + warn + + atlassian-public + https://packages.atlassian.com/mvn/maven-external/ + + diff --git a/src/main/java/com/baloise/open/bitbucket/codefreeze/condition/IsCodeFreezeHookEnabledCondition.java b/src/main/java/com/baloise/open/bitbucket/codefreeze/condition/IsCodeFreezeHookEnabledCondition.java new file mode 100644 index 0000000..dc2620c --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/codefreeze/condition/IsCodeFreezeHookEnabledCondition.java @@ -0,0 +1,51 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.codefreeze.condition; + +import com.atlassian.bitbucket.hook.repository.RepositoryHook; +import com.atlassian.bitbucket.hook.repository.RepositoryHookService; +import com.atlassian.bitbucket.repository.Repository; +import com.atlassian.bitbucket.scope.RepositoryScope; +import com.atlassian.bitbucket.scope.Scope; +import com.atlassian.plugin.PluginParseException; +import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; +import com.atlassian.plugin.web.Condition; + +import java.util.Map; + +public class IsCodeFreezeHookEnabledCondition implements Condition { + + final RepositoryHookService repositoryHookService; + + public IsCodeFreezeHookEnabledCondition(@ComponentImport RepositoryHookService repositoryHookService){ + this.repositoryHookService = repositoryHookService; + } + + public void init(Map map) throws PluginParseException { + } + + public boolean shouldDisplay(Map params) { + Boolean mergeCheckActive = Boolean.FALSE; + Boolean pushHookActive = Boolean.FALSE; + + Repository repo = (Repository) params.get("repository"); + if(repo != null) { + Scope scope = new RepositoryScope(repo); + + RepositoryHook mergeHook = repositoryHookService.getByKey(scope, "com.baloise.open.bitbucket.codefreeze:isAdminCodefreezeMergeCheck"); + mergeCheckActive = mergeHook != null && mergeHook.isEnabled(); + + RepositoryHook pushHook = repositoryHookService.getByKey(scope, "com.baloise.open.bitbucket.codefreeze:IsAfterCodeFreezeHook"); + pushHookActive = pushHook != null && pushHook.isEnabled(); + } + return mergeCheckActive || pushHookActive; + } +} diff --git a/src/main/java/com/baloise/open/bitbucket/codefreeze/condition/IsHookEnabledCondition.java b/src/main/java/com/baloise/open/bitbucket/codefreeze/condition/IsHookEnabledCondition.java new file mode 100644 index 0000000..dd26b77 --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/codefreeze/condition/IsHookEnabledCondition.java @@ -0,0 +1,51 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.codefreeze.condition; + +import com.atlassian.bitbucket.hook.repository.RepositoryHook; +import com.atlassian.bitbucket.hook.repository.RepositoryHookService; +import com.atlassian.bitbucket.repository.Repository; +import com.atlassian.bitbucket.scope.RepositoryScope; +import com.atlassian.bitbucket.scope.Scope; +import com.atlassian.plugin.PluginParseException; +import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; +import com.atlassian.plugin.web.Condition; + +import java.util.Map; + +public class IsHookEnabledCondition implements Condition { + + final RepositoryHookService repositoryHookService; + + public IsHookEnabledCondition(@ComponentImport RepositoryHookService repositoryHookService){ + this.repositoryHookService = repositoryHookService; + } + + public void init(Map map) throws PluginParseException { + } + + public boolean shouldDisplay(Map params) { + Boolean mergeCheckActive = Boolean.FALSE; + Boolean pushHookActive = Boolean.FALSE; + + Repository repo = (Repository) params.get("repository"); + if(repo != null) { + Scope scope = new RepositoryScope(repo); + + RepositoryHook mergeHook = repositoryHookService.getByKey(scope, "com.baloise.open.bitbucket.codefreeze:isAdminMergeCheck"); + mergeCheckActive = mergeHook != null && mergeHook.isEnabled(); + + RepositoryHook pushHook = repositoryHookService.getByKey(scope, "com.baloise.open.bitbucket.codefreeze:IsAfterCodeFreezeHook"); + pushHookActive = pushHook != null && pushHook.isEnabled(); + } + return mergeCheckActive || pushHookActive; + } +} diff --git a/src/main/java/com/baloise/open/bitbucket/codefreeze/hooks/IsAdminCodefreezeMergeCheck.java b/src/main/java/com/baloise/open/bitbucket/codefreeze/hooks/IsAdminCodefreezeMergeCheck.java new file mode 100644 index 0000000..bc4c8ba --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/codefreeze/hooks/IsAdminCodefreezeMergeCheck.java @@ -0,0 +1,63 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.codefreeze.hooks; + +import com.baloise.open.bitbucket.codefreeze.persistence.CodeFreezeSetting; +import com.baloise.open.bitbucket.codefreeze.persistence.CodeFreezeSettingDao; +import com.baloise.open.bitbucket.codefreeze.util.FrozenBranchUtil; +import com.atlassian.bitbucket.hook.repository.PreRepositoryHookContext; +import com.atlassian.bitbucket.hook.repository.PullRequestMergeHookRequest; +import com.atlassian.bitbucket.hook.repository.RepositoryHookResult; +import com.atlassian.bitbucket.hook.repository.RepositoryMergeCheck; +import com.atlassian.bitbucket.permission.Permission; +import com.atlassian.bitbucket.permission.PermissionService; +import com.atlassian.bitbucket.pull.PullRequest; +import com.atlassian.bitbucket.repository.Repository; +import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.Nonnull; + +@Component("isAdminCodefreezeMergeCheck") +public class IsAdminCodefreezeMergeCheck implements RepositoryMergeCheck { + + private final PermissionService permissionService; + private final CodeFreezeSettingDao codeFreezeSettingDao; + + @Autowired + public IsAdminCodefreezeMergeCheck(@ComponentImport PermissionService permissionService, CodeFreezeSettingDao codeFreezeSettingDao) { + this.permissionService = permissionService; + this.codeFreezeSettingDao = codeFreezeSettingDao; + } + + @Nonnull + @Override + public RepositoryHookResult preUpdate(@Nonnull PreRepositoryHookContext context, + @Nonnull PullRequestMergeHookRequest request) { + PullRequest pullRequest = request.getPullRequest(); + Repository repository = pullRequest.getToRef().getRepository(); + if (!permissionService.hasRepositoryPermission(repository, Permission.REPO_ADMIN)) { + CodeFreezeSetting codeFreezeSetting = codeFreezeSettingDao.getCodeFreezeSettings(repository); + + Boolean isBranchFrozen = FrozenBranchUtil.isBranchFrozen(request.getToRef().getDisplayId(), codeFreezeSetting); + if (isBranchFrozen) { + if (pullRequest.getReviewers().stream().noneMatch(reviewer -> reviewer.isApproved() && + permissionService.hasRepositoryPermission(reviewer.getUser(), repository, Permission.REPO_ADMIN))) { + return RepositoryHookResult.rejected("Codefreeze", "CodeFreeze is in force. RM needs to accept changes."); + } + } + } + + return RepositoryHookResult.accepted(); + } + +} diff --git a/src/main/java/com/baloise/open/bitbucket/codefreeze/hooks/IsAfterCodeFreezeHook.java b/src/main/java/com/baloise/open/bitbucket/codefreeze/hooks/IsAfterCodeFreezeHook.java new file mode 100644 index 0000000..aa0744e --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/codefreeze/hooks/IsAfterCodeFreezeHook.java @@ -0,0 +1,69 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.codefreeze.hooks; + +import com.baloise.open.bitbucket.codefreeze.persistence.CodeFreezeSetting; +import com.baloise.open.bitbucket.codefreeze.persistence.CodeFreezeSettingDao; +import com.baloise.open.bitbucket.codefreeze.util.FrozenBranchUtil; +import com.atlassian.bitbucket.hook.repository.PreRepositoryHook; +import com.atlassian.bitbucket.hook.repository.PreRepositoryHookContext; +import com.atlassian.bitbucket.hook.repository.RepositoryHookResult; +import com.atlassian.bitbucket.hook.repository.RepositoryPushHookRequest; +import com.atlassian.bitbucket.permission.PermissionService; +import com.atlassian.bitbucket.repository.RefChangeType; +import com.atlassian.bitbucket.repository.Repository; +import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.Nonnull; + +@Component("IsAfterCodeFreezeHook") +public class IsAfterCodeFreezeHook implements PreRepositoryHook { + + private final PermissionService permissionService; + private final CodeFreezeSettingDao codeFreezeSettingDao; + + private final String frozenMsg = "Branch is frozen create proper pull request" + + " and contact nearest Release Manager!"; + + + + @Autowired + public IsAfterCodeFreezeHook(@ComponentImport PermissionService permissionService, + CodeFreezeSettingDao codeFreezeSettingDao) { + this.permissionService = permissionService; + this.codeFreezeSettingDao = codeFreezeSettingDao; + } + + + @Nonnull + @Override + public RepositoryHookResult preUpdate(@Nonnull PreRepositoryHookContext preRepositoryHookContext, @Nonnull RepositoryPushHookRequest request) { + final Repository repo = request.getRepository(); + final CodeFreezeSetting codeFreezeSetting = codeFreezeSettingDao.getCodeFreezeSettings(repo); + +// if(!permissionService.hasRepositoryPermission(repo,Permission.REPO_ADMIN)) { + Boolean isFrozen = request.getRefChanges().stream() + .anyMatch(refChange -> + refChange.getType() == RefChangeType.UPDATE + && FrozenBranchUtil.isBranchFrozen(refChange.getRef().getDisplayId(), codeFreezeSetting)); + + if (isFrozen) { + request.getScmHookDetails().ifPresent(scmDetails -> { + scmDetails.out().println(frozenMsg); + }); + return RepositoryHookResult.rejected(frozenMsg, frozenMsg); + } +// } + return RepositoryHookResult.accepted(); + } +} diff --git a/src/main/java/com/baloise/open/bitbucket/codefreeze/persistence/BranchFreeze.java b/src/main/java/com/baloise/open/bitbucket/codefreeze/persistence/BranchFreeze.java new file mode 100644 index 0000000..ddcbe44 --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/codefreeze/persistence/BranchFreeze.java @@ -0,0 +1,48 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.codefreeze.persistence; + +import net.java.ao.Accessor; +import net.java.ao.Entity; +import net.java.ao.Mutator; +import net.java.ao.Preload; +import net.java.ao.schema.*; + +import java.util.Date; + + +@Table("BranchFreeze") +@Preload +@Indexes( + @Index(name = "freezesettingsbranch", methodNames = {"getCodeFreezeSetting", "getBranchName"}) +) +public interface BranchFreeze extends Entity { + String COLUMN_CODE_FREEZE_SETTINGS_ID = "CODE_FREEZE_SETTING_ID"; + String COLUMN_BRANCHNAME = "BRANCHNAME"; + String COLUMN_DATE = "DATE"; + + CodeFreezeSetting getCodeFreezeSetting(); + void setCodeFreezeSetting(CodeFreezeSetting codeFreezeSetting); + + @Accessor(COLUMN_BRANCHNAME) + @StringLength(StringLength.MAX_LENGTH) + @NotNull + String getBranchName(); + + @Mutator(COLUMN_BRANCHNAME) + void setBranchName(String branchName); + + @Accessor(COLUMN_DATE) + Date getDate(); + + @Mutator(COLUMN_DATE) + void setDate(Date codefreezeDate); +} diff --git a/src/main/java/com/baloise/open/bitbucket/codefreeze/persistence/CodeFreezeSetting.java b/src/main/java/com/baloise/open/bitbucket/codefreeze/persistence/CodeFreezeSetting.java new file mode 100644 index 0000000..fc674ac --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/codefreeze/persistence/CodeFreezeSetting.java @@ -0,0 +1,34 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.codefreeze.persistence; + +import net.java.ao.Accessor; +import net.java.ao.Entity; +import net.java.ao.OneToMany; +import net.java.ao.Preload; +import net.java.ao.schema.Indexed; +import net.java.ao.schema.NotNull; +import net.java.ao.schema.Table; + + +@Table("FreezeConfig") +@Preload +public interface CodeFreezeSetting extends Entity{ + String COLUMN_REPO_ID = "REPO_ID"; + + @Accessor(COLUMN_REPO_ID) + @Indexed + @NotNull + Integer getRepositoryId(); + + @OneToMany + BranchFreeze[] getBranchFreezes(); +} diff --git a/src/main/java/com/baloise/open/bitbucket/codefreeze/persistence/CodeFreezeSettingDao.java b/src/main/java/com/baloise/open/bitbucket/codefreeze/persistence/CodeFreezeSettingDao.java new file mode 100644 index 0000000..6ae08f0 --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/codefreeze/persistence/CodeFreezeSettingDao.java @@ -0,0 +1,106 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.codefreeze.persistence; + +import com.atlassian.activeobjects.external.ActiveObjects; +import com.atlassian.bitbucket.repository.Repository; +import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; +import com.google.common.collect.ImmutableMap; +import net.java.ao.Query; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Date; + +import static com.baloise.open.bitbucket.codefreeze.persistence.BranchFreeze.COLUMN_DATE; +import static com.baloise.open.bitbucket.codefreeze.persistence.CodeFreezeSetting.COLUMN_REPO_ID; +import static com.baloise.open.bitbucket.codefreeze.persistence.BranchFreeze.COLUMN_CODE_FREEZE_SETTINGS_ID; +import static com.baloise.open.bitbucket.codefreeze.persistence.BranchFreeze.COLUMN_BRANCHNAME; + +@Component +public class CodeFreezeSettingDao{ + private final ActiveObjects activeObjects; + + @Autowired + public CodeFreezeSettingDao(@ComponentImport ActiveObjects activeObjects){ + this.activeObjects = activeObjects; + } + + public CodeFreezeSetting getCodeFreezeSettings(Repository repo){ + + CodeFreezeSetting[] results = activeObjects.find(CodeFreezeSetting.class, + Query.select().where(COLUMN_REPO_ID + " = ?", repo.getId())); + return results.length == 1 ? results[0] : null; + } + + public CodeFreezeSetting createOrUpdateCodeFreezeSetting(Repository repo){ + CodeFreezeSetting codeFreezeSetting = getCodeFreezeSettings(repo); + if(codeFreezeSetting == null){ + codeFreezeSetting = activeObjects.create(CodeFreezeSetting.class, ImmutableMap.builder() + .put(COLUMN_REPO_ID, repo.getId()) + .build()); + } + return codeFreezeSetting; + } + + public BranchFreeze createOrUpdateBranchFreeze(Repository repo, String branch, Date date){ + CodeFreezeSetting codeFreezeSetting = createOrUpdateCodeFreezeSetting(repo); + + BranchFreeze[] branchFreezes = activeObjects.find(BranchFreeze.class, + Query.select().where(COLUMN_CODE_FREEZE_SETTINGS_ID + " = ?" + + " AND " + + COLUMN_BRANCHNAME + " = ?" , + codeFreezeSetting.getID(), + branch + )); + + BranchFreeze branchFreeze = null; + if(branchFreezes.length > 0){ + branchFreeze = branchFreezes[0]; + } + + if(branchFreeze == null){ + branchFreeze = activeObjects.create(BranchFreeze.class, ImmutableMap.builder() + .put(COLUMN_CODE_FREEZE_SETTINGS_ID, codeFreezeSetting.getID()) + .put(COLUMN_BRANCHNAME, branch) + .put(COLUMN_DATE, date) + .build()); + } + branchFreeze.setDate(date); + branchFreeze.save(); + return branchFreeze; + } + + public void deleteBranchFreeze(Repository repo, String branch){ + CodeFreezeSetting codeFreezeSetting = createOrUpdateCodeFreezeSetting(repo); + BranchFreeze branchFreeze = activeObjects.find(BranchFreeze.class, + Query.select().where(COLUMN_CODE_FREEZE_SETTINGS_ID + " = ?" + + " AND " + + COLUMN_BRANCHNAME + " = ?" , + codeFreezeSetting.getID(), + branch + ))[0]; + + activeObjects.delete(branchFreeze); + } + + public BranchFreeze getBranchFreeze(Integer ID){ + BranchFreeze[] branchFreezes = activeObjects.find(BranchFreeze.class, + Query.select().where("ID" + " = ?" , + ID + )); + return branchFreezes.length > 0 ? branchFreezes[0] : null; + } + + public void deleteBranchFreeze(Integer branchID){ + activeObjects.delete(getBranchFreeze(branchID)); + } +} diff --git a/src/main/java/com/baloise/open/bitbucket/codefreeze/rest/BranchFreezeModel.java b/src/main/java/com/baloise/open/bitbucket/codefreeze/rest/BranchFreezeModel.java new file mode 100644 index 0000000..2487ba2 --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/codefreeze/rest/BranchFreezeModel.java @@ -0,0 +1,72 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.codefreeze.rest; + + +import com.baloise.open.bitbucket.codefreeze.persistence.BranchFreeze; +import org.apache.http.client.utils.DateUtils; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement +public class BranchFreezeModel { + @XmlElement + private Integer ID; + + @XmlElement + private String branch; + + @XmlElement + private String date; + + public BranchFreezeModel(){ + } + + public BranchFreezeModel(BranchFreeze branchFreeze){ + ID = branchFreeze.getID(); + branch = branchFreeze.getBranchName(); + date = DateUtils.formatDate(branchFreeze.getDate(), "YYYY-MM-dd"); + } + + + public Integer getID() { + return ID; + } + + public void setID(Integer ID) { + this.ID = ID; + } + + public String getBranch() { + return branch; + } + + public void setBranch(String branch) { + this.branch = branch; + } + + public String getDate() { + return date; + } + + public void setDate(String date) { + this.date = date; + } + + @Override + public String toString() { + return "BranchFreezeModel{" + + ", branch='" + branch + '\'' + + ", date=" + date + + '}'; + } +} diff --git a/src/main/java/com/baloise/open/bitbucket/codefreeze/rest/CodeFreezeResource.java b/src/main/java/com/baloise/open/bitbucket/codefreeze/rest/CodeFreezeResource.java new file mode 100644 index 0000000..917b3b6 --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/codefreeze/rest/CodeFreezeResource.java @@ -0,0 +1,122 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.codefreeze.rest; + +import com.atlassian.bitbucket.permission.Permission; +import com.atlassian.bitbucket.permission.PermissionService; +import com.atlassian.bitbucket.repository.Repository; +import com.atlassian.bitbucket.rest.util.ResourcePatterns; +import com.baloise.open.bitbucket.codefreeze.persistence.CodeFreezeSetting; +import com.baloise.open.bitbucket.codefreeze.persistence.CodeFreezeSettingDao; +import com.baloise.open.bitbucket.codefreeze.persistence.BranchFreeze; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.TimeZone; +import java.util.stream.Collectors; + +@Component +@Path(ResourcePatterns.REPOSITORY_URI + "/branchfreeze") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class CodeFreezeResource { + + private final com.baloise.open.bitbucket.codefreeze.persistence.CodeFreezeSettingDao CodeFreezeSettingDao; + private final PermissionService PermissionService; + + @Autowired + public CodeFreezeResource(CodeFreezeSettingDao CodeFreezeSettingDao, PermissionService PermissionService) { + this.CodeFreezeSettingDao = CodeFreezeSettingDao; + this.PermissionService = PermissionService; + } + + @GET + @Path("/") + public List getBranchFreezes(@Context Repository repo) { + CodeFreezeSetting codeFreezeSetting = CodeFreezeSettingDao.getCodeFreezeSettings(repo); + BranchFreeze[] frozenBranches = {}; + if (codeFreezeSetting != null) { + frozenBranches = codeFreezeSetting.getBranchFreezes(); + } + return Arrays.stream(frozenBranches) + .map(BranchFreezeModel::new) + .collect(Collectors.toList()); + } + + @POST + public BranchFreezeModel createBranchFreeze(@Context Repository repo, BranchFreezeModel newBranchFreeze) { + if (PermissionService.hasRepositoryPermission(repo, Permission.REPO_ADMIN)) { + return createOrUpdateBranchFreeze(repo, newBranchFreeze); + } else { + throw new WebApplicationException(Response.status(Response.Status.FORBIDDEN) + .entity("Insufficient permissions") + .build()); + } + } + + //PUT method for a create action is incorrect, but is kept for backwards compatibility + @PUT + public BranchFreezeModel createBranchFreezePUT(@Context Repository repo, BranchFreezeModel newBranchFreeze) { + if (PermissionService.hasRepositoryPermission(repo, Permission.REPO_ADMIN)) { + return createOrUpdateBranchFreeze(repo, newBranchFreeze); + } else { + throw new WebApplicationException(Response.status(Response.Status.FORBIDDEN) + .entity("Insufficient permissions") + .build()); + } + } + + @Path("/{branchFreezeId}") + @DELETE + public void removeBranchFreeze(@Context Repository repo, @PathParam("branchFreezeId") String branchfreezeId) { + if (PermissionService.hasRepositoryPermission(repo, Permission.REPO_ADMIN)) { + CodeFreezeSetting CodeFreezeSetting = CodeFreezeSettingDao.getCodeFreezeSettings(repo); + if (CodeFreezeSetting == null) { + throw new WebApplicationException(Response.status(Response.Status.NOT_FOUND) + .entity("Webhook not found") + .build()); + } + if (CodeFreezeSetting.getRepositoryId().equals(repo.getId())) { + CodeFreezeSettingDao.deleteBranchFreeze(Integer.valueOf(branchfreezeId)); + } + } else { + throw new WebApplicationException(Response.status(Response.Status.FORBIDDEN) + .entity("Insufficient permissions") + .build()); + } + } + + private BranchFreezeModel createOrUpdateBranchFreeze(Repository repo, + BranchFreezeModel updatedBranchFreeze) { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + sdf.setTimeZone(TimeZone.getTimeZone("UTC")); + Date parsedDate; + try{ + parsedDate = sdf.parse(updatedBranchFreeze.getDate()); + }catch (ParseException e){ + throw new WebApplicationException(Response.status(Response.Status.BAD_REQUEST) + .entity("Date wrong format") + .build()); + } + + BranchFreeze createdBranchFreeze = CodeFreezeSettingDao.createOrUpdateBranchFreeze(repo, updatedBranchFreeze.getBranch(), parsedDate); + return new BranchFreezeModel(createdBranchFreeze); + } +} diff --git a/src/main/java/com/baloise/open/bitbucket/codefreeze/servlet/CodeFreezeServlet.java b/src/main/java/com/baloise/open/bitbucket/codefreeze/servlet/CodeFreezeServlet.java new file mode 100644 index 0000000..52e979c --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/codefreeze/servlet/CodeFreezeServlet.java @@ -0,0 +1,206 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.codefreeze.servlet; + +import com.baloise.open.bitbucket.codefreeze.persistence.BranchFreeze; +import com.baloise.open.bitbucket.codefreeze.persistence.CodeFreezeSetting; +import com.baloise.open.bitbucket.codefreeze.persistence.CodeFreezeSettingDao; +import com.baloise.open.bitbucket.codefreeze.rest.BranchFreezeModel; +import com.baloise.open.bitbucket.common.servlet.AbstractSimpleServlet; +import com.atlassian.bitbucket.permission.Permission; +import com.atlassian.bitbucket.permission.PermissionService; +import com.atlassian.bitbucket.project.Project; +import com.atlassian.bitbucket.project.ProjectService; +import com.atlassian.bitbucket.repository.Repository; +import com.atlassian.bitbucket.repository.RepositoryService; +import com.atlassian.bitbucket.util.DateFormatter; +import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; +import com.atlassian.soy.renderer.SoyTemplateRenderer; +import com.google.common.collect.ImmutableMap; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.List; +import java.util.TimeZone; +import java.util.stream.Collectors; + + +public class CodeFreezeServlet extends AbstractSimpleServlet { + + RepositoryService repositoryService; + ProjectService projectService; + CodeFreezeSettingDao codeFreezeSettingDao; + PermissionService permissionService; + DateFormatter dateFormatter; + + Boolean isAdmin; + Repository repository; + CodeFreezeSetting codeFreezeSetting; + Project project; + + final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + + public CodeFreezeServlet(@ComponentImport SoyTemplateRenderer soyTemplateRenderer, @ComponentImport RepositoryService repositoryService, + @ComponentImport ProjectService projectService, @ComponentImport PermissionService permissionService, + CodeFreezeSettingDao codeFreezeSettingDao) { + super(soyTemplateRenderer); + this.repositoryService = repositoryService; + this.projectService = projectService; + this.codeFreezeSettingDao = codeFreezeSettingDao; + this.permissionService = permissionService; + sdf.setTimeZone(TimeZone.getTimeZone("UTC")); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + +// if(isSettings){ +// codeFreezeSettingDao.addOrUpdateBranchFreeze(repository, "9/0/0/master", new Date()); +// } +// String template = isSettings ? ".projectSettings" : "plugin.example.project"; + + req.getLocale(); + ImmutableMap.Builder parameters = ImmutableMap.builder(); + parameters.put("project", project) + .put("repository", repository); + + if (req.getParameter("edit") != null) { + if (!isAdmin) { + resp.sendError(HttpServletResponse.SC_UNAUTHORIZED); + } + String template = "codefreeze.templates.branchEdit"; + + if (req.getParameter("id") != null) { + BranchFreeze branchFreeze = codeFreezeSettingDao.getBranchFreeze(Integer.valueOf(req.getParameter("id"))); + BranchFreezeModel branchFreezeModel = new BranchFreezeModel(branchFreeze); + parameters.put("branch", branchFreezeModel); + } + + render(resp, template, parameters.build()); + } else { + if (req.getParameter("delete") != null) { + if (!isAdmin) { + resp.sendError(HttpServletResponse.SC_UNAUTHORIZED); + } else { + Integer idToDelete = Integer.valueOf(req.getParameter("id")); + codeFreezeSettingDao.deleteBranchFreeze(idToDelete); + resp.sendRedirect(req.getRequestURL().toString()); + } + } else { + String template = "codefreeze.templates.repositorySettings"; + BranchFreezeModel[] branchFreezes = convertBranchFreezesToModels(codeFreezeSetting.getBranchFreezes()); + render(resp, template, parameters + .put("branches", branchFreezes) + .put("isAdmin", isAdmin).build()); + } + } + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + if (!isAdmin) { + resp.sendError(HttpServletResponse.SC_UNAUTHORIZED); + } else { + Repository repository = getRepository(req); +// List queryParams = URLEncodedUtils.parse(getFullURL(req), "UTF-8"); + + CodeFreezeSetting codeFreezeSetting = codeFreezeSettingDao.createOrUpdateCodeFreezeSetting(repository); + java.util.Date parsedDate; + + try{ + parsedDate = sdf.parse(req.getParameter("date")); + }catch (ParseException e){ + throw new IOException("Error parsing date"); + } + + codeFreezeSettingDao.createOrUpdateBranchFreeze(repository, req.getParameter("branch"), + parsedDate); + + BranchFreezeModel[] branchFreezes = convertBranchFreezesToModels(codeFreezeSetting.getBranchFreezes()); + resp.sendRedirect(req.getRequestURL().toString()); + // render(resp, template, ImmutableMap.builder() +// .put("repository", repository) +// .put("branches", branchFreezes) +// .put("isAdmin",isAdmin).build()); + } + } + + public static URI getFullURL(HttpServletRequest request) throws ServletException { + StringBuffer requestURL = request.getRequestURL(); + String queryString = request.getQueryString(); + String url; + if (queryString == null) { + url = requestURL.toString(); + + } else { + url = requestURL.append('?').append(queryString).toString(); + } + try { + return new URI(url); + } catch (URISyntaxException e) { + throw new ServletException(e); + } + } + + private Repository getRepository(HttpServletRequest req) throws IOException { + // Get repoSlug from path + String pathInfo = req.getPathInfo(); + + String[] components = pathInfo.split("/"); + + if (components.length < 3) { + return null; + } + + Repository repository = repositoryService.getBySlug(components[1], components[2]); + if (repository == null) { + return null; + } + return repository; + } + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // Get projectKey from path + String pathInfo = req.getPathInfo(); + String[] components = pathInfo.split("/"); + + if (components.length < 2) { + resp.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + + project = projectService.getByKey(components[1]); + + if (project == null) { + resp.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + + repository = repositoryService.getBySlug(components[1], components[2]); + codeFreezeSetting = codeFreezeSettingDao.createOrUpdateCodeFreezeSetting(repository); + isAdmin = permissionService.hasRepositoryPermission(repository, Permission.REPO_ADMIN); + + super.service(req, resp); + } + + private BranchFreezeModel[] convertBranchFreezesToModels(BranchFreeze[] branchFreezes) { + List list = Arrays.stream(branchFreezes).map(branchFreeze -> new BranchFreezeModel(branchFreeze)).collect(Collectors.toList()); + return list.toArray(new BranchFreezeModel[list.size()]); + } +} diff --git a/src/main/java/com/baloise/open/bitbucket/codefreeze/util/FrozenBranchUtil.java b/src/main/java/com/baloise/open/bitbucket/codefreeze/util/FrozenBranchUtil.java new file mode 100644 index 0000000..bb710bd --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/codefreeze/util/FrozenBranchUtil.java @@ -0,0 +1,30 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.codefreeze.util; + +import com.baloise.open.bitbucket.codefreeze.persistence.BranchFreeze; +import com.baloise.open.bitbucket.codefreeze.persistence.CodeFreezeSetting; + +import java.util.Arrays; +import java.util.Date; + +public abstract class FrozenBranchUtil { + + public static Boolean isBranchFrozen(String branch, CodeFreezeSetting codeFreezeSetting) { + if (codeFreezeSetting == null) { + return Boolean.FALSE; + } else { + BranchFreeze[] frozenBranches = codeFreezeSetting.getBranchFreezes(); + return Arrays.stream(frozenBranches).anyMatch(branchFreeze -> branchFreeze.getBranchName().contains(branch) + && new Date().after(branchFreeze.getDate())); + } + } +} diff --git a/src/main/java/com/baloise/open/bitbucket/common/files/FileUtils.java b/src/main/java/com/baloise/open/bitbucket/common/files/FileUtils.java new file mode 100644 index 0000000..808db87 --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/common/files/FileUtils.java @@ -0,0 +1,32 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.common.files; + +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class FileUtils { + public String loadFileAsString(String path){ + String fileContents = null; + try { + InputStream in = getClass().getResourceAsStream(path); + fileContents = new String(IOUtils.toByteArray(in)); + } catch (IOException e) { + e.printStackTrace(); + } + return fileContents; + } +} diff --git a/src/main/java/com/baloise/open/bitbucket/common/groovy/GroovySandbox.java b/src/main/java/com/baloise/open/bitbucket/common/groovy/GroovySandbox.java new file mode 100644 index 0000000..b557151 --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/common/groovy/GroovySandbox.java @@ -0,0 +1,91 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.common.groovy; + + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import groovy.lang.Binding; +import groovy.lang.Closure; +import groovy.lang.GroovyShell; +import groovy.lang.Script; +import org.codehaus.groovy.control.CompilerConfiguration; +import org.kohsuke.groovy.sandbox.GroovyValueFilter; +import org.kohsuke.groovy.sandbox.SandboxTransformer; + +import java.util.HashSet; +import java.util.Set; + +public class GroovySandbox { + + private static final Set BASE_ALLOWED_TYPES = new HashSet<>(); + private static final Set EXTENDED_ALLOWED_TYPES = new HashSet<>(); + + + public GroovySandbox(){ + setBaseAllowedTypes(); + } + + private void setBaseAllowedTypes() { + BASE_ALLOWED_TYPES.add(String.class); + BASE_ALLOWED_TYPES.add(JsonObject.class); + BASE_ALLOWED_TYPES.add(JsonArray.class); + BASE_ALLOWED_TYPES.add(JsonPrimitive.class); + BASE_ALLOWED_TYPES.add(JsonElement.class); + BASE_ALLOWED_TYPES.add(Integer.class); + BASE_ALLOWED_TYPES.add(Boolean.class); + BASE_ALLOWED_TYPES.add(Character.class); + + BASE_ALLOWED_TYPES.add(java.lang.Class.class); + BASE_ALLOWED_TYPES.add(java.util.regex.Matcher.class); + BASE_ALLOWED_TYPES.add(java.util.ArrayList.class); + BASE_ALLOWED_TYPES.add(java.lang.String[].class); + BASE_ALLOWED_TYPES.add(java.lang.Object[].class); + BASE_ALLOWED_TYPES.add(org.codehaus.groovy.runtime.GStringImpl.class); + } + + public GroovyShell createSandbox(Binding binding){ + return createSandbox(binding, true); + } + + public GroovyShell createSandbox(Binding binding, Boolean useFiltering){ + CompilerConfiguration cc = new CompilerConfiguration(); + cc.addCompilationCustomizers(new SandboxTransformer()); + //new Binding + GroovyShell sh = new GroovyShell(binding, cc); + + GroovyValueFilter gvf = new GroovyValueFilter(){ + public Object filter(Object o){ return o;}}; + + if(useFiltering) { + gvf = new GroovyValueFilter() { + public Object filter(Object o) { + if (o == null || BASE_ALLOWED_TYPES.contains(o.getClass())) { + return o; + } + if (o instanceof Script || o instanceof Closure) { + return o; + } + if (EXTENDED_ALLOWED_TYPES.contains(o.getClass().getCanonicalName())) { + return o; + } + throw new SecurityException("Denied:" + o.getClass()); + } + }; + } + + gvf.register(); + + return sh; + } +} diff --git a/src/main/java/com/baloise/open/bitbucket/common/groovy/ScriptExecutionException.java b/src/main/java/com/baloise/open/bitbucket/common/groovy/ScriptExecutionException.java new file mode 100644 index 0000000..2f34e56 --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/common/groovy/ScriptExecutionException.java @@ -0,0 +1,21 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.common.groovy; + +public class ScriptExecutionException extends Exception { + ScriptExecutionException(){ + super(); + } + + ScriptExecutionException(String message, Throwable cause){ + super(message, cause); + } +} diff --git a/src/main/java/com/baloise/open/bitbucket/common/groovy/ScriptHelper.java b/src/main/java/com/baloise/open/bitbucket/common/groovy/ScriptHelper.java new file mode 100644 index 0000000..2c0ab83 --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/common/groovy/ScriptHelper.java @@ -0,0 +1,54 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.common.groovy; + +import groovy.lang.Binding; +import groovy.lang.GroovyShell; +import org.codehaus.groovy.control.CompilationFailedException; + +import java.util.HashMap; +import java.util.function.BiConsumer; + +public class ScriptHelper { + public static String validateScript(String script) { + GroovyShell sh = new GroovySandbox().createSandbox(new Binding()); + String errorMessages = null; + try { + sh.parse(script); + } catch (CompilationFailedException exception) { + errorMessages = exception.getMessage(); + } + return errorMessages; + } + + public static Object executeScript(String script, HashMap variables) throws ScriptExecutionException { + return executeScript(script, variables, false); + } + + public static Object executeScript(String script, HashMap variables, Boolean bypassSecurity) throws ScriptExecutionException { + Object executionResult = null; + Binding groovyBinding = new Binding(); + variables.forEach(new BiConsumer() { + @Override + public void accept(String s, Object o) { + groovyBinding.setVariable(s, o); + } + }); + + GroovyShell sh = new GroovySandbox().createSandbox(groovyBinding, !bypassSecurity); + try { + executionResult = sh.evaluate(script); + } catch (Exception exception) { + throw new ScriptExecutionException("Error during script execution", exception); + } + return executionResult; + } +} diff --git a/src/main/java/com/baloise/open/bitbucket/common/integration/jira/FieldRetrieveHandler.java b/src/main/java/com/baloise/open/bitbucket/common/integration/jira/FieldRetrieveHandler.java new file mode 100644 index 0000000..b7dc24a --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/common/integration/jira/FieldRetrieveHandler.java @@ -0,0 +1,55 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.common.integration.jira; + +import com.baloise.open.bitbucket.common.json.JsonParser; +import com.atlassian.applinks.api.ApplicationLinkResponseHandler; +import com.atlassian.sal.api.net.Response; +import com.atlassian.sal.api.net.ResponseException; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import org.apache.log4j.Logger; + + +import java.util.ArrayList; +import java.util.List; + +public class FieldRetrieveHandler implements ApplicationLinkResponseHandler> { + final Logger log = Logger.getLogger(this.getClass().getCanonicalName()); + + @Override + public List credentialsRequired(Response response) throws ResponseException { + return null; + } + + @Override + public List handle(Response response) throws ResponseException { + String responseBody = response.getResponseBodyAsString(); + if(response.getStatusCode() != 200){ + throw new ResponseException("Response invalid"); + } + List simpleJiraIssueFields = new ArrayList<>(); + JsonArray ja = JsonParser.parseJsonArray(responseBody); + ja.forEach(jsonElement -> { + if(jsonElement.isJsonObject()){ + JsonObject jsonObject = jsonElement.getAsJsonObject(); + try { + SimpleJiraIssueField issueField = new SimpleJiraIssueField(jsonObject.get("id").getAsString(), jsonObject.get("name").getAsString()); + simpleJiraIssueFields.add(issueField); + }catch (Exception e){ + log.error("Error parsing fields:"+jsonObject.toString()+"\n"+ + e.getMessage()); + } + } + }); + return simpleJiraIssueFields; + } +} diff --git a/src/main/java/com/baloise/open/bitbucket/common/integration/jira/IssueFieldsRetrieveHandler.java b/src/main/java/com/baloise/open/bitbucket/common/integration/jira/IssueFieldsRetrieveHandler.java new file mode 100644 index 0000000..74adaed --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/common/integration/jira/IssueFieldsRetrieveHandler.java @@ -0,0 +1,35 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.common.integration.jira; + + +import com.baloise.open.bitbucket.common.json.JsonParser; +import com.atlassian.applinks.api.ApplicationLinkResponseHandler; +import com.atlassian.sal.api.net.Response; +import com.atlassian.sal.api.net.ResponseException; +import com.google.gson.JsonObject; + + +public class IssueFieldsRetrieveHandler implements ApplicationLinkResponseHandler { + + @Override + public JsonObject credentialsRequired(Response response) throws ResponseException { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("name", "CredentialsRequiredException"); + return jsonObject; + } + + @Override + public JsonObject handle(Response response) throws ResponseException { + String responseBody = response.getResponseBodyAsString(); + return JsonParser.parseJsonObject(responseBody); + } +} diff --git a/src/main/java/com/baloise/open/bitbucket/common/integration/jira/JiraIntegrationUtil.java b/src/main/java/com/baloise/open/bitbucket/common/integration/jira/JiraIntegrationUtil.java new file mode 100644 index 0000000..221965d --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/common/integration/jira/JiraIntegrationUtil.java @@ -0,0 +1,65 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.common.integration.jira; + +import com.atlassian.applinks.api.*; +import com.atlassian.applinks.api.application.jira.JiraApplicationType; +import com.atlassian.applinks.api.auth.types.OAuthAuthenticationProvider; +import com.atlassian.sal.api.net.Request; +import com.atlassian.sal.api.net.ResponseException; +import com.google.gson.JsonObject; +import org.apache.log4j.Logger; + +import java.util.List; + +public class JiraIntegrationUtil { + final Logger log = Logger.getLogger(this.getClass().getName()); + private ApplicationLink appLink; + private ApplicationLinkRequestFactory requestFactory; + + public JiraIntegrationUtil(ApplicationLinkService applicationLinkService) { + appLink = applicationLinkService.getPrimaryApplicationLink(JiraApplicationType.class); + requestFactory = appLink.createAuthenticatedRequestFactory(OAuthAuthenticationProvider.class); + } + + public List getAvailableFields() throws CredentialsRequiredException { + ApplicationLinkRequest request = requestFactory.createRequest(Request.MethodType.GET, getURLForFields()); + List results = null; + try { + results = request.execute(new FieldRetrieveHandler()); + } catch (ResponseException e) { + log.error(e.getMessage()); + } + return results; + } + + public JsonObject getIssueFields(String key, String finalFieldsIds) throws CredentialsRequiredException, ResponseException { + JsonObject jsonIssue = null; + ApplicationLinkRequest applicationLinkRequest = requestFactory.createRequest(Request.MethodType.GET, getURLForSpecificIssue(key, finalFieldsIds)); + jsonIssue = applicationLinkRequest.execute(new IssueFieldsRetrieveHandler()); + return jsonIssue; + } + + private String getURLForFields() { + return "/rest/api/latest/field"; + } + + private String getURLForSpecificIssue(String issueKey, String fields) { + String url = "rest/api/latest/issue/" + issueKey; + //Check for settings of fields + if (fields != null && !fields.isEmpty()) { + url = url + "?" + fields; + } + return url; + } + + +} diff --git a/src/main/java/com/baloise/open/bitbucket/common/integration/jira/JiraProxyResource.java b/src/main/java/com/baloise/open/bitbucket/common/integration/jira/JiraProxyResource.java new file mode 100644 index 0000000..3834ceb --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/common/integration/jira/JiraProxyResource.java @@ -0,0 +1,67 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.common.integration.jira; + +import com.atlassian.applinks.api.ApplicationLinkService; +import com.atlassian.applinks.api.CredentialsRequiredException; +import com.atlassian.bitbucket.permission.PermissionService; +import com.atlassian.bitbucket.user.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.List; + + +@Component +@Path("/jiraproxy") +@Produces(MediaType.APPLICATION_JSON) +public class JiraProxyResource { + private final PermissionService PermissionService; + private final UserService UserService; + private final ApplicationLinkService ApplicationLinkService; + + @Autowired + public JiraProxyResource(PermissionService PermissionService, UserService UserSetting, ApplicationLinkService applicationLinkService) { + this.PermissionService = PermissionService; + this.UserService = UserSetting; + this.ApplicationLinkService = applicationLinkService; + } + + @GET + @Path("/field") + public Response get(@QueryParam("filter") String filter) throws CredentialsRequiredException { + String filterNormalized; + if(filter != null){ + filterNormalized = filter.toLowerCase(); + }else{ + filterNormalized = ""; + } + + List result = null; + try { + JiraIntegrationUtil jiraIntegrationUtil = new JiraIntegrationUtil(ApplicationLinkService); + result = jiraIntegrationUtil.getAvailableFields(); + } catch (CredentialsRequiredException e) { + Response resp = Response.status(Response.Status.UNAUTHORIZED).contentLocation(e.getAuthorisationURI()).entity("{\"errors\":[{\"status\":\"401\",\"title\":\"Unauthorized\",\"authenticationOption\": \""+e.getAuthorisationURI().toASCIIString()+"\"}]}").build(); + return resp; + } + if(!filterNormalized.isEmpty() && result != null) { + result.removeIf(simpleJiraIssueField -> !simpleJiraIssueField.text.toLowerCase().contains(filterNormalized) && !simpleJiraIssueField.id.toLowerCase().contains(filterNormalized)); + } + return Response.ok(result).build(); + } +} diff --git a/src/main/java/com/baloise/open/bitbucket/common/integration/jira/SimpleJiraIssueField.java b/src/main/java/com/baloise/open/bitbucket/common/integration/jira/SimpleJiraIssueField.java new file mode 100644 index 0000000..e16ffe7 --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/common/integration/jira/SimpleJiraIssueField.java @@ -0,0 +1,40 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.common.integration.jira; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Simple container describing field for UI needs + */ +@XmlRootElement +public class SimpleJiraIssueField { + + @XmlElement + String id; + + @XmlElement + String text; + + SimpleJiraIssueField(String id, String text){ + this.id = id; + this.text = text; + } + + public String getId(){ + return id; + } + + public String getText(){ + return text; + } +} diff --git a/src/main/java/com/baloise/open/bitbucket/common/json/JsonParser.java b/src/main/java/com/baloise/open/bitbucket/common/json/JsonParser.java new file mode 100644 index 0000000..9ab314d --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/common/json/JsonParser.java @@ -0,0 +1,30 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.common.json; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +public class JsonParser { + private static JsonElement parse(String jsonString){ + com.google.gson.JsonParser jp = new com.google.gson.JsonParser(); + return jp.parse(jsonString); + } + + public static JsonArray parseJsonArray(String jsonString){ + return parse(jsonString).getAsJsonArray(); + } + + public static JsonObject parseJsonObject(String jsonString){ + return parse(jsonString).getAsJsonObject(); + } +} diff --git a/src/main/java/com/baloise/open/bitbucket/common/servlet/AbstractSimpleServlet.java b/src/main/java/com/baloise/open/bitbucket/common/servlet/AbstractSimpleServlet.java new file mode 100644 index 0000000..a2070c3 --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/common/servlet/AbstractSimpleServlet.java @@ -0,0 +1,45 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.common.servlet; + +import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; +import com.atlassian.soy.renderer.SoyException; +import com.atlassian.soy.renderer.SoyTemplateRenderer; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Map; + +public abstract class AbstractSimpleServlet extends HttpServlet { + protected final SoyTemplateRenderer soyTemplateRenderer; + + public AbstractSimpleServlet(@ComponentImport SoyTemplateRenderer soyTemplateRenderer) { + this.soyTemplateRenderer = soyTemplateRenderer; + } + + protected void render(HttpServletResponse resp, String templateName, Map data) throws IOException, ServletException { + resp.setContentType("text/html;charset=UTF-8"); + try { + soyTemplateRenderer.render(resp.getWriter(), + "com.baloise.open.bitbucket.codefreeze:codefreeze-soy", + templateName, + data); + } catch (SoyException e) { + Throwable cause = e.getCause(); + if (cause instanceof IOException) { + throw (IOException) cause; + } + throw new ServletException(e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/baloise/open/bitbucket/issuechecker/condition/IsIssueCheckerEnabledCondition.java b/src/main/java/com/baloise/open/bitbucket/issuechecker/condition/IsIssueCheckerEnabledCondition.java new file mode 100644 index 0000000..d23363a --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/issuechecker/condition/IsIssueCheckerEnabledCondition.java @@ -0,0 +1,57 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.issuechecker.condition; + +import com.atlassian.applinks.api.ApplicationId; +import com.atlassian.applinks.api.ApplicationLink; +import com.atlassian.applinks.api.ApplicationLinkService; +import com.atlassian.applinks.api.application.jira.JiraApplicationType; +import com.atlassian.bitbucket.hook.repository.RepositoryHook; +import com.atlassian.bitbucket.hook.repository.RepositoryHookService; +import com.atlassian.bitbucket.repository.Repository; +import com.atlassian.bitbucket.scope.RepositoryScope; +import com.atlassian.bitbucket.scope.Scope; +import com.atlassian.plugin.PluginParseException; +import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; +import com.atlassian.plugin.web.Condition; + +import java.util.Map; + +public class IsIssueCheckerEnabledCondition implements Condition { + final RepositoryHookService repositoryHookService; + final ApplicationLinkService applicationLinkService; + + public IsIssueCheckerEnabledCondition(@ComponentImport RepositoryHookService repositoryHookService, + @ComponentImport ApplicationLinkService applicationLinkService){ + this.repositoryHookService = repositoryHookService; + this.applicationLinkService = applicationLinkService; + } + + public void init(Map map) throws PluginParseException { + } + + public boolean shouldDisplay(Map params) { + ApplicationLink jiraLink = applicationLinkService.getPrimaryApplicationLink(JiraApplicationType.class); + if(jiraLink == null){ + return false; + } + Boolean mergeCheckActive = Boolean.FALSE; + + Repository repo = (Repository) params.get("repository"); + if(repo != null) { + Scope scope = new RepositoryScope(repo); + + RepositoryHook mergeHook = repositoryHookService.getByKey(scope, "ch.baloise.bitbucket.codefreeze:IsIssueKeyCorrect"); + mergeCheckActive = mergeHook != null && mergeHook.isEnabled(); + } + return mergeCheckActive; + } +} diff --git a/src/main/java/com/baloise/open/bitbucket/issuechecker/hooks/IsIssueKeyCorrect.java b/src/main/java/com/baloise/open/bitbucket/issuechecker/hooks/IsIssueKeyCorrect.java new file mode 100644 index 0000000..8f292dc --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/issuechecker/hooks/IsIssueKeyCorrect.java @@ -0,0 +1,173 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.issuechecker.hooks; + +import com.baloise.open.bitbucket.common.groovy.ScriptExecutionException; +import com.baloise.open.bitbucket.common.groovy.ScriptHelper; +import com.baloise.open.bitbucket.common.integration.jira.JiraIntegrationUtil; +import com.baloise.open.bitbucket.issuechecker.persistence.BranchIssueChecker; +import com.baloise.open.bitbucket.issuechecker.persistence.IssueCheckerSetting; +import com.baloise.open.bitbucket.issuechecker.persistence.IssueCheckerSettingDao; +import com.baloise.open.bitbucket.issuechecker.persistence.WhiteListGroup; +import com.atlassian.applinks.api.*; +import com.atlassian.bitbucket.auth.AuthenticationContext; +import com.atlassian.bitbucket.hook.repository.PreRepositoryHookContext; +import com.atlassian.bitbucket.hook.repository.PullRequestMergeHookRequest; +import com.atlassian.bitbucket.hook.repository.RepositoryHookResult; +import com.atlassian.bitbucket.hook.repository.RepositoryMergeCheck; +import com.atlassian.bitbucket.integration.jira.JiraIssueService; +import com.atlassian.bitbucket.permission.PermissionService; +import com.atlassian.bitbucket.pull.PullRequest; +import com.atlassian.bitbucket.pull.PullRequestRef; +import com.atlassian.bitbucket.repository.Repository; +import com.atlassian.bitbucket.repository.StandardRefType; +import com.atlassian.bitbucket.user.ApplicationUser; +import com.atlassian.bitbucket.user.UserService; +import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; +import com.atlassian.sal.api.net.ResponseException; +import com.google.gson.JsonObject; +import org.apache.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.Nonnull; +import java.util.*; + +@Component("IsIssueKeyCorrect") +public class IsIssueKeyCorrect implements RepositoryMergeCheck { + final Logger log = Logger.getLogger(this.getClass().getName()); + + private final PermissionService permissionService; + private final IssueCheckerSettingDao issueCheckerSettingDao; + private final JiraIssueService jiraIssueService; + private final ApplicationLinkService applicationLinkService; + private final UserService userService; + private final AuthenticationContext authenticationContext; + + private static final String SHORTMSG = "Jira issues not meeting criteria"; + + + @Autowired + public IsIssueKeyCorrect(@ComponentImport PermissionService permissionService, + IssueCheckerSettingDao issueCheckerSettingDao, @ComponentImport JiraIssueService jiraIssueService, + @ComponentImport ApplicationLinkService applicationLinkService, + @ComponentImport UserService userService, @ComponentImport AuthenticationContext authenticationContext) { + this.permissionService = permissionService; + this.issueCheckerSettingDao = issueCheckerSettingDao; + this.jiraIssueService = jiraIssueService; + this.applicationLinkService = applicationLinkService; + this.userService = userService; + this.authenticationContext = authenticationContext; + } + + + @Nonnull + @Override + public RepositoryHookResult preUpdate(@Nonnull PreRepositoryHookContext context, + @Nonnull PullRequestMergeHookRequest request) { + PullRequest pullRequest = request.getPullRequest(); + Repository repository = pullRequest.getToRef().getRepository(); + Repository repo = request.getRepository(); + IssueCheckerSetting settings = issueCheckerSettingDao.getIssueCheckerSettings(repo); + BranchIssueChecker[] branches = null; + + if (settings != null) { + branches = settings.getBranches(); + } + PullRequestRef targetRef = pullRequest.getToRef(); + Map> validationErrors = null; + + Boolean mergeCheckConfigured = settings != null && + (settings.getScript() != null || settings.getScript().isEmpty()); + + if (mergeCheckConfigured && userIsNotWhitelisted(authenticationContext.getCurrentUser(), repo) && targetRef.getType() == StandardRefType.BRANCH && isBranchProtected(targetRef)) { + java.util.Set issues + = jiraIssueService.getIssuesForPullRequest(repository.getId(), pullRequest.getId()); + + validationErrors = (validateIssues(issues, request.getFromRef().getDisplayId(), request.getToRef().getDisplayId(), settings)); + } + + if(validationErrors!= null && !validationErrors.isEmpty()){ + StringJoiner joiner = new StringJoiner("\n"); + validationErrors.forEach((key, message) -> joiner.add(key + ":" + message)); + return RepositoryHookResult.rejected(SHORTMSG, joiner.toString()); + } + return RepositoryHookResult.accepted(); + } + + private boolean userIsNotWhitelisted(ApplicationUser currentUser, Repository repository) { + IssueCheckerSetting issueCheckerSetting = issueCheckerSettingDao.getOrCreateIssueCheckerSetting(repository); + if (Arrays.asList(issueCheckerSetting.getWhiteListUsers()).stream().anyMatch(whiteListUser -> whiteListUser.getUserID() == Integer.toString(currentUser.getId()))) { + return false; + } + for (WhiteListGroup whiteListGroup : issueCheckerSetting.getWhiteListGroups()) { + if (userService.isUserInGroup(currentUser, whiteListGroup.getName())) { + return false; + } + } + return true; + } + + + private Boolean isBranchProtected(PullRequestRef pullRequestRef) { + String branch = pullRequestRef.getDisplayId(); + IssueCheckerSetting setting = issueCheckerSettingDao.getIssueCheckerSettings(pullRequestRef.getRepository()); + return Arrays.stream(setting.getBranches()).anyMatch(branchIssueChecker -> branch.matches(branchIssueChecker.getBranchName())); + } + + private Map> validateIssues(java.util.Set issues, String fromRef, String toRef, IssueCheckerSetting settings) { + + String fields = settings.getIssueFields(); + Map> validationErrors = new HashMap<>(); + + if (fields == null || fields.isEmpty()) { + fields = "fixVersions"; + } + + final String finalFieldsIds = fields; + issues.forEach(jiraIssue -> { + Collection errorMessages = new ArrayList<>(); + try { + JiraIntegrationUtil jiraIntegrationUtil = new JiraIntegrationUtil(applicationLinkService); + JsonObject jsonIssue = jiraIntegrationUtil.getIssueFields(jiraIssue.getKey(), finalFieldsIds); + + HashMap variables = new HashMap<>(); + variables.put("issue", jsonIssue); + variables.put("fromRef", fromRef); + variables.put("toRef", toRef); + String result = null; + Boolean isScriptLimited = settings.getIsScriptLimited() != null && settings.getIsScriptLimited() == true; + try { + log.debug("issue:"+ jiraIssue.getKey()+"fromRef:"+fromRef+" toRef:"+toRef); + log.debug("Script:\n"+settings.getScript()); + result = (String) ScriptHelper.executeScript(settings.getScript(), variables, !isScriptLimited); + } catch (ScriptExecutionException scriptError) { + errorMessages.add(scriptError.getMessage()); + log.error(scriptError.toString()); + } + if (result != null && !result.isEmpty()) { + errorMessages.add(result); + } + } catch (CredentialsRequiredException e) { + log.error("Authorisation error"); + errorMessages.add("Authorisation is required for IssueCheckerPlugin please follow:\n" + + " " + e.getAuthorisationURI()); + } catch (ResponseException e) { + errorMessages.add("Could not validate"); + log.error(e.toString()); + } + if (!errorMessages.isEmpty()) { + validationErrors.put(jiraIssue.getKey(), errorMessages); + } + }); + return validationErrors; + } +} diff --git a/src/main/java/com/baloise/open/bitbucket/issuechecker/persistence/BranchIssueChecker.java b/src/main/java/com/baloise/open/bitbucket/issuechecker/persistence/BranchIssueChecker.java new file mode 100644 index 0000000..57b2120 --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/issuechecker/persistence/BranchIssueChecker.java @@ -0,0 +1,40 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.issuechecker.persistence; + + +import net.java.ao.*; +import net.java.ao.schema.*; + +@Table("BranchIssueChecker") +@Preload +@Indexes( + @Index(name = "issuecheckersettingsbranch", methodNames = {"getIssueCheckerSetting", "getBranchName"}) +) +public interface BranchIssueChecker extends Entity { + String COLUMN_ISSUE_CHECKER_SETTING_ID = "ISSUE_CHECKER_SETTING_ID"; + String COLUMN_BRANCHNAME = "BRANCHNAME"; + + IssueCheckerSetting getIssueCheckerSetting(); + void setIssueCheckerSetting(IssueCheckerSetting issueCheckerSetting); + + @Accessor(COLUMN_BRANCHNAME) + @StringLength(StringLength.MAX_LENGTH) + @NotNull + String getBranchName(); + + @Mutator(COLUMN_BRANCHNAME) + void setBranchName(String branchName); + + @OneToMany + WhiteListUser[] getWhiteListUsers(); + +} diff --git a/src/main/java/com/baloise/open/bitbucket/issuechecker/persistence/FieldIssueChecker.java b/src/main/java/com/baloise/open/bitbucket/issuechecker/persistence/FieldIssueChecker.java new file mode 100644 index 0000000..5445595 --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/issuechecker/persistence/FieldIssueChecker.java @@ -0,0 +1,47 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.issuechecker.persistence; + +import net.java.ao.Accessor; +import net.java.ao.Entity; +import net.java.ao.Mutator; +import net.java.ao.Preload; +import net.java.ao.schema.*; + +@Table("FieldIssueChecker") +@Preload +@Indexes( + @Index(name = "issuecheckersettingsfield", methodNames = {"getIssueCheckerSetting", "getFieldId"}) +) +public interface FieldIssueChecker extends Entity { + String COLUMN_ISSUE_CHECKER_SETTING_ID = "ISSUE_CHECKER_SETTING_ID"; + String COLUMN_FIELD_ID = "FIELD_ID"; + String COLUMN_FIELD_NAME = "FIELD_NAME"; + + IssueCheckerSetting getIssueCheckerSetting(); + void setIssueCheckerSetting(IssueCheckerSetting issueCheckerSetting); + + @Accessor(COLUMN_FIELD_ID) + @StringLength(40) + @NotNull + String getFieldId(); + + @Mutator(COLUMN_FIELD_ID) + void setFieldId(String branchName); + + @Accessor(COLUMN_FIELD_NAME) + @StringLength(StringLength.MAX_LENGTH) + @NotNull + String getFieldName(); + + @Mutator(COLUMN_FIELD_NAME) + void setFieldName(String setFieldName); +} diff --git a/src/main/java/com/baloise/open/bitbucket/issuechecker/persistence/IssueCheckerSetting.java b/src/main/java/com/baloise/open/bitbucket/issuechecker/persistence/IssueCheckerSetting.java new file mode 100644 index 0000000..1a9d504 --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/issuechecker/persistence/IssueCheckerSetting.java @@ -0,0 +1,61 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.issuechecker.persistence; + +import net.java.ao.*; +import net.java.ao.schema.Indexed; +import net.java.ao.schema.NotNull; +import net.java.ao.schema.StringLength; +import net.java.ao.schema.Table; + + +@Table("IssueCheckerConfig") +@Preload +public interface IssueCheckerSetting extends Entity{ + String COLUMN_REPO_ID = "REPO_ID"; + String COLUMN_ISSUE_FIELDS = "FIELDS"; + String COLUMN_SCRIPT = "SCRIPT"; + String COLUMN_IS_SCRIPT_LIMITED = "IS_SCRIPT_LIMITED"; + + @Accessor(COLUMN_REPO_ID) + @Indexed + @NotNull + Integer getRepositoryId(); + + @OneToMany + BranchIssueChecker[] getBranches(); + + @Accessor(COLUMN_ISSUE_FIELDS) + @StringLength(StringLength.UNLIMITED) + String getIssueFields(); + + @Mutator(COLUMN_ISSUE_FIELDS) + void setIssueFields(String issueFields); + + @Accessor(COLUMN_SCRIPT) + @StringLength(StringLength.UNLIMITED) + String getScript(); + + @Mutator(COLUMN_SCRIPT) + void setScript(String script); + + @Accessor(COLUMN_IS_SCRIPT_LIMITED) + Boolean getIsScriptLimited(); + + @Mutator(COLUMN_IS_SCRIPT_LIMITED) + void setIsScriptLimited(Boolean isScriptLimited); + + @OneToMany + WhiteListUser[] getWhiteListUsers(); + + @OneToMany + WhiteListGroup[] getWhiteListGroups(); +} diff --git a/src/main/java/com/baloise/open/bitbucket/issuechecker/persistence/IssueCheckerSettingDao.java b/src/main/java/com/baloise/open/bitbucket/issuechecker/persistence/IssueCheckerSettingDao.java new file mode 100644 index 0000000..7999b1f --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/issuechecker/persistence/IssueCheckerSettingDao.java @@ -0,0 +1,147 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.issuechecker.persistence; + +import com.atlassian.activeobjects.external.ActiveObjects; +import com.atlassian.bitbucket.repository.Repository; +import com.atlassian.bitbucket.user.UserService; +import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; +import com.google.common.collect.ImmutableMap; +import net.java.ao.Query; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + + +@Component +public class IssueCheckerSettingDao { + private final ActiveObjects activeObjects; + private final UserService userService; + + @Autowired + public IssueCheckerSettingDao(@ComponentImport ActiveObjects activeObjects, @ComponentImport UserService userService) { + this.activeObjects = activeObjects; + this.userService = userService; + } + + public IssueCheckerSetting getIssueCheckerSettings(Repository repo) { + + IssueCheckerSetting[] results = activeObjects.find(IssueCheckerSetting.class, + Query.select().where(IssueCheckerSetting.COLUMN_REPO_ID + " = ?", repo.getId())); + return results.length == 1 ? results[0] : null; + } + + public IssueCheckerSetting getOrCreateIssueCheckerSetting(Repository repo) { + IssueCheckerSetting IssueCheckerSetting = getIssueCheckerSettings(repo); + if (IssueCheckerSetting == null) { + IssueCheckerSetting = activeObjects.create(IssueCheckerSetting.class, ImmutableMap.builder() + .put(com.baloise.open.bitbucket.issuechecker.persistence.IssueCheckerSetting.COLUMN_REPO_ID, repo.getId()) + .build()); + } + return IssueCheckerSetting; + } + + /** + * @param id id of the branch to delete + * @return true upon successful removal of the branch + */ + public boolean deleteBranchIssueChecker(Integer id) { + BranchIssueChecker[] branchIssueChecker = activeObjects.find(BranchIssueChecker.class, + Query.select().where("ID = ?", + id + )); + + if (branchIssueChecker.length == 0) { + return false; + } else { + try { + activeObjects.delete(branchIssueChecker[0]); + return true; + }catch(Exception e){ + return false; + } + } + } + + public void saveUserWhitelist(Repository repo, List userIDs) { + IssueCheckerSetting issueCheckerSetting = getOrCreateIssueCheckerSetting(repo); + + List existingIds = Arrays.stream(issueCheckerSetting.getWhiteListUsers()).map(WhiteListUser::getUserID).collect(Collectors.toList()); + List idsToRemove = new ArrayList<>(existingIds); + List idsToAdd = new ArrayList<>(userIDs); + idsToRemove.removeIf(s -> userIDs.contains(s)); + + idsToAdd.removeIf(s -> existingIds.contains(s) || s.isEmpty()); + + if (!idsToRemove.isEmpty()) { + activeObjects.deleteWithSQL(WhiteListUser.class, WhiteListUser.COLUMN_ISSUE_CHECKER_SETTING_ID + " = ?" + + " AND " + WhiteListUser.COLUMN_USER_ID + " IN (?)", issueCheckerSetting.getID(), + String.join(",", idsToRemove)); + } + for (String userId : idsToAdd) { + WhiteListUser newObj = activeObjects.create(WhiteListUser.class, ImmutableMap.builder() + .put(WhiteListUser.COLUMN_ISSUE_CHECKER_SETTING_ID, issueCheckerSetting.getID()) + .put(WhiteListUser.COLUMN_USER_ID, userId) + .build()); + } + } + + public void saveGroupWhitelist(Repository repository, List groupNames) { + IssueCheckerSetting issueCheckerSetting = getOrCreateIssueCheckerSetting(repository); + + List existingNames = Arrays.stream(issueCheckerSetting.getWhiteListGroups()).map(WhiteListGroup::getName).collect(Collectors.toList()); + List namesToRemove = new ArrayList<>(existingNames); + List groupToAdd = new ArrayList<>(groupNames); + namesToRemove.removeIf(s -> groupNames.contains(s)); + + groupToAdd.removeIf(s -> existingNames.contains(s) || s.isEmpty()); + + if (!namesToRemove.isEmpty()) { + activeObjects.deleteWithSQL(WhiteListGroup.class, WhiteListGroup.COLUMN_ISSUE_CHECKER_SETTING_ID + "= ?" + + " AND " + WhiteListGroup.COLUMN_NAME + " IN (?)", issueCheckerSetting.getID(), + String.join(",", namesToRemove)); + } + for (String groupName : groupToAdd) { + WhiteListGroup newObj = activeObjects.create(WhiteListGroup.class, ImmutableMap.builder() + .put(WhiteListGroup.COLUMN_ISSUE_CHECKER_SETTING_ID, issueCheckerSetting.getID()) + .put(WhiteListGroup.COLUMN_NAME, groupName) + .build()); + } + } + + public void saveScript(Repository repository, String scriptParam) { + + if (scriptParam != null && !scriptParam.isEmpty()) { + + } + IssueCheckerSetting issueCheckerSetting = getOrCreateIssueCheckerSetting(repository); + issueCheckerSetting.setScript(scriptParam); + issueCheckerSetting.save(); + } + + public void saveFields(Repository repository, String fieldsIds) { + IssueCheckerSetting issueCheckerSetting = getOrCreateIssueCheckerSetting(repository); + issueCheckerSetting.setIssueFields(fieldsIds); + issueCheckerSetting.save(); + } + + public void saveBranch(String newBranchParam, Repository repository) { + IssueCheckerSetting issueCheckerSetting = getOrCreateIssueCheckerSetting(repository); + activeObjects.create(BranchIssueChecker.class, ImmutableMap.builder() + .put(BranchIssueChecker.COLUMN_ISSUE_CHECKER_SETTING_ID, issueCheckerSetting.getID()) + .put(BranchIssueChecker.COLUMN_BRANCHNAME, newBranchParam) + .build()); + } +} diff --git a/src/main/java/com/baloise/open/bitbucket/issuechecker/persistence/WhiteListGroup.java b/src/main/java/com/baloise/open/bitbucket/issuechecker/persistence/WhiteListGroup.java new file mode 100644 index 0000000..73a259d --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/issuechecker/persistence/WhiteListGroup.java @@ -0,0 +1,39 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.issuechecker.persistence; + +import net.java.ao.Accessor; +import net.java.ao.Entity; +import net.java.ao.Mutator; +import net.java.ao.Preload; +import net.java.ao.schema.*; + + +@Table("WLGRPIssueChecker") +@Preload +@Indexes( + @Index(name = "groupindex", methodNames = {"getName", "getIssueCheckerSetting"}) +) +public interface WhiteListGroup extends Entity{ + String COLUMN_NAME = "NAME"; + String COLUMN_ISSUE_CHECKER_SETTING_ID = "ISSUE_CHECKER_SETTING_ID"; + + @Accessor(COLUMN_NAME) + @NotNull + String getName(); + + @Mutator(COLUMN_NAME) + void setName(String groupName); + + @NotNull + IssueCheckerSetting getIssueCheckerSetting(); + void setIssueCheckerSetting(IssueCheckerSetting issueCheckerSetting); +} diff --git a/src/main/java/com/baloise/open/bitbucket/issuechecker/persistence/WhiteListUser.java b/src/main/java/com/baloise/open/bitbucket/issuechecker/persistence/WhiteListUser.java new file mode 100644 index 0000000..a3a6181 --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/issuechecker/persistence/WhiteListUser.java @@ -0,0 +1,38 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.issuechecker.persistence; + +import net.java.ao.*; +import net.java.ao.schema.*; + + +@Table("WLUSRIssueChecker") +@Preload +@Indexes( + @Index(name = "groupindex", methodNames = {"getUserID", "getIssueCheckerSetting"}) +) +public interface WhiteListUser extends Entity{ + String COLUMN_USER_ID = "USER_ID"; + String COLUMN_ISSUE_CHECKER_SETTING_ID = "ISSUE_CHECKER_SETTING_ID"; + + @Accessor(COLUMN_USER_ID) + @NotNull + String getUserID(); + + @Mutator(COLUMN_USER_ID) + void setUserID(String UserID); + + @NotNull + IssueCheckerSetting getIssueCheckerSetting(); + void setIssueCheckerSetting(IssueCheckerSetting issueCheckerSetting); + +} + diff --git a/src/main/java/com/baloise/open/bitbucket/issuechecker/rest/BranchIssueCheckerModel.java b/src/main/java/com/baloise/open/bitbucket/issuechecker/rest/BranchIssueCheckerModel.java new file mode 100644 index 0000000..6f470b8 --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/issuechecker/rest/BranchIssueCheckerModel.java @@ -0,0 +1,54 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.issuechecker.rest; + +import com.baloise.open.bitbucket.issuechecker.persistence.BranchIssueChecker; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement +public class BranchIssueCheckerModel { + @XmlElement + private Integer id; + + @XmlElement + private String name; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "BranchIssueCheckerModel{" + + "id=" + id + + ", name='" + name + '\'' + + '}'; + } + + public BranchIssueCheckerModel(BranchIssueChecker branchIssueChecker){ + this.id = branchIssueChecker.getID(); + this.name = branchIssueChecker.getBranchName(); + } +} diff --git a/src/main/java/com/baloise/open/bitbucket/issuechecker/rest/IssueCheckerResource.java b/src/main/java/com/baloise/open/bitbucket/issuechecker/rest/IssueCheckerResource.java new file mode 100644 index 0000000..5b8209a --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/issuechecker/rest/IssueCheckerResource.java @@ -0,0 +1,63 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.issuechecker.rest; + +import com.baloise.open.bitbucket.issuechecker.persistence.IssueCheckerSetting; +import com.baloise.open.bitbucket.issuechecker.persistence.IssueCheckerSettingDao; +import com.atlassian.bitbucket.permission.PermissionService; +import com.atlassian.bitbucket.repository.Repository; +import com.atlassian.bitbucket.rest.util.ResourcePatterns; +import com.atlassian.bitbucket.user.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; + + +@Component +@Path("/issuechecker") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class IssueCheckerResource { + private final com.baloise.open.bitbucket.issuechecker.persistence.IssueCheckerSettingDao IssueCheckerSettingDao; + private final com.atlassian.bitbucket.permission.PermissionService PermissionService; + private final UserService UserService; + + @Autowired + public IssueCheckerResource(IssueCheckerSettingDao IssueCheckerSettingDao, PermissionService PermissionService, UserService UserSetting) { + this.IssueCheckerSettingDao = IssueCheckerSettingDao; + this.PermissionService = PermissionService; + this.UserService = UserSetting; + } + + @GET + @Path("/"+ ResourcePatterns.REPOSITORY_URI) + public IssueCheckerSettingModel get(@Context Repository repo) { + IssueCheckerSetting issueCheckerSetting = IssueCheckerSettingDao.getIssueCheckerSettings(repo); + return new IssueCheckerSettingModel(issueCheckerSetting, UserService); + } + + @DELETE + @Path("/branch/{id}") + public boolean deleteBranch(@PathParam("id") String branchId) { + Boolean wasSuccess = null; + try { + Integer branchIdNumber = Integer.parseInt(branchId); + IssueCheckerSettingDao.deleteBranchIssueChecker(Integer.parseInt(branchId)); + wasSuccess = true; + } catch (NumberFormatException e) { + wasSuccess = false; + } + return wasSuccess; + } +} diff --git a/src/main/java/com/baloise/open/bitbucket/issuechecker/rest/IssueCheckerSettingModel.java b/src/main/java/com/baloise/open/bitbucket/issuechecker/rest/IssueCheckerSettingModel.java new file mode 100644 index 0000000..b683533 --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/issuechecker/rest/IssueCheckerSettingModel.java @@ -0,0 +1,90 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.issuechecker.rest; + +import com.baloise.open.bitbucket.issuechecker.persistence.IssueCheckerSetting; +import com.atlassian.bitbucket.user.UserService; +import com.google.gson.Gson; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Arrays; + +@XmlRootElement +public class IssueCheckerSettingModel { + @XmlElement + private Integer ID; + + @XmlElement + private String script; + + @XmlElement + private BranchIssueCheckerModel[] branches; + + @XmlElement + private WhiteListUserModel[] whiteListUsers; + + @XmlElement + private WhiteListGroupModel[] whiteListGroups; + + @XmlElement + private Boolean isScriptLimited; + + public Integer getID() { + return ID; + } + + public String getScript() { + return script; + } + + public BranchIssueCheckerModel[] getBranches() { + return branches; + } + + public WhiteListUserModel[] getWhiteListUsers() { + return whiteListUsers; + } + + public WhiteListGroupModel[] getWhiteListGroups(){ + return whiteListGroups; + } + + public Boolean getIsScriptLimited() { + return isScriptLimited; + } + public IssueCheckerSettingModel(IssueCheckerSetting issueCheckerSetting, UserService userService){ + this.ID = issueCheckerSetting.getID(); + this.script = issueCheckerSetting.getScript(); + if(this.script == null){ + this.script = ""; + } + this.branches = Arrays.stream(issueCheckerSetting.getBranches()).map(BranchIssueCheckerModel::new).toArray(BranchIssueCheckerModel[]::new); + this.whiteListGroups = Arrays.stream(issueCheckerSetting.getWhiteListGroups()).map(WhiteListGroupModel::new).toArray(WhiteListGroupModel[]::new); + this.whiteListUsers = Arrays.stream(issueCheckerSetting.getWhiteListUsers()).map(whiteListUser -> {return new WhiteListUserModel(whiteListUser, userService);}).toArray(WhiteListUserModel[]::new); + this.isScriptLimited = issueCheckerSetting.getIsScriptLimited(); + } + + @Override + public String toString() { + return "IssueCheckerSettingModel{" + + "ID=" + ID + + ", script='" + script + '\'' + + ", branches=" + Arrays.toString(branches) + + ", whiteListUsers=" + Arrays.toString(whiteListUsers) + + '}'; + } + + public String toJsonString(){ + Gson gson = new Gson(); + return gson.toJson(this); + } +} diff --git a/src/main/java/com/baloise/open/bitbucket/issuechecker/rest/WhiteListGroupModel.java b/src/main/java/com/baloise/open/bitbucket/issuechecker/rest/WhiteListGroupModel.java new file mode 100644 index 0000000..6106cab --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/issuechecker/rest/WhiteListGroupModel.java @@ -0,0 +1,52 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.issuechecker.rest; + +import com.baloise.open.bitbucket.issuechecker.persistence.WhiteListGroup; + +import javax.xml.bind.annotation.XmlElement; + +public class WhiteListGroupModel { + @XmlElement + private String id; + + @XmlElement + private String name; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "WhiteListGroupModel{" + + "id=" + id + + ", name='" + name + '\'' + + '}'; + } + + WhiteListGroupModel(WhiteListGroup whiteListGroup){ + this.id = whiteListGroup.getName(); + this.name = whiteListGroup.getName(); + } +} diff --git a/src/main/java/com/baloise/open/bitbucket/issuechecker/rest/WhiteListUserModel.java b/src/main/java/com/baloise/open/bitbucket/issuechecker/rest/WhiteListUserModel.java new file mode 100644 index 0000000..5a69c61 --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/issuechecker/rest/WhiteListUserModel.java @@ -0,0 +1,74 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.issuechecker.rest; + +import com.baloise.open.bitbucket.issuechecker.persistence.WhiteListUser; +import com.atlassian.bitbucket.user.ApplicationUser; +import com.atlassian.bitbucket.user.UserService; + +import javax.xml.bind.annotation.XmlElement; + +public class WhiteListUserModel { + @XmlElement + private Integer id; + + @XmlElement + private String slug; + + @XmlElement + private String displayName; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getSlug() { + return slug; + } + + public void setSlug(String slug) { + this.slug = slug; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + @Override + public String toString() { + return "WhiteListUserModel{" + + "id=" + id + + ", slug='" + slug + '\'' + + ", displayName='" + displayName + '\'' + + '}'; + } + + WhiteListUserModel(WhiteListUser whiteListUser, UserService userService) { + try { + ApplicationUser user = userService.getUserById(Integer.parseInt(whiteListUser.getUserID())); + this.id = user.getId(); + this.displayName = user.getDisplayName(); + this.slug = user.getSlug(); + } catch (Exception e) { + this.displayName = "ERROR"; + this.slug = "ERROR"; + } + } + +} diff --git a/src/main/java/com/baloise/open/bitbucket/issuechecker/servlet/IssueCheckerServlet.java b/src/main/java/com/baloise/open/bitbucket/issuechecker/servlet/IssueCheckerServlet.java new file mode 100644 index 0000000..1562aef --- /dev/null +++ b/src/main/java/com/baloise/open/bitbucket/issuechecker/servlet/IssueCheckerServlet.java @@ -0,0 +1,213 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.issuechecker.servlet; + +import com.baloise.open.bitbucket.common.files.FileUtils; +import com.baloise.open.bitbucket.common.groovy.ScriptHelper; +import com.baloise.open.bitbucket.common.integration.jira.JiraIntegrationUtil; +import com.baloise.open.bitbucket.common.integration.jira.SimpleJiraIssueField; +import com.baloise.open.bitbucket.common.servlet.AbstractSimpleServlet; +import com.baloise.open.bitbucket.issuechecker.persistence.IssueCheckerSetting; +import com.baloise.open.bitbucket.issuechecker.persistence.IssueCheckerSettingDao; +import com.baloise.open.bitbucket.issuechecker.rest.IssueCheckerSettingModel; +import com.atlassian.applinks.api.ApplicationLink; +import com.atlassian.applinks.api.ApplicationLinkService; +import com.atlassian.applinks.api.CredentialsRequiredException; +import com.atlassian.applinks.api.application.jira.JiraApplicationType; +import com.atlassian.bitbucket.permission.Permission; +import com.atlassian.bitbucket.permission.PermissionService; +import com.atlassian.bitbucket.project.Project; +import com.atlassian.bitbucket.project.ProjectService; +import com.atlassian.bitbucket.repository.Repository; +import com.atlassian.bitbucket.repository.RepositoryService; +import com.atlassian.bitbucket.user.UserService; +import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport; +import com.atlassian.soy.renderer.SoyTemplateRenderer; +import com.google.common.collect.ImmutableMap; +import com.google.gson.Gson; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +public class IssueCheckerServlet extends AbstractSimpleServlet { + RepositoryService repositoryService; + ProjectService projectService; + IssueCheckerSettingDao issueCheckerSettingDao; + PermissionService permissionService; + UserService userService; + ApplicationLinkService applicationLinkService; + + Boolean isAdmin; + Repository repository; + IssueCheckerSetting issueCheckerSetting; + Project project; + + private final static String TEMPLATE = "issuechecker.templates.repositorySettings"; + private final static String EXAMPLE_SCRIPT = IssueCheckerServlet.readExampleFile(); + + public IssueCheckerServlet(@ComponentImport SoyTemplateRenderer soyTemplateRenderer, @ComponentImport RepositoryService repositoryService, + @ComponentImport ProjectService projectService, @ComponentImport PermissionService permissionService, + @ComponentImport UserService userService, + @ComponentImport ApplicationLinkService applicationLinkService, + IssueCheckerSettingDao issueCheckerSettingDao) { + super(soyTemplateRenderer); + this.repositoryService = repositoryService; + this.projectService = projectService; + this.issueCheckerSettingDao = issueCheckerSettingDao; + this.permissionService = permissionService; + this.userService = userService; + this.applicationLinkService = applicationLinkService; + } + + private ImmutableMap.Builder appendCommonFields(ImmutableMap.Builder parameters) { + ApplicationLink appLink = applicationLinkService.getPrimaryApplicationLink(JiraApplicationType.class); + Gson gson = new Gson(); + + if (appLink != null) { + parameters.put("jiraLink", appLink.getDisplayUrl().toASCIIString()); + String fieldsIds = issueCheckerSetting.getIssueFields(); + if (fieldsIds != null && !fieldsIds.isEmpty()) { + try { + parameters.put("jiraFieldsJson", gson.toJson(retrieveFieldsByIds(fieldsIds))); + } catch (CredentialsRequiredException e) { + parameters.put("error", "Authorisation is required for IssueCheckerPlugin please follow:\n"+ + ""+e.getAuthorisationURI()+""); + } + } + } + + IssueCheckerSettingModel settingModel = new IssueCheckerSettingModel(issueCheckerSetting, userService); + + parameters.put("project", project) + .put("repository", repository) + .put("setting", settingModel) + .put("userWhiteListJson", gson.toJson(settingModel.getWhiteListUsers())) + .put("exampleScript", EXAMPLE_SCRIPT) + .put("groupWhiteListJson", gson.toJson(settingModel.getWhiteListGroups())); + + return parameters; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + ImmutableMap.Builder parameters = ImmutableMap.builder(); + + appendCommonFields(parameters); + + render(resp, TEMPLATE, parameters + .build()); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + req.getLocale(); + ImmutableMap.Builder parameters = ImmutableMap.builder(); + + Boolean encounteredError = false; + + if (!isAdmin) { + resp.sendError(HttpServletResponse.SC_UNAUTHORIZED); + } else { + appendCommonFields(parameters); + + String userParam = req.getParameter("users"); + String groupParam = req.getParameter("groups"); + String scriptParam = req.getParameter("script"); + String fieldParam = req.getParameter("fields"); + String newBranchParam = req.getParameter("newbranch"); + + if (userParam != null) { + parameters.put("users", userParam); + String[] userSelectorParam = userParam.split(","); + issueCheckerSettingDao.saveUserWhitelist(repository, Arrays.asList(userSelectorParam)); + } + if (groupParam != null) { + parameters.put("groups", groupParam); + String[] groupSelectorParam = groupParam.split(","); + issueCheckerSettingDao.saveGroupWhitelist(repository, Arrays.asList(groupSelectorParam)); + } + if (scriptParam != null) { + if (!scriptParam.isEmpty()) { + parameters.put("script", scriptParam); + String validationResult = ScriptHelper.validateScript(scriptParam); + if (validationResult == null || validationResult.isEmpty()) { + issueCheckerSettingDao.saveScript(repository, scriptParam); + } else { + encounteredError = true; + parameters.put("scriptError", validationResult); + } + } + } + if (fieldParam != null) { + parameters.put("fields", fieldParam); + issueCheckerSettingDao.saveFields(repository, fieldParam); + } + if (newBranchParam != null) { + issueCheckerSettingDao.saveBranch(newBranchParam, repository); + } + + if (encounteredError) { + render(resp, TEMPLATE, parameters + .build()); + } else { + //reload the page for best results + resp.sendRedirect(req.getRequestURL().toString()); + } + } + } + + private List retrieveFieldsByIds(String issueFields) throws CredentialsRequiredException { + JiraIntegrationUtil jiraIntegrationUtil = new JiraIntegrationUtil(applicationLinkService); + List results = jiraIntegrationUtil.getAvailableFields(); + List ids = Arrays.asList(issueFields.split(",")); + results.removeIf(simpleJiraIssueField -> !ids.contains(simpleJiraIssueField.getId())); + return results; + } + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // Get projectKey from path + String pathInfo = req.getPathInfo(); + String[] components = pathInfo.split("/"); + + if (components.length < 2) { + resp.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + + project = projectService.getByKey(components[1]); + if (project == null) { + resp.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + + repository = repositoryService.getBySlug(components[1], components[2]); + issueCheckerSetting = issueCheckerSettingDao.getOrCreateIssueCheckerSetting(repository); + isAdmin = permissionService.hasRepositoryPermission(repository, Permission.REPO_ADMIN); + + super.service(req, resp); + } + + private static String readExampleFile() { + String fileContent = new FileUtils().loadFileAsString("/examples/ParseScript.groovy"); + if (fileContent != null) { + return fileContent; + } else { + return ""; + } + } + +} diff --git a/src/main/resources/META-INF/spring/plugin-context.xml b/src/main/resources/META-INF/spring/plugin-context.xml new file mode 100644 index 0000000..46a1522 --- /dev/null +++ b/src/main/resources/META-INF/spring/plugin-context.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/src/main/resources/atlassian-plugin.xml b/src/main/resources/atlassian-plugin.xml new file mode 100644 index 0000000..0c56efb --- /dev/null +++ b/src/main/resources/atlassian-plugin.xml @@ -0,0 +1,148 @@ + + + + + + + ${project.description} + ${project.version} + + images/pluginLogo.png + images/pluginLogo.png + + + + + + + + + + + + + + + + + /plugins/servlet/codefreeze/repository/${repository.project.key}/${repository.slug}/settings + + + + + + /plugins/servlet/issuechecker/repository/${repository.project.key}/${repository.slug}/settings + + + + + com.atlassian.auiplugin:ajs + + com.atlassian.auiplugin:aui-select2 + + + + + + + + + codefreezeUserSelector + + + + com.atlassian.auiplugin:ajs + + com.atlassian.auiplugin:aui-select2 + + codefreezeJiraFieldSelector + + + + com.atlassian.auiplugin:ajs + + + codefreezeAddBranchDialog + + + + + + + + + + + + bitbucket.layout.repository + + + + + Disable merges after codefreeze that do not have admin approval. + + repository + project + + /images/icon-codefreeze.svg + + + Rejects commits pushed to specific branch after code freeze. + + repository + project + + /images/icon-codefreeze.svg + + + + Use groovy script to validate issue fields contained in pull request. + + repository + project + + /images/icon-codefreeze.svg + + + + The module configuring the Active Objects service used by this plugin + + com.baloise.open.bitbucket.codefreeze.persistence.CodeFreezeSetting + com.baloise.open.bitbucket.codefreeze.persistence.BranchFreeze + + com.baloise.open.bitbucket.issuechecker.persistence.BranchIssueChecker + com.baloise.open.bitbucket.issuechecker.persistence.IssueCheckerSetting + com.baloise.open.bitbucket.issuechecker.persistence.WhiteListUser + com.baloise.open.bitbucket.issuechecker.persistence.WhiteListGroup + com.baloise.open.bitbucket.issuechecker.persistence.FieldIssueChecker + + + + + com.atlassian.bitbucket.server.bitbucket-web:server-soy-templates + com.atlassian.bitbucket.server.bitbucket-web-api:branch-selector-field + ch.baloise.bitbucket.codefreeze:codefreeze-user-select + ch.baloise.bitbucket.codefreeze:codefreezeUserSelector + ch.baloise.bitbucket.codefreeze:codefreezeJiraFieldSelector + + + + The Repository Servlet Plugin for CodeFreeze module + /codefreeze/repository/* + + + Provides a Rest service for so other components can CRUD codefreeze. + + + The Repository Servlet Plugin for CodeFreeze module + /issuechecker/repository/* + + diff --git a/src/main/resources/codefreeze.properties b/src/main/resources/codefreeze.properties new file mode 100644 index 0000000..8df8c66 --- /dev/null +++ b/src/main/resources/codefreeze.properties @@ -0,0 +1,14 @@ +# +# Copyright 2021 Baloise Group +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +# + +#put any key/value pairs here +my.plugin.name=CodeFreeze +jira-proxy-resource.name=Jira Proxy Resource +jira-proxy-resource.description=The Jira Proxy Resource Plugin diff --git a/src/main/resources/css/codefreeze.css b/src/main/resources/css/codefreeze.css new file mode 100644 index 0000000..710b87c --- /dev/null +++ b/src/main/resources/css/codefreeze.css @@ -0,0 +1,10 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + diff --git a/src/main/resources/css/icon.css b/src/main/resources/css/icon.css new file mode 100644 index 0000000..3d4febd --- /dev/null +++ b/src/main/resources/css/icon.css @@ -0,0 +1,14 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +.icon-codefreeze { + background-image: url('icon-codefreeze.svg'); + background-size: cover; +} \ No newline at end of file diff --git a/src/main/resources/examples/ParseScript.groovy b/src/main/resources/examples/ParseScript.groovy new file mode 100644 index 0000000..0315dbf --- /dev/null +++ b/src/main/resources/examples/ParseScript.groovy @@ -0,0 +1,39 @@ + + +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +/* +Objects passed in to the script: +issue - issue object as retrieved from the jira rest service. Contains basic properties (id, key) +and array of fields that were set on the configuration script +toRef - reference to which pull request points to + +return nonEmpty string upon failing validation which will be displayed. +*/ +package groovy + +import com.google.gson.JsonObject + + +def isFixVersionCorrectToTargetBranch(issue, toRef){ + //do some calculation + return true +} + +if(issue.fields.fixVersions.size() != 1) { + return "Only one fix version allowed." +} + +if(!isFixVersionCorrectToTargetBranch(issue, toRef)){ + return "Incorrect fix version for target branch" +} + +return null diff --git a/src/main/resources/images/icon-codefreeze.svg b/src/main/resources/images/icon-codefreeze.svg new file mode 100644 index 0000000..b2c06cd --- /dev/null +++ b/src/main/resources/images/icon-codefreeze.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/images/pluginIcon.png b/src/main/resources/images/pluginIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..798d9e27dfadeab3e17d87c3faf874487f6dd036 GIT binary patch literal 958 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GXl47&iy_gt!8^K=AS7r!QZ=e*F0P z$%}UnpS^ka0mA(Rl=}4PGgt{0;_B6_`}gm^d-v{v0|&53GcbV6{PYEA=7VQ%-hcS~ z`SX`|@7}+E|MBg+_b*?+z4hSv`RfmF+H%%&Fi-9ubjBDtbgUY9fvpXIkxk_$&NXjqHE^H)y@elo}w4t zAmWs-;9V->RKR1KYZBeqHf{N$)mxlW+PQ6VZ4;WiXD+YnU!dq+Y8qAVoYKl-mZ9WR zmRvohsC{;R%S>L|9Mh<}^_#cv-@k9>lC?|MY??fOwXkEpR!~)B=>%opayicur{vbv z8@2(%=j4eKCr_T(wtIhM*%ZJ0{;<-?hLKHiRnzCK+_GiozN1Hv9Y22j+O_Lfu3kHS z`uxeW7xx@KRnoVzbKd4tXD?pAe*NxUpm*;dJ9D{n!S+eZcb~m@t=xp^B`~<O>_ z45UG94+giB*>*t2Cr=m05Q)pl2?;4l)6x@Dlb=7Bkp3(sB_ZL-qiNH_144qr0w+#? z{wyiw!Nbtd;OiHzT)KAg>Sgl>PZJ+JHa9UcGc+|e-?m}PCb0*}0wS9>Zrxm7P*PM@ zSXv@1lp-Kf_Wi?`Pv1U%{mjq7BgDqZ(%#b6>h9v^di=<-M%G8dLX#&22VK5oY_$0R z(@o2rl|NZq4PB3_E=_3M^r`FAs#mja?fTW`wxPizxY#>8y4pQGd@_fJgGb1k-OHD6 zUtjOkKf@xR!Qa6t$V-Z6iHd3i-{XfD*6Z!C8<`)MX5lF!N|bSNY~I%*U%`$(A3J*%*w=E z*TBrmz+i>U!6hgfa`RI%(<*Umxan9a0n{J~vLQG>t)x7$D3!r6B|j-u!8128JvAsb WF{QHbWU38N5re0zpUXO@geCw9n!?Ed literal 0 HcmV?d00001 diff --git a/src/main/resources/images/pluginLogo.png b/src/main/resources/images/pluginLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..494581964857708b1e82bf233766769df0a99b61 GIT binary patch literal 3409 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/js/addBranchDialog.js b/src/main/resources/js/addBranchDialog.js new file mode 100644 index 0000000..3a9f17c --- /dev/null +++ b/src/main/resources/js/addBranchDialog.js @@ -0,0 +1,59 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +var codeFreezeBranchDialog = { + // Shows the dialog when the "Show dialog" button is clicked + showDialogFunction: function () { + AJS.$("#add-branch-button").click(function (e) { + e.preventDefault(); + AJS.dialog2("#add-branch-dialog").show(); + }); + }, + // Hides the dialog + submitDialogFunction: function () { + var currentClick = AJS.$("#add-branch-dialog-submit-button").click; + + AJS.$("#add-branch-dialog-submit-button").click(function (e) { + currentClick(e); + AJS.$("#newbranchInput").text = ""; + AJS.dialog2("#add-branch-dialog").hide(); + }); + }, + cancelDialogFunction: function(){ + AJS.$("#dialog-cancel-button").click(function (e) { + e.preventDefault(); + AJS.dialog2("#add-branch-dialog").hide(); + AJS.$("#newbranchInput").text = ""; + }); + }, + removeBranchButtonsInit: function(){ + AJS.$(".remove-branch-button").click(codeFreezeBranchDialog.removeBranchButtonFunction) + }, + removeBranchButtonFunction : function(e){ + var branchID = $(this).prop("name"); + AJS.$.ajax({ + url: AJS.contextPath()+"/rest/codefreeze/1.0/issuechecker/branch/"+branchID, + type: "DELETE", + data: ({}), + dataType: "json", + success: function(msg){ + location.reload(); + } + }); + AJS.$("#BranchRow"+branchID).remove(); + } +} + +AJS.$(window).load(function () { + codeFreezeBranchDialog.showDialogFunction(); + codeFreezeBranchDialog.submitDialogFunction(); + codeFreezeBranchDialog.cancelDialogFunction(); + codeFreezeBranchDialog.removeBranchButtonsInit(); +}) \ No newline at end of file diff --git a/src/main/resources/js/codefreeze.js b/src/main/resources/js/codefreeze.js new file mode 100644 index 0000000..710b87c --- /dev/null +++ b/src/main/resources/js/codefreeze.js @@ -0,0 +1,10 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + diff --git a/src/main/resources/js/fieldSelector.js b/src/main/resources/js/fieldSelector.js new file mode 100644 index 0000000..bfcfbea --- /dev/null +++ b/src/main/resources/js/fieldSelector.js @@ -0,0 +1,104 @@ + +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +var fieldSelectorUtils = +{ + + ajaxData : [], + initalizeFieldSelector: function(){ + AJS.$("#code-freeze-jira-field-selector").auiSelect2({ + minimumInputLength: 3, + multiple: true, + ajax: { + url: AJS.contextPath()+"/rest/codefreeze/1.0/jiraproxy/field", // JIRA-relative URL to the REST end-point + type: "GET", + dataType: "json", + cache: true, + // query parameters for the remote ajax call + data: function data(term) { + return { + filter: term + }; + }, + // parse data from the server into form select2 expects + results: function results(data) { + if (!$.trim(data)) { + return { + results: [] + }; + } else { + return { + results: data + }; + } + }, + params: { + error: function (response) { + if(response.status == 401){ + AJS.messages.error({ + title: "Authorization to jira required!", + body: "

Follow Jira authentication link

", + closeable: true + }); + + //scroll up to error message + location.href = "#"; + location.href = "#aui-message-bar"; + } + } + } + }, + // define how selected element should look like + formatSelection: function formatSelection(field) { + return Select2.util.escapeMarkup(field.text); + }, + // define message showed when there are no matches + formatNoMatches: function formatNoMatches(query) { + return "No matches found"; + } + }); + }, + setPreselectedFields: function(){ + var preselectedFieldsContainer = $("#issueCheckerFields"); + if(preselectedFieldsContainer!= null && preselectedFieldsContainer.text() != null && preselectedFieldsContainer.length > 0) { + $("#code-freeze-jira-field-selector").auiSelect2('data', JSON.parse(preselectedFieldsContainer.text())); + } + }, + matchCustom: function (params, data) { + // If there are no search terms, return all of the data + if ($.trim(params.term) === '') { + return data; + } + + // Do not display the item if there is no 'text' property + if (typeof data.text === 'undefined') { + return null; + } + + // `params.term` should be the term that is used for searching + // `data.text` is the text that is displayed for the data object + if (data.text.indexOf(params.term) > -1) { + var modifiedData = $.extend({}, data, true); + // You can return modified objects from here + // This includes matching the `children` how you want in nested data sets + return modifiedData; + } + + // Return `null` if the term should not be displayed + return null; + } +}; + +//$("#test").select2({multiple: true, data: [{id: "test", text: "text"}]}); +AJS.$(window).load(function () { + fieldSelectorUtils.initalizeFieldSelector(); + fieldSelectorUtils.setPreselectedFields(); +}) \ No newline at end of file diff --git a/src/main/resources/js/userSelector.js b/src/main/resources/js/userSelector.js new file mode 100644 index 0000000..d74e95c --- /dev/null +++ b/src/main/resources/js/userSelector.js @@ -0,0 +1,131 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +var codeFrezeUserSelector = { + parseGroupsDataIntoSelect2: function (groups) { + var results = [] + groups.forEach(function (group) { + results.push({id: group.name, text: group.name}) + }) + return results + }, + initializeUserSelector: function () { + AJS.$("#code-freeze-user-selector").auiSelect2({ + minimumInputLength: 3, + multiple: true, + ajax: { + url: AJS.contextPath() + "/rest/api/1.0/admin/users", // JIRA-relative URL to the REST end-point + type: "GET", + dataType: 'json', + cache: true, + // query parameters for the remote ajax call + data: function data(term) { + return { + filter: term + }; + }, + // parse data from the server into form select2 expects + results: function results(data) { + if (!$.trim(data)) { + return { + results: [] + }; + } else { + return { + results: data.values + }; + } + } + }, + // define how selected element should look like + formatSelection: function formatSelection(user) { + return Select2.util.escapeMarkup(user.displayName); + }, + // define how single option should look like + formatResult: function formatResult(user, container, query, escapeMarkup) { + // format result string + var resultText = user.displayName + " - (" + user.name + ")"; + var higlightedMatch = []; + // we need this to disable html escaping by select2 as we are doing it on our own + var noopEscapeMarkup = function noopEscapeMarkup(s) { + return s; + }; + // highlight matches of the query term using matcher provided by the select2 library + Select2.util.markMatch(escapeMarkup(resultText), escapeMarkup(query.term), higlightedMatch, noopEscapeMarkup); + // convert array to string + higlightedMatch = higlightedMatch.join(""); + // return avatarHtml + higlightedMatch; + return higlightedMatch + }, + // define message showed when there are no matches + formatNoMatches: function formatNoMatches(query) { + return "No matches found"; + } + }); + }, + initializeGroupSelector: function () { + AJS.$("#code-freeze-group-selector").auiSelect2({ + minimumInputLength: 3, + multiple: true, + ajax: { + url: AJS.contextPath() + "/rest/api/1.0/admin/groups", // JIRA-relative URL to the REST end-point + type: "GET", + dataType: 'json', + cache: true, + // query parameters for the remote ajax call + data: function data(term) { + return { + filter: term + }; + }, + // parse data from the server into form select2 expects + results: function results(data) { + return { + results: codeFrezeUserSelector.parseGroupsDataIntoSelect2(data.values) + }; + } + }, + // define how selected element should look like + formatSelection: function formatSelection(group) { + return Select2.util.escapeMarkup(group.id); + }, + // define message showed when there are no matches + formatNoMatches: function formatNoMatches(query) { + return "No matches found"; + } + }); + }, + setPreselectedUsers: function () { + $("#code-freeze-user-selector").auiSelect2('data', JSON.parse($("#issueCheckerWhiteListUsers").text())); + }, + setPreselectedGroups: function () { + $("#code-freeze-group-selector").auiSelect2('data', JSON.parse($("#issueCheckerWhiteListGroups").text())); + } +} + + +//Executed to late +// AJS.toInit(function(){ +// codeFreezeUtils = {whiteListUsers: [], +// whiteListGroups: [], +// jiraFields: [] +// }; +// initializeUserSelector(); +// initializeGroupSelector(); +// initalizeVariablesFromFields(); +// setPreselectedUsers() +// }); + +AJS.$(window).load(function () { + codeFrezeUserSelector.initializeUserSelector(); + codeFrezeUserSelector.initializeGroupSelector(); + codeFrezeUserSelector.setPreselectedUsers(); + codeFrezeUserSelector.setPreselectedGroups(); +}) diff --git a/src/main/resources/templates/codefreeze.soy b/src/main/resources/templates/codefreeze.soy new file mode 100644 index 0000000..bf7db32 --- /dev/null +++ b/src/main/resources/templates/codefreeze.soy @@ -0,0 +1,131 @@ +{namespace codefreeze.templates} + +/** + * @param repository Repository object + * @param branches + * @param isAdmin + * @param error + */ +{template .repositorySettings} + + + + + + + {$repository.slug} / Code freeze settings + + +

Repository: {$repository.slug}

+

Codefreeze configuration page.

+ {if $error} + Encountered Error + {/if} + + {if $isAdmin} +

+ Freeze branch +

+ {/if} + + + + + + + + + + {foreach $branch in $branches} + + + + {if $isAdmin} + + {/if} + + {ifempty} + + + + {/foreach} + +
branchdateActions
{$branch.branch}{call aui.form.input} + {param id: 'date' /} + {param type: 'date' /} + {param isDisabled: true /} + {param value: $branch.date/} + {/call} + +
No frozen branches configured.
+ + +{/template} + +/** + * @param repository Repository object + * @param branch + */ +{template .branchEdit} + + + + + + + {$repository.slug} / Code freeze settings + + +
+
+
+
    +
  1. Branch Freezes
  2. +
+ {if $branch} +

Edit {$branch.branch}

+ {else} +

Add new branchFreeze

+ {/if} +
+
+
+ {call aui.form.form} + {param action: '' /} + {param content} + {call aui.form.textField} + {param id: 'branch' /} + {param labelContent: 'Branch name' /} + {/call} + {call aui.form.fieldGroup} + {param content} + {call aui.form.label} + {param forField: 'date' /} + {param content: 'Date from: ' /} + {/call} + {call aui.form.input} + {param id: 'date' /} + {param type: 'date' /} + {param value: $branch ? $branch.date : '' /} + {/call} + {/param} + {/call} + {call aui.form.fieldGroup} + {param content} + {call aui.form.submit} + {param text: 'add'/} + {/call} + {/param} + {/call} + {/param} + {/call} + + +{/template} \ No newline at end of file diff --git a/src/main/resources/templates/issuechecker.soy b/src/main/resources/templates/issuechecker.soy new file mode 100644 index 0000000..1aed302 --- /dev/null +++ b/src/main/resources/templates/issuechecker.soy @@ -0,0 +1,158 @@ +{namespace issuechecker.templates} + +/** + * @param repository Repository object + * @param setting + * @param userWhiteListJson + * @param groupWhiteListJson + * @param? jiraFieldsJson + * @param? jiraLink + * @param? scriptError + * @param? exampleScript + * @param? script used to save not displayed version in case of error + * @param error + */ +{template .repositorySettings} + + {webResourceManager_requireResourcesForContext('codefreezeUserSelector')} + {webResourceManager_requireResourcesForContext('codefreezeJiraFieldSelector')} + {webResourceManager_requireResourcesForContext('codefreezeAddBranchDialog')} + + + + + + {$repository.slug} / IssueChecker settings + + +
+ + + {if $jiraFieldsJson} + + {/if} + {if $jiraLink} + + {/if} +

Repository: {$repository.slug}

+

IssueChecker configuration page.

+ {if $error} + Encountered Error + {/if} +

Protected branches

+ + + + + + + + + + {foreach $branch in $setting.branches} + + + + + {ifempty} + + + + {/foreach} + + + + +
branchActions
{call aui.form.input} + {param id: 'branch' /} + {param type: 'string' /} + {param isDisabled: true /} + {param value: $branch.name /}{/call} +
+ +// {call aui.buttons.buttons} +// {param content} +// {call aui.buttons.button} +// {param text: 'Remove' /} +// {param isDisabled: $isAdmin /} +// {param id: 'r'/} +// {/call} +// {/param} +// {/call} +
+
No branches configured.
+ +
+

+ {if $scriptError} +
+

+ Script was not saved: +

+

{$scriptError}

+
+ {/if} +
+
+ Processing script + +
+ + +
+ +
+
+
+
+ WhiteLists: + +
+ +
+ +
+ +
+ +
+
+ {if $jiraLink} +
+
+ Jira issue fields + +
+ +
+
+ {/if} + + +{/template} \ No newline at end of file diff --git a/src/test/java/com/baloise/open/bitbucket/common/groovy/GroovySandboxTest.java b/src/test/java/com/baloise/open/bitbucket/common/groovy/GroovySandboxTest.java new file mode 100644 index 0000000..01ec18a --- /dev/null +++ b/src/test/java/com/baloise/open/bitbucket/common/groovy/GroovySandboxTest.java @@ -0,0 +1,55 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.common.groovy; + + +import com.baloise.open.bitbucket.common.json.JsonParser; +import com.baloise.open.bitbucket.common.files.FileUtils; +import com.google.gson.JsonObject; +import groovy.lang.Binding; +import groovy.lang.GroovyShell; +import org.junit.Test; + +public class GroovySandboxTest { + + private FileUtils fileUtils = new FileUtils(); + + @Test + public void testParseFromString(){ + FileUtils fileUtils = new FileUtils(); + + String script = fileUtils.loadFileAsString("/groovy/ParseScript.groovy"); + String JSON = fileUtils.loadFileAsString("/json/issueResponse.json"); + JsonObject json = JsonParser.parseJsonObject(JSON); + Binding binding = new Binding(); + binding.setVariable("issue", json); + binding.setVariable("fromRef", "10/2/0/GALRE-64718"); + binding.setVariable("toRef", "10/2/0/master"); + GroovyShell sh = new GroovySandbox().createSandbox(binding); + String result = (String)sh.evaluate(script); + assert(result != null); + } + + @Test + public void testParseFromStringHappyPath(){ + + String script = fileUtils.loadFileAsString("/groovy/ParseScript.groovy"); + String JSON = fileUtils.loadFileAsString("/json/issueResponseSingleFix.json"); + JsonObject json = JsonParser.parseJsonObject(JSON); + Binding binding = new Binding(); + binding.setVariable("issue", json); + binding.setVariable("fromRef", "10/2/0/GALRE-64718"); + binding.setVariable("toRef", "10/2/0/master"); + GroovyShell sh = new GroovySandbox().createSandbox(binding); + String result = (String)sh.evaluate(script); + assert(result == null); + } +} diff --git a/src/test/java/com/baloise/open/bitbucket/common/integration/jira/FieldRetrieveHandlerTest.java b/src/test/java/com/baloise/open/bitbucket/common/integration/jira/FieldRetrieveHandlerTest.java new file mode 100644 index 0000000..5a73bd8 --- /dev/null +++ b/src/test/java/com/baloise/open/bitbucket/common/integration/jira/FieldRetrieveHandlerTest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.common.integration.jira; + +import com.baloise.open.bitbucket.common.files.FileUtils; +import com.atlassian.sal.api.net.Response; +import com.atlassian.sal.api.net.ResponseException; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import java.util.List; + + +public class FieldRetrieveHandlerTest { + FieldRetrieveHandler testee; + + @Test + public void handlerTest(){ + testee = new FieldRetrieveHandler(); + + List fields = null; + try { + fields = testee.handle(mockResponse("/json/fieldsResponse.json")); + } catch (ResponseException e) { + e.printStackTrace(); + Assert.fail(); + } + + Assert.assertEquals("Fields number", 2, fields.size()); + Assert.assertTrue(fields.stream().anyMatch(simpleJiraIssueField -> simpleJiraIssueField.id.equals("customfield_18353"))); + Assert.assertTrue(fields.stream().anyMatch(simpleJiraIssueField -> simpleJiraIssueField.text.equals("Organizations"))); + Assert.assertTrue(fields.stream().anyMatch(simpleJiraIssueField -> simpleJiraIssueField.id.equals("fixVersions"))); + Assert.assertTrue(fields.stream().anyMatch(simpleJiraIssueField -> simpleJiraIssueField.text.equals("Fix Version/s"))); + + } + + private Response mockResponse(String filePath){ + Response mockResponse = Mockito.mock(Response.class); + try { + Mockito.when(mockResponse.getResponseBodyAsString()).thenReturn(new FileUtils().loadFileAsString(filePath)); + Mockito.when(mockResponse.getStatusCode()).thenReturn(200); + } catch (ResponseException e) { + e.printStackTrace(); + } + + return mockResponse; + } +} diff --git a/src/test/java/com/baloise/open/bitbucket/issuechecker/jiralink/JiraIssueDetailedTest.java b/src/test/java/com/baloise/open/bitbucket/issuechecker/jiralink/JiraIssueDetailedTest.java new file mode 100644 index 0000000..35cdd15 --- /dev/null +++ b/src/test/java/com/baloise/open/bitbucket/issuechecker/jiralink/JiraIssueDetailedTest.java @@ -0,0 +1,30 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package com.baloise.open.bitbucket.issuechecker.jiralink; + + +import com.baloise.open.bitbucket.common.json.JsonParser; +import com.google.gson.*; +import org.junit.Assert; +import org.junit.Test; + +public class JiraIssueDetailedTest { + + static final String JSON = "{\"expand\":\"renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations\",\"id\":\"488463\",\"self\":\"https://jira.baloisenet.com/atlassian/rest/api/latest/issue/488463\",\"key\":\"GALRE-64781\",\"fields\":{\"issuetype\":{\"self\":\"https://jira.baloisenet.com/atlassian/rest/api/2/issuetype/3\",\"id\":\"3\",\"description\":\"A task that needs to be done.\",\"iconUrl\":\"https://jira.baloisenet.com/atlassian/secure/viewavatar?size=xsmall&avatarId=33148&avatarType=issuetype\",\"name\":\"Task\",\"subtask\":false,\"avatarId\":33148},\"fixVersions\":[{\"self\":\"https://jira.baloisenet.com/atlassian/rest/api/2/version/105645\",\"id\":\"105645\",\"description\":\"Fliesst nicht ins Sprint Reporting vom PMO ein.\",\"name\":\"NotInReport\",\"archived\":false,\"released\":false}],\"customfield_12250\":\"1|hyhfie:\"}}"; + + @Test + public void testParseFromString(){ + JsonObject json = JsonParser.parseJsonObject(JSON); + JsonObject fields = json.getAsJsonObject("fields"); + + Assert.assertTrue("Expected 1 element in fixVersions array", fields.getAsJsonArray("fixVersions").size() == 1); + } +} diff --git a/src/test/resources/groovy/ParseScript.groovy b/src/test/resources/groovy/ParseScript.groovy new file mode 100644 index 0000000..e705129 --- /dev/null +++ b/src/test/resources/groovy/ParseScript.groovy @@ -0,0 +1,52 @@ +/* + * Copyright 2021 Baloise Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +package groovy + +import com.google.gson.JsonObject + +//For running script without sandbox + +//if(!binding.hasVariable("issue")) { +// String JSON = new File(getClass().getResource('/json/issueResponseSingleFix.json').toURI()).text +// JsonObject json = JsonParser.parse(JSON); +// binding.setVariable("issue", json); +// binding.setVariable("fromRef", "10/2/0/GALRE-64718"); +// binding.setVariable("toRef", "10/2/0/master"); +//} +//END + +def getExpectedFixVersion(String toRef){ + def extractedBranchNo = (toRef =~ /(\d+\\/){3,4}/)[0][0] + def digits = extractedBranchNo.split("/") + if(digits.size() < 4){ + digits += ["0"] + } + return "GWR_IS ${digits.join(".")}".toString() +} + +def getFixVersion(JsonObject issue){ + String fixVersion = issue.fields.fixVersions[0].name.getAsString() + return fixVersion +} + +if(issue.fields.fixVersions == null){ + return "No fix version set".toString() +} + +if(issue.fields.fixVersions.size() != 1) { + return "Only one fix version allowed.".toString() +} + +if(getExpectedFixVersion(toRef) != getFixVersion(issue)){ + return "Wrong fix version. Expected: ${getExpectedFixVersion(toRef)}, Actual : ${getFixVersion(issue)}".toString() +} + +return null diff --git a/src/test/resources/json/fieldsResponse.json b/src/test/resources/json/fieldsResponse.json new file mode 100644 index 0000000..8d2db51 --- /dev/null +++ b/src/test/resources/json/fieldsResponse.json @@ -0,0 +1,36 @@ +[ + { + "id": "customfield_18353", + "name": "Organizations", + "custom": true, + "orderable": true, + "navigable": true, + "searchable": true, + "clauseNames": [ + "cf[18353]", + "Organizations" + ], + "schema": { + "type": "array", + "items": "sd-customerorganization", + "custom": "com.atlassian.servicedesk:sd-customer-organizations", + "customId": 18353 + } + }, + { + "id": "fixVersions", + "name": "Fix Version/s", + "custom": false, + "orderable": true, + "navigable": true, + "searchable": true, + "clauseNames": [ + "fixVersion" + ], + "schema": { + "type": "array", + "items": "version", + "system": "fixVersions" + } + } +] \ No newline at end of file diff --git a/src/test/resources/json/issueResponse.json b/src/test/resources/json/issueResponse.json new file mode 100644 index 0000000..6c78679 --- /dev/null +++ b/src/test/resources/json/issueResponse.json @@ -0,0 +1,44 @@ +{ + "expand": "renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations", + "id": "488463", + "self": "https://jira.baloisenet.com/atlassian/rest/api/latest/issue/488463", + "key": "GALRE-64781", + "fields": { + "issuetype": { + "self": "https://jira.baloisenet.com/atlassian/rest/api/2/issuetype/3", + "id": "3", + "description": "A task that needs to be done.", + "iconUrl": "https://jira.baloisenet.com/atlassian/secure/viewavatar?size=xsmall&avatarId=33148&avatarType=issuetype", + "name": "Task", + "subtask": false, + "avatarId": 33148 + }, + "fixVersions": [ + { + "self": "https://jira.baloisenet.com/atlassian/rest/api/2/version/105645", + "id": "105645", + "description": "Fliesst nicht ins Sprint Reporting vom PMO ein.", + "name": "NotInReport", + "archived": false, + "released": false + }, + { + "self": "https://jira.baloisenet.com/atlassian/rest/api/2/version/105646", + "id": "105646", + "description": "Release GWR_IS 10.2.3.0", + "name": "GWR_IS 10.2.0.0", + "archived": false, + "released": false + }, + { + "self": "https://jira.baloisenet.com/atlassian/rest/api/2/version/105647", + "id": "105647", + "description": "Release GWR_IS 10.2.0.0", + "name": "GWR_IS 10.2.1.0", + "archived": false, + "released": false + } + ], + "customfield_12250": "1|hyhfie:" + } +} \ No newline at end of file diff --git a/src/test/resources/json/issueResponseSingleFix.json b/src/test/resources/json/issueResponseSingleFix.json new file mode 100644 index 0000000..b15be09 --- /dev/null +++ b/src/test/resources/json/issueResponseSingleFix.json @@ -0,0 +1,28 @@ +{ + "expand": "renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations", + "id": "488463", + "self": "https://jira.baloisenet.com/atlassian/rest/api/latest/issue/488463", + "key": "GALRE-64781", + "fields": { + "issuetype": { + "self": "https://jira.baloisenet.com/atlassian/rest/api/2/issuetype/3", + "id": "3", + "description": "A task that needs to be done.", + "iconUrl": "https://jira.baloisenet.com/atlassian/secure/viewavatar?size=xsmall&avatarId=33148&avatarType=issuetype", + "name": "Task", + "subtask": false, + "avatarId": 33148 + }, + "fixVersions": [ + { + "self": "https://jira.baloisenet.com/atlassian/rest/api/2/version/105646", + "id": "105646", + "description": "Release GWR_IS 10.2.0.0", + "name": "GWR_IS 10.2.0.0", + "archived": false, + "released": false + } + ], + "customfield_12250": "1|hyhfie:" + } +} \ No newline at end of file From 6ef58ceebe6d0accff1273a89ae1e6f108f1fc31 Mon Sep 17 00:00:00 2001 From: Arthur Neudeck Date: Fri, 11 Jun 2021 13:32:11 +0200 Subject: [PATCH 2/4] Try to fix markdown issues with headings. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5cc1405..26203a0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -#Codefreeze +# Codefreeze -##Short Documentation +## Short Documentation *Codefreeze* is a basic plugin for [Atlassian Bitbucket](https://www.atlassian.com/software/bitbucket) that allows you ahead of time set date from which pushing directly to specified branches is no longer possible. From 155561990b21a6b4d7c42f8641e1d39fbcc4135c Mon Sep 17 00:00:00 2001 From: Arthur Neudeck Date: Fri, 11 Jun 2021 13:33:09 +0200 Subject: [PATCH 3/4] Fix markdown issues with headings. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 26203a0..a602d18 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ The authorization needs to be defined in JIRA: # Open Issues - Reinit the Github task https://github.com/baloise/open-source/issues/117 -##Hints for Developing Plugins +## Hints for Developing Plugins Here are the SDK commands you'll use immediately: @@ -76,5 +76,5 @@ Example: `localhost:7990/bitbucket/rest/codefreeze/1.0/projects/PROJECT_1/repos/rep_1/branchfreeze` -##Open Source +## Open Source This project is open source and follows the [Baloise Open Source Guidelines](https://baloise.github.io/open-source/docs/arc42/). \ No newline at end of file From af9fdd110bd0dd6da8c09c234bbc68ffa4390417 Mon Sep 17 00:00:00 2001 From: Arthur Neudeck Date: Tue, 15 Jun 2021 11:49:08 +0200 Subject: [PATCH 4/4] Centralized dependency versions in props and fixed junit version. --- pom.xml | 52 +++++++++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/pom.xml b/pom.xml index b4c16dc..0304153 100644 --- a/pom.xml +++ b/pom.xml @@ -45,30 +45,30 @@ 1.8 1.8 - 5.2.0 - 1.2.0 - 1.3 + 5.5.2 + 1.8.5 + 2.0.1 6.5.1 6.5.1 8.0.2 false + ${project.groupId}.${project.artifactId} 2.1.7 + 1.0.2 + 3.7.4 + 3.5 2.8.0 + 20190722 + 2.3.1 + 1.27 + 1.4 + 2.4 1 1.1.1 - 4.12 - 2.0.1 - UTF-8 - 1.8 - 1.8 - - - https://itsec.balgroupit.com/ - com.baloise.open.bitbucket.codefreeze @@ -80,6 +80,13 @@ pom import + + org.junit + junit-bom + ${junit.version} + pom + import + @@ -93,12 +100,12 @@ org.kohsuke groovy-sandbox - 1.27 + ${groovy-sandbox.version} org.json json - 20190722 + ${json.version} @@ -123,7 +130,7 @@ com.atlassian.plugins atlassian-plugins-webresource - 3.7.4 + ${atlassian-plugins-webresource.version} provided @@ -194,26 +201,25 @@ javax.servlet servlet-api - 2.4 + ${servlet-api.version} provided javax.xml.bind jaxb-api - 2.3.1 + ${jaxb-api.version} provided com.atlassian.plugins.rest atlassian-rest-common - 1.0.2 + ${atlassian-rest-common.version} provided - junit - junit - ${junit.libversion} + org.junit.jupiter + junit-jupiter test @@ -226,13 +232,13 @@ org.apache.wink wink-client - 1.4 + ${wink-client.version} test org.mockito mockito-all - 1.8.5 + ${mockito-all.version} test