diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 1a62436670..3841098de5 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -93,7 +93,7 @@ jobs: env: MAVEN_OPTS: -Xmx2g -XX:ReservedCodeCacheSize=1g -XX:+UseG1GC -Dorg.slf4j.simpleLogger.defaultLogLevel=WARN -Duser.language=en -Duser.country=AU -Duser.region=AU -Duser.timezone=Australia/Melbourne -Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false run: | - mvn -V -T 0.7C package verify -B -Pcontrib-check,include-grpc -Ddir-only -ntp -ff -pl -nifi-assembly,-nifi-toolkit/nifi-toolkit-assembly,-nifi-system-tests -nsu + mvn -V -T 0.7C package verify -B -Pcontrib-check,include-grpc,nifi-registry-no-integration-tests -Ddir-only -ntp -ff -pl -nifi-assembly,-nifi-toolkit/nifi-toolkit-assembly,-nifi-system-tests -nsu - name: Upload artifact uses: actions/upload-artifact@v2 if: always() @@ -176,7 +176,7 @@ jobs: env: MAVEN_OPTS: -Xmx2g -XX:ReservedCodeCacheSize=1g -XX:+UseG1GC -Dorg.slf4j.simpleLogger.defaultLogLevel=WARN -Duser.language=ja -Duser.country=JP -Duser.region=JP -Duser.timezone=Asia/Tokyo -Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false run: | - mvn -V -T 0.7C package verify -B -Pcontrib-check,include-grpc -Ddir-only -ntp -ff -pl -nifi-assembly,-nifi-toolkit/nifi-toolkit-assembly,-nifi-system-tests -nsu + mvn -V -T 0.7C package verify -B -Pcontrib-check,include-grpc,nifi-registry-no-integration-tests -Ddir-only -ntp -ff -pl -nifi-assembly,-nifi-toolkit/nifi-toolkit-assembly,-nifi-system-tests -nsu - name: Upload artifact uses: actions/upload-artifact@v2 if: always() @@ -250,7 +250,7 @@ jobs: env: MAVEN_OPTS: -Xmx2g -Dorg.slf4j.simpleLogger.defaultLogLevel=WARN -Duser.language=fr -Duser.country=FR -Duser.region=FR -Duser.timezone=Europe/Paris -Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false run: | - mvn -V -T 0.7C package -B -Ddir-only -ntp -ff -pl -nifi-assembly -pl -nifi-system-tests -nsu + mvn -V -T 0.7C package -B -Pnifi-registry-no-integration-tests -Ddir-only -ntp -ff -pl -nifi-assembly -pl -nifi-system-tests -nsu - name: Upload artifact uses: actions/upload-artifact@v2 if: ${{ false }} diff --git a/LICENSE b/LICENSE index f1db125b9f..58868d4c65 100644 --- a/LICENSE +++ b/LICENSE @@ -364,3 +364,25 @@ This product bundles source from 'AbstractingTheJavaConsole'. The source is avai LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +This product bundles karma-test-shim.js and systemjs-angular-loader.js from 'Angular Quickstart' which is available under an MIT license. + + Copyright (c) 2010-2016 Google, Inc. https://angularjs.org + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 769bb17000..4f91d7a0b2 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ reliable system to process and distribute data. - [Requirements](#requirements) - [Getting Started](#getting-started) - [MiNiFi subproject](#minifi-subproject) +- [Registry subproject](#registry-subproject) - [Getting Help](#getting-help) - [Documentation](#documentation) - [License](#license) @@ -183,6 +184,84 @@ To build: docker run -d -v YOUR_CONFIG.YML:/opt/minifi/minifi-${minifi.version}/conf/config.yml apacheminifi:${minifi.version} ``` +## Registry subproject + +Registry—a subproject of Apache NiFi—is a complementary application that provides a central location for storage and management of shared resources across one or more instances of NiFi and/or MiNiFi. + +### Getting Registry Started + +1) Build nifi + See [Gettin Started](#getting-started) for NiFi + + or + + Build only the Registry subproject: + + cd nifi/nifi-registry + mvn clean install + + If you wish to enable style and license checks, specify the contrib-check profile: + + mvn clean install -Pcontrib-check + +3) Start Registry + + cd nifi-registry/nifi-registry-assembly/target/nifi-registry--bin/nifi-registry-/ + ./bin/nifi-registry.sh start + + Note that the application web server can take a while to load before it is accessible. + +4) Accessing the application web UI + + With the default settings, the application UI will be available at [http://localhost:18080/nifi-registry](http://localhost:18080/nifi-registry) + +5) Accessing the application REST API + + If you wish to test against the application REST API, you can access the REST API directly. With the default settings, the base URL of the REST API will be at `http://localhost:18080/nifi-registry-api`. A UI for testing the REST API will be available at [http://localhost:18080/nifi-registry-api/swagger/ui.html](http://localhost:18080/nifi-registry-api/swagger/ui.html) + +6) Accessing the application logs + + Logs will be available in `logs/nifi-registry-app.log` + +### Database Testing + +In order to ensure that NiFi Registry works correctly against different relational databases, +the existing integration tests can be run against different databases by leveraging the [Testcontainers framework](https://www.testcontainers.org/). + +Spring profiles are used to control the DataSource factory that will be made available to the Spring application context. +DataSource factories are provided that use the Testcontainers framework to start a Docker container for a given database and create a corresponding DataSource. +If no profile is specified then an H2 DataSource will be used by default and no Docker containers are required. + +Assuming Docker is running on the system where the build is running, then the following commands can be run: + +| Target Database | Build Command | +| --------------- | ------------- | +| All supported | `mvn verify -Ptest-all-dbs` | +| H2 (default) | `mvn verify` | +| PostgreSQL 9.x | `mvn verify -Dspring.profiles.active=postgres` | +| PostgreSQL 10.x | `mvn verify -Dspring.profiles.active=postgres-10` | +| MySQL 5.6 | `mvn verify -Pcontrib-check -Dspring.profiles.active=mysql-56` | +| MySQL 5.7 | `mvn verify -Pcontrib-check -Dspring.profiles.active=mysql-57` | +| MySQL 8 | `mvn verify -Pcontrib-check -Dspring.profiles.active=mysql-8` | + + When one of the Testcontainer profiles is activated, the test output should show logs that indicate a container has been started, such as the following: + + 2019-05-15 16:14:45.078 INFO 66091 --- [ main] 🐳 [mysql:5.7] : Creating container for image: mysql:5.7 + 2019-05-15 16:14:45.145 INFO 66091 --- [ main] o.t.utility.RegistryAuthLocator : Credentials not found for host (index.docker.io) when using credential helper/store (docker-credential-osxkeychain) + 2019-05-15 16:14:45.646 INFO 66091 --- [ main] 🐳 [mysql:5.7] : Starting container with ID: ca85c8c5a1990d2a898fad04c5897ddcdb3a9405e695cc11259f50f2ebe67c5f + 2019-05-15 16:14:46.437 INFO 66091 --- [ main] 🐳 [mysql:5.7] : Container mysql:5.7 is starting: ca85c8c5a1990d2a898fad04c5897ddcdb3a9405e695cc11259f50f2ebe67c5f + 2019-05-15 16:14:46.479 INFO 66091 --- [ main] 🐳 [mysql:5.7] : Waiting for database connection to become available at jdbc:mysql://localhost:33051/test?useSSL=false&allowPublicKeyRetrieval=true using query 'SELECT 1' + +The Flyway connection should also indicate the given database: + + 2019-05-15 16:15:02.114 INFO 66091 --- [ main] o.a.n.r.db.CustomFlywayConfiguration : Determined database type is MYSQL + 2019-05-15 16:15:02.115 INFO 66091 --- [ main] o.a.n.r.db.CustomFlywayConfiguration : Setting migration locations to [classpath:db/migration/common, classpath:db/migration/mysql] + 2019-05-15 16:15:02.373 INFO 66091 --- [ main] o.a.n.r.d.CustomFlywayMigrationStrategy : First time initializing database... + 2019-05-15 16:15:02.380 INFO 66091 --- [ main] o.f.c.internal.license.VersionPrinter : Flyway Community Edition 5.2.1 by Boxfuse + 2019-05-15 16:15:02.403 INFO 66091 --- [ main] o.f.c.internal.database.DatabaseFactory : Database: jdbc:mysql://localhost:33051/test (MySQL 5.7) + +For a full list of the available DataSource factories, consult the `nifi-registry-test` module. + ## Getting Help If you have questions, you can reach out to our mailing list: dev@nifi.apache.org ([archive](http://mail-archives.apache.org/mod_mbox/nifi-dev)). For more interactive discussions, community members can often be found in the following locations: diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/pom.xml index f597466699..b965729068 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/pom.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/pom.xml @@ -29,7 +29,7 @@ org.apache.nifi.registry nifi-registry-data-model - ${nifi.registry.version} + 1.14.0-SNAPSHOT diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/pom.xml index e3aee63fe4..f5adfd97ad 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/pom.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/pom.xml @@ -75,17 +75,17 @@ org.apache.nifi.registry nifi-registry-data-model - ${nifi.registry.version} + 1.14.0-SNAPSHOT org.apache.nifi.registry nifi-registry-flow-diff - ${nifi.registry.version} + 1.14.0-SNAPSHOT org.apache.nifi.registry nifi-registry-client - ${nifi.registry.version} + 1.14.0-SNAPSHOT diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/pom.xml index 01115fa0aa..ef72fd9c7b 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/pom.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/pom.xml @@ -68,12 +68,12 @@ language governing permissions and limitations under the License. --> org.apache.nifi.registry nifi-registry-data-model - ${nifi.registry.version} + 1.14.0-SNAPSHOT org.apache.nifi.registry nifi-registry-client - ${nifi.registry.version} + 1.14.0-SNAPSHOT diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml index 0838e36b5b..fd3fcb74e1 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml @@ -185,17 +185,17 @@ org.apache.nifi.registry nifi-registry-data-model - ${nifi.registry.version} + 1.14.0-SNAPSHOT org.apache.nifi.registry nifi-registry-flow-diff - ${nifi.registry.version} + 1.14.0-SNAPSHOT org.apache.nifi.registry nifi-registry-client - ${nifi.registry.version} + 1.14.0-SNAPSHOT com.fasterxml.jackson.core diff --git a/nifi-registry/build-and-run.sh b/nifi-registry/build-and-run.sh new file mode 100755 index 0000000000..168e330e07 --- /dev/null +++ b/nifi-registry/build-and-run.sh @@ -0,0 +1,33 @@ +#!/bin/sh +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +REGISTRY_SCRIPT=`find nifi-registry-assembly/target/ -name nifi-registry.sh | head -1` +REGISTRY_BIN_DIR=$(dirname "${REGISTRY_SCRIPT}") +REGISTRY_DIR=$REGISTRY_BIN_DIR/.. +SKIP_UI=$1 + +./${REGISTRY_SCRIPT} stop + +if [ "$SKIP_UI" == "skipUi" ]; then + mvn clean install -Pcontrib-check --projects \!nifi-registry-web-ui +else + mvn clean install -Pcontrib-check +fi + +./${REGISTRY_SCRIPT} start + +tail -n 500 -f ${REGISTRY_DIR}/logs/nifi-registry-app.log diff --git a/nifi-registry/nifi-registry-assembly/LICENSE b/nifi-registry/nifi-registry-assembly/LICENSE new file mode 100644 index 0000000000..68891c98d1 --- /dev/null +++ b/nifi-registry/nifi-registry-assembly/LICENSE @@ -0,0 +1,1614 @@ + + Apache License + Version 2.0, January 2004 + https://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: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) 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 + + (d) 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 + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + +This product bundles 'Angular Quickstart' which is available under an MIT license. + + Copyright (c) 2010-2016 Google, Inc. https://angularjs.org + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'asm' which is available under a 3-Clause BSD style license. +For details see https://asm.ow2.org/asmdex-license.html + + Copyright (c) 2012 France Télécom + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + THE POSSIBILITY OF SUCH DAMAGE. + +The binary distribution of this product bundles 'Antlr 3' which is available +under a "3-clause BSD" license. For details see https://www.antlr3.org/license.html + + Copyright (c) 2010 Terence Parr + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + Neither the name of the author nor the names of its contributors may be used + to endorse or promote products derived from this software without specific + prior written permission. + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + THE POSSIBILITY OF SUCH DAMAGE. + +The binary distribution of this product bundles 'Bouncy Castle JDK 1.5' +under an MIT style license. + + Copyright (c) 2000 - 2015 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org) + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +The binary distribution of this product bundles 'Slf4j' which is available under +an MIT license. + + Copyright (c) 2004-2013 QOS.ch + All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +The binary distribution of this product bundles 'dom4j' which is available under +a "3-Clause BSD" license. For details: https://github.com/dom4j/dom4j/blob/master/LICENSE + + Copyright 2001-2016 (C) MetaStuff, Ltd. and DOM4J contributors. All Rights Reserved. + + Redistribution and use of this software and associated documentation + ("Software"), with or without modification, are permitted provided + that the following conditions are met: + + 1. Redistributions of source code must retain copyright + statements and notices. Redistributions must also contain a + copy of this document. + + 2. Redistributions in binary form must reproduce the + above copyright notice, this list of conditions and the + following disclaimer in the documentation and/or other + materials provided with the distribution. + + 3. The name "DOM4J" must not be used to endorse or promote + products derived from this Software without prior written + permission of MetaStuff, Ltd. For written permission, + please contact dom4j-info@metastuff.com. + + 4. Products derived from this Software may not be called "DOM4J" + nor may "DOM4J" appear in their names without prior written + permission of MetaStuff, Ltd. DOM4J is a registered + trademark of MetaStuff, Ltd. + + 5. Due credit should be given to the DOM4J Project - https://dom4j.github.io/ + + THIS SOFTWARE IS PROVIDED BY METASTUFF, LTD. AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT + NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + METASTUFF, LTD. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + OF THE POSSIBILITY OF SUCH DAMAGE. + +This product bundles 'Angular Core' which is available under an MIT license. + + Copyright (c) 2014-2017 Google, Inc. https://angular.io + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Angular Common' which is available under an MIT license. + + Copyright (c) 2014-2017 Google, Inc. https://angular.io + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Angular Common Http' which is available under an MIT license. + + Copyright (c) 2014-2017 Google, Inc. https://angular.io + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Angular Platform Browser' which is available under an MIT license. + + Copyright (c) 2014-2017 Google, Inc. https://angular.io + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Angular Platform Browser Dynamic' which is available under an MIT license. + + Copyright (c) 2014-2017 Google, Inc. https://angular.io + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Angular Http' which is available under an MIT license. + + Copyright (c) 2014-2017 Google, Inc. https://angular.io + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Angular Router' which is available under an MIT license. + + Copyright (c) 2014-2017 Google, Inc. https://angular.io + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Angular Forms' which is available under an MIT license. + + Copyright (c) 2014-2017 Google, Inc. https://angular.io + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Angular Flex Layout' which is available under an MIT license. + + Copyright (c) 2017 Google LLC. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Angular Material' which is available under an MIT license. + + Copyright (c) 2017 Google LLC. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Angular Platform Browser Animations' which is available under an MIT license. + + Copyright (c) 2014-2017 Google, Inc. https://angular.io + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Angular CDK' which is available under an MIT license. + + Copyright (c) 2017 Google LLC. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Angular CDK Accordion' which is available under an MIT license. + + Copyright (c) 2017 Google LLC. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Angular CDK Layout' which is available under an MIT license. + + Copyright (c) 2017 Google LLC. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Angular CDK A11y' which is available under an MIT license. + + Copyright (c) 2017 Google LLC. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Angular CDK Collections' which is available under an MIT license. + + Copyright (c) 2017 Google LLC. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Angular CDK Observers' which is available under an MIT license. + + Copyright (c) 2017 Google LLC. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Angular CDK Overlay' which is available under an MIT license. + + Copyright (c) 2017 Google LLC. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Angular Platform' which is available under an MIT license. + + Copyright (c) 2017 Google LLC. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Angular CDK Portal' which is available under an MIT license. + + Copyright (c) 2017 Google LLC. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Angular Keycodes' which is available under an MIT license. + + Copyright (c) 2017 Google LLC. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Angular CDK Bidi' which is available under an MIT license. + + Copyright (c) 2017 Google LLC. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Angular CDK Coercion' which is available under an MIT license. + + Copyright (c) 2017 Google LLC. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Angular CDK Table' which is available under an MIT license. + + Copyright (c) 2017 Google LLC. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Angular CDK RXJS' which is available under an MIT license. + + Copyright (c) 2017 Google LLC. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Angular CDK Scrolling' which is available under an MIT license. + + Copyright (c) 2017 Google LLC. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Angular CDK Stepper' which is available under an MIT license. + + Copyright (c) 2017 Google LLC. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Angular Animations' which is available under an MIT license. + + Copyright (c) 2014-2017 Google, Inc. https://angular.io + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Angular Animations Browser' which is available under an MIT license. + + Copyright (c) 2014-2017 Google, Inc. https://angular.io + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Angular Compiler' which is available under an MIT license. + + Copyright (c) 2014-2017 Google, Inc. https://angular.io + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Hammer JS' which is available under an MIT license. + + Copyright (C) 2011-2017 by Jorik Tangelder (Eight Media) + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Covalent Core' which is available under an MIT license. + + Copyright (c) 2016 by Teradata. All rights reserved. https://teradata.com + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Moment JS' which is available under an MIT license. + + Copyright (c) JS Foundation and other contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Angular2 Moment' which is available under an MIT license. + + Copyright (c) 2013-2017 Uri Shaked and contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Zone JS' which is available under an MIT license. + + Copyright (c) 2016 Google, Inc. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Core JS' which is available under an MIT license. + + Copyright (c) 2014-2017 Denis Pushkarev + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'SuperAgent' which is available under an MIT license. + + Copyright (c) 2014-2016 TJ Holowaychuk + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Querystring' which is available under an MIT license. + + Copyright 2012 Irakli Gozalishvili + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'System JS' which is available under an MIT license. + + Copyright (C) 2013-2016 Guy Bedford + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'System JS Plugin Text' which is available under an MIT license. + + Copyright (c) 2013 jspm + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'jQuery' which is available under an MIT license. + + Copyright JS Foundation and other contributors, https://js.foundation/ + + This software consists of voluntary contributions made by many + individuals. For exact contribution history, see the revision history + available at https://github.com/jquery/jquery + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'JSch' which is available under a 3-Clause BSD style license. +For details see https://www.jcraft.com/jsch/LICENSE.txt + + Copyright (c) 2002-2015 Atsuhiko Yamanaka, JCraft,Inc. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, + INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +This product bundles 'JGit' which is available under a Eclipse Distribution License - v 1.0 license. +For details see https://www.eclipse.org/org/documents/edl-v10.php + + Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors. + + All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, are permitted provided that the following + conditions are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + - Neither the name of the Eclipse Foundation, Inc. nor the + names of its contributors may be used to endorse or promote + products derived from this software without specific prior + written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +This product bundles 'Jakarta jaxb-api' which is available under a Eclipse Distribution License - v 1.0 license. +For details see https://www.eclipse.org/org/documents/edl-v10.php + + Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors. + + All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, are permitted provided that the following + conditions are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + - Neither the name of the Eclipse Foundation, Inc. nor the + names of its contributors may be used to endorse or promote + products derived from this software without specific prior + written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +This product bundles 'Jakarta jaxb-ri' which is available under a Eclipse Distribution License - v 1.0 license. +For details see https://www.eclipse.org/org/documents/edl-v10.php + + Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors. + + All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, are permitted provided that the following + conditions are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + - Neither the name of the Eclipse Foundation, Inc. nor the + names of its contributors may be used to endorse or promote + products derived from this software without specific prior + written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +This product bundles 'Jakarta jaxb-fi' which is available under a Eclipse Distribution License - v 1.0 license. +For details see https://www.eclipse.org/org/documents/edl-v10.php + + Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors. + + All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, are permitted provided that the following + conditions are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + - Neither the name of the Eclipse Foundation, Inc. nor the + names of its contributors may be used to endorse or promote + products derived from this software without specific prior + written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +This product bundles 'Jakarta jaxb-istack-commons' which is available under a Eclipse Distribution License - v 1.0 license. +For details see https://www.eclipse.org/org/documents/edl-v10.php + + Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors. + + All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, are permitted provided that the following + conditions are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + - Neither the name of the Eclipse Foundation, Inc. nor the + names of its contributors may be used to endorse or promote + products derived from this software without specific prior + written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +This product bundles 'Jakarta jaxb-stax-ex' which is available under a Eclipse Distribution License - v 1.0 license. +For details see https://www.eclipse.org/org/documents/edl-v10.php + + Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors. + + All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, are permitted provided that the following + conditions are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + - Neither the name of the Eclipse Foundation, Inc. nor the + names of its contributors may be used to endorse or promote + products derived from this software without specific prior + written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +This product bundles 'Jakarta txw2 runtime' which is available under a Eclipse Distribution License - v 1.0 license. +For details see https://www.eclipse.org/org/documents/edl-v10.php + + Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors. + + All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, are permitted provided that the following + conditions are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + - Neither the name of the Eclipse Foundation, Inc. nor the + names of its contributors may be used to endorse or promote + products derived from this software without specific prior + written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +This product bundles 'Jakarta Activation' which is available under a Eclipse Distribution License - v 1.0 license. +For details see https://www.eclipse.org/org/documents/edl-v10.php + + Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors. + + All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, are permitted provided that the following + conditions are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + - Neither the name of the Eclipse Foundation, Inc. nor the + names of its contributors may be used to endorse or promote + products derived from this software without specific prior + written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/nifi-registry/nifi-registry-assembly/NOTICE b/nifi-registry/nifi-registry-assembly/NOTICE new file mode 100644 index 0000000000..4e77c5cc3f --- /dev/null +++ b/nifi-registry/nifi-registry-assembly/NOTICE @@ -0,0 +1,319 @@ +Apache NiFi Registry +Copyright 2017-2020 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (https://www.apache.org/). + +This includes derived works from the Apache NiFi (ASLv2 licensed) project (https://git-wip-us.apache.org/repos/asf?p=nifi.git): + Copyright 2015-2020 The Apache Software Foundation + This includes sources for bootstrapping, runtime, component API, security/authorization API +=========================================== +Apache Software License v2 +=========================================== + +The following binary components are provided under the Apache Software License v2 + + (ASLv2) Jetty + The following NOTICE information applies: + Jetty Web Container + Copyright 1995-2019 Mort Bay Consulting Pty Ltd. + + (ASLv2) Apache Commons Codec + The following NOTICE information applies: + Apache Commons Codec + Copyright 2002-2014 The Apache Software Foundation + + src/test/org/apache/commons/codec/language/DoubleMetaphoneTest.java + contains test data from https://aspell.net/test/orig/batch0.tab. + Copyright (C) 2002 Kevin Atkinson (kevina@gnu.org) + + =============================================================================== + + The content of package org.apache.commons.codec.language.bm has been translated + from the original php source code available at https://stevemorse.org/phoneticinfo.htm + with permission from the original authors. + Original source copyright: + Copyright (c) 2008 Alexander Beider & Stephen P. Morse. + + (ASLv2) Apache Commons Lang + The following NOTICE information applies: + Apache Commons Lang + Copyright 2001-2017 The Apache Software Foundation + + This product includes software from the Spring Framework, + under the Apache License 2.0 (see: StringUtils.containsWhitespace()) + + (ASLv2) Jackson JSON processor + The following NOTICE information applies: + # Jackson JSON processor + + Jackson is a high-performance, Free/Open Source JSON processing library. + It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has + been in development since 2007. + It is currently developed by a community of developers, as well as supported + commercially by FasterXML.com. + + ## Licensing + + Jackson core and extension components may licensed under different licenses. + To find the details that apply to this artifact see the accompanying LICENSE file. + For more information, including possible other licensing options, contact + FasterXML.com (https://fasterxml.com). + + ## Credits + + A list of contributors may be found from CREDITS file, which is included + in some artifacts (usually source distributions); but is always available + from the source code management (SCM) system project uses. + + (ASLv2) Java Native Access Platform + The following NOTICE information applies: + Java Native Access Platform + Copyright 2013 Timothy Wall, Matthias Bläsing + + This product includes software developed by + The Apache Software Foundation (https://www.apache.org/). + + =============================================================================== + + The BracketFinder (package org.apache.commons.math3.optimization.univariate) + and PowellOptimizer (package org.apache.commons.math3.optimization.general) + classes are based on the Python code in module "optimize.py" (version 0.5) + developed by Travis E. Oliphant for the SciPy library (https://www.scipy.org/) + Copyright © 2003-2009 SciPy Developers. + =============================================================================== + + The LinearConstraint, LinearObjectiveFunction, LinearOptimizer, + RelationShip, SimplexSolver and SimplexTableau classes in package + org.apache.commons.math3.optimization.linear include software developed by + Benjamin McCann (https://www.benmccann.com) and distributed with + the following copyright: Copyright 2009 Google Inc. + =============================================================================== + + This product includes software developed by the + University of Chicago, as Operator of Argonne National + Laboratory. + The LevenbergMarquardtOptimizer class in package + org.apache.commons.math3.optimization.general includes software + translated from the lmder, lmpar and qrsolv Fortran routines + from the Minpack package + Minpack Copyright Notice (1999) University of Chicago. All rights reserved + =============================================================================== + + The GraggBulirschStoerIntegrator class in package + org.apache.commons.math3.ode.nonstiff includes software translated + from the odex Fortran routine developed by E. Hairer and G. Wanner. + Original source copyright: + Copyright (c) 2004, Ernst Hairer + =============================================================================== + + The EigenDecompositionImpl class in package + org.apache.commons.math3.linear includes software translated + from some LAPACK Fortran routines. Original source copyright: + Copyright (c) 1992-2008 The University of Tennessee. All rights reserved. + =============================================================================== + + The MersenneTwister class in package org.apache.commons.math3.random + includes software translated from the 2002-01-26 version of + the Mersenne-Twister generator written in C by Makoto Matsumoto and Takuji + Nishimura. Original source copyright: + Copyright (C) 1997 - 2002, Makoto Matsumoto and Takuji Nishimura, + All rights reserved + =============================================================================== + + The LocalizedFormatsTest class in the unit tests is an adapted version of + the OrekitMessagesTest class from the orekit library distributed under the + terms of the Apache 2 licence. Original source copyright: + Copyright 2010 CS Systèmes d'Information + =============================================================================== + + The HermiteInterpolator class and its corresponding test have been imported from + the orekit library distributed under the terms of the Apache 2 licence. Original + source copyright: + Copyright 2010-2012 CS Systèmes d'Information + =============================================================================== + + The creation of the package "o.a.c.m.analysis.integration.gauss" was inspired + by an original code donated by Sébastien Brisard. + =============================================================================== + + (ASLv2) JSON-SMART + The following NOTICE information applies: + Copyright 2011 JSON-SMART authors + + (ASLv2) JsonPath + The following NOTICE information applies: + Copyright 2011 JsonPath authors + + (ASLv2) Classmate + The following NOTICE information applies + Java ClassMate library was originally written by Tatu Saloranta (tatu.saloranta@iki.fi) + + Other developers who have contributed code are: + + * Brian Langel + + (ASLv2) Apache Commons IO + The following NOTICE information applies: + Apache Commons IO + Copyright 2002-2016 The Apache Software Foundation + + (ASLv2) Apache log4j + The following NOTICE information applies: + Apache log4j + Copyright 2010 The Apache Software Foundation + + (ASLv2) Spring Framework + The following NOTICE information applies: + Spring Framework 5.1.8.RELEASE + Copyright (c) 2002-2019 Pivotal, Inc. + + (ASLv2) Spring Security + The following NOTICE information applies: + Spring Framework 5.1.5.RELEASE + Copyright (c) 2002-2019 Pivotal, Inc. + + This product includes software developed by Spring Security + Project (https://www.springframework.org/security). + + (ASLv2) Spring LDAP + The following NOTICE information applies: + Spring LDAP 2.3.2.RELEASE + Copyright (c) 2002-2017 Pivotal, Inc. + + This product includes software developed by the Spring LDAP + Project (https://www.springframework.org/ldap). + + (ASLv2) Apache Tomcat Embed EL + The following NOTICE information applies: + Apache Tomcat + Copyright 1999-2017 The Apache Software Foundation + + This product includes software developed at + The Apache Software Foundation (https://www.apache.org/). + + This software contains code derived from netty-native + developed by the Netty project + (https://netty.io, https://github.com/netty/netty-tcnative/) + and from finagle-native developed at Twitter + (https://github.com/twitter/finagle). + + The Windows Installer is built with the Nullsoft + Scriptable Install System (NSIS), which is + open source software. The original software and + related information is available at + https://nsis.sourceforge.net. + + Java compilation software for JSP pages is provided by the Eclipse + JDT Core Batch Compiler component, which is open source software. + The original software and related information is available at + https://www.eclipse.org/jdt/core/. + + For portions of the Tomcat JNI OpenSSL API and the OpenSSL JSSE integration + The org.apache.tomcat.jni and the org.apache.tomcat.net.openssl packages + are derivative work originating from the Netty project and the finagle-native + project developed at Twitter + * Copyright 2014 The Netty Project + * Copyright 2014 Twitter + + The original XML Schemas for Java EE Deployment Descriptors: + - javaee_5.xsd + - javaee_web_services_1_2.xsd + - javaee_web_services_client_1_2.xsd + - javaee_6.xsd + - javaee_web_services_1_3.xsd + - javaee_web_services_client_1_3.xsd + - jsp_2_2.xsd + - web-app_3_0.xsd + - web-common_3_0.xsd + - web-fragment_3_0.xsd + - javaee_7.xsd + - javaee_web_services_1_4.xsd + - javaee_web_services_client_1_4.xsd + - jsp_2_3.xsd + - web-app_3_1.xsd + - web-common_3_1.xsd + - web-fragment_3_1.xsd + - javaee_8.xsd + - web-app_4_0.xsd + - web-common_4_0.xsd + - web-fragment_4_0.xsd + + may be obtained from: + https://www.oracle.com/webfolder/technetwork/jsc/xml/ns/javaee/index.html + + (ASLv2) SnakeYAML + The following NOTICE information applies: + Copyright (c) 2008, https://www.snakeyaml.org + + (ASLv2) Swagger UI + The following NOTICE information applies: + Copyright 2017 SmartBear Software + + (ASLv2) Nimbus OAuth 2.0 SDK with OpenID Connect extensions + The following NOTICE information applies: + Nimbus OAuth 2.0 SDK with OpenID Connect extensions + Copyright 2012-2020, Connect2id Ltd and contributors. + + (ASLv2) Guava + The following NOTICE information applies: + Guava + Copyright 2015 The Guava Authors + +************************ +Common Development and Distribution License 1.1 +************************ + +The following binary components are provided under the Common Development and Distribution License 1.1. See project link for details. + + (CDDL 1.1) (GPL2 w/ CPE) JavaMail API (compat) (javax.mail:mail:jar:1.4.7 - https://kenai.com/projects/javamail/mail) + (CDDL 1.1) (GPL2 w/ CPE) Java Servlet API (javax.servlet:javax.servlet-api:jar:3.1.0 - https://servlet-spec.java.net) + (CDDL 1.1) (GPL2 w/ CPE) JavaServer Pages Standard Tag Library (javax.servlet.jsp.jstl:jstl:jar:1.2 - https://javaee.github.io/jstl-api/) + (CDDL 1.1) (GPL2 w/ CPE) javax.annotation API (javax.annotation:javax.annotation-api:jar:1.2 - https://jcp.org/en/jsr/detail?id=250) + (CDDL 1.1) (GPL2 w/ CPE) aopalliance-repackaged (org.glassfish.hk2.external:aopalliance-repackaged:jar:2.5.0-b42 - https://javaee.github.io/glassfish/) + (CDDL 1.1) (GPL2 w/ CPE) asm-all-repackaged (org.glassfish.hk2.external:asm-all-repackaged:jar:2.5.0-b42 - https://javaee.github.io/glassfish/) + (CDDL 1.1) (GPL2 w/ CPE) class-model (org.glassfish.hk2:class-model:jar:2.5.0-b42 - https://javaee.github.io/glassfish/) + (CDDL 1.1) (GPL2 w/ CPE) config-types (org.glassfish.hk2:config-types:jar:2.5.0-b42 - https://javaee.github.io/glassfish/) + (CDDL 1.1) (GPL2 w/ CPE) hk2 (org.glassfish.hk2:hk2:jar:2.5.0-b42 - https://javaee.github.io/glassfish/) + (CDDL 1.1) (GPL2 w/ CPE) hk2-api (org.glassfish.hk2:hk2-api:jar:2.5.0-b42 - https://javaee.github.io/glassfish/) + (CDDL 1.1) (GPL2 w/ CPE) hk2-utils (org.glassfish.hk2:hk2-utils:jar:2.5.0-b42 - https://javaee.github.io/glassfish/) + (CDDL 1.1) (GPL2 w/ CPE) hk2-locator (org.glassfish.hk2:hk2-locator:jar:2.5.0-b42 - https://javaee.github.io/glassfish/) + (CDDL 1.1) (GPL2 w/ CPE) hk2-config (org.glassfish.hk2:hk2-config:jar:2.5.0-b42 - https://javaee.github.io/glassfish/) + (CDDL 1.1) (GPL2 w/ CPE) hk2-core (org.glassfish.hk2:hk2-core:jar:2.5.0-b42 - https://javaee.github.io/glassfish/) + (CDDL 1.1) (GPL2 w/ CPE) hk2-runlevel (org.glassfish.hk2:hk2-runlevel:jar:2.5.0-b42 - https://javaee.github.io/glassfish/) + (CDDL 1.1) (GPL2 w/ CPE) spring-bridge (org.glassfish.hk2:spring-bridge:jar:2.5.0-b42 - https://javaee.github.io/glassfish/) + (CDDL 1.1) (GPL2 w/ CPE) javax.inject:1 as OSGi bundle (org.glassfish.hk2.external:javax.inject:jar:2.4.0-b25 - https://hk2.java.net/external/javax.inject) + (CDDL 1.1) (GPL2 w/ CPE) javax.ws.rs-api (javax.ws.rs:javax.ws.rs-api:jar:2.1 - https://jax-rs-spec.java.net) + (CDDL 1.1) (GPL2 w/ CPE) javax.el (org.glassfish:javax.el:jar:3.0.1-b08 - https://github.com/javaee/el-spec) + (CDDL 1.1) (GPL2 w/ CPE) jersey-bean-validation (org.glassfish.jersey.ext:jersey-bean-validation:jar:2.27 - https://jersey.github.io/) + (CDDL 1.1) (GPL2 w/ CPE) jersey-client (org.glassfish.jersey.core:jersey-client:jar:2.27 - https://jersey.github.io/) + (CDDL 1.1) (GPL2 w/ CPE) jersey-common (org.glassfish.jersey.core:jersey-common:jar:2.27 - https://jersey.github.io/) + (CDDL 1.1) (GPL2 w/ CPE) jersey-container-servlet-core (org.glassfish.jersey.containers:jersey-container-servlet-core:jar:2.27 - https://jersey.github.io/) + (CDDL 1.1) (GPL2 w/ CPE) jersey-entity-filtering (org.glassfish.jersey.ext:jersey-entity-filtering:jar:2.27 - https://jersey.github.io/) + (CDDL 1.1) (GPL2 w/ CPE) jersey-hk2 (org.glassfish.jersey.inject:jersey-hk2:jar:2.27 - https://jersey.github.io/) + (CDDL 1.1) (GPL2 w/ CPE) jersey-media-jaxb (org.glassfish.jersey.media:jersey-media-jaxb:jar:2.27 - https://jersey.github.io/) + (CDDL 1.1) (GPL2 w/ CPE) jersey-media-json-jackson (org.glassfish.jersey.media:jersey-media-json-jackson:jar:2.27 - https://jersey.github.io/) + (CDDL 1.1) (GPL2 w/ CPE) jersey-server (org.glassfish.jersey.core:jersey-server:jar:2.27 - https://jersey.github.io/) + (CDDL 1.1) (GPL2 w/ CPE) jersey-spring4 (org.glassfish.jersey.ext:jersey-spring4:jar:2.27 - https://jersey.github.io/) + (CDDL 1.1) (GPL2 w/ CPE) OSGi resource locator bundle (org.glassfish.hk2:osgi-resource-locator:jar:1.0.1 - https://glassfish.org/osgi-resource-locator) + + +************************ +Common Development and Distribution License 1.0 +************************ + +The following binary components are provided under the Common Development and Distribution License 1.0. See project link for details. + + (CDDL 1.0) JavaBeans Activation Framework (JAF) (javax.activation:activation:jar:1.1 - https://java.sun.com/products/javabeans/jaf/index.jsp) + + +************************ +Eclipse Public License 1.0 +************************ + +The following binary components are provided under the Eclipse Public License 1.0. See project link for details. + + (EPL 1.0)(MPL 2.0) H2 Database (com.h2database:h2:jar:h2-1.4.199 - https://www.h2database.com/html/license.html) + (EPL 1.0)(LGPL 2.1) Logback Classic (ch.qos.logback:logback-classic:jar:1.2.3 - https://logback.qos.ch/) + (EPL 1.0)(LGPL 2.1) Logback Core (ch.qos.logback:logback-core:jar:1.2.3 - https://logback.qos.ch/) + (EPL 1.0) AspectJ Weaver (org.aspectj:aspectjweaver:jar:1.8.13 - https://www.eclipse.org/aspectj/) diff --git a/nifi-registry/nifi-registry-assembly/README.md b/nifi-registry/nifi-registry-assembly/README.md new file mode 100644 index 0000000000..2983eb3a0a --- /dev/null +++ b/nifi-registry/nifi-registry-assembly/README.md @@ -0,0 +1,63 @@ + +# Apache NiFi Registry + +Registry—a subproject of Apache NiFi—is a complementary application that provides a central location for storage and management of shared resources across one or more instances of NiFi and/or MiNiFi. + +## Table of Contents + +- [Requirements](#requirements) +- [Getting Started](#getting-started) +- [Getting Help](#getting-help) +- [License](#license) + +## Requirements + +* Java 1.8 (above 1.8.0_45) + +## Getting Started + +To start NiFi Registry: +- [linux/osx] execute bin/nifi-registry.sh start +- [windows] execute bin/run-nifi-registry.bat +- Direct your browser to http://localhost:18080/nifi-registry/ + +## Getting Help + +If you have questions, you can reach out to our mailing list: dev@nifi.apache.org +([archive](http://mail-archives.apache.org/mod_mbox/nifi-dev)). For more interactive discussions, community members can often be found in the following locations: + +- Apache NiFi Slack Workspace: https://apachenifi.slack.com/ + + New users can join the workspace using the following [invite link](https://s.apache.org/nifi-community-slack). + +- IRC: #nifi on [irc.freenode.net](http://webchat.freenode.net/?channels=#nifi) + +## License + +Except as otherwise noted this software is licensed under the +[Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html) + +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/nifi-registry/nifi-registry-assembly/pom.xml b/nifi-registry/nifi-registry-assembly/pom.xml new file mode 100644 index 0000000000..1f0a9ea756 --- /dev/null +++ b/nifi-registry/nifi-registry-assembly/pom.xml @@ -0,0 +1,493 @@ + + + + 4.0.0 + + org.apache.nifi.registry + nifi-registry + 1.14.0-SNAPSHOT + + nifi-registry-assembly + pom + This is the assembly for nifi-registry. + + + + maven-assembly-plugin + + nifi-registry-${project.version} + false + + + + make shared resource + + single + + package + + + 0775 + 0775 + 0664 + + + src/main/assembly/dependencies.xml + + posix + + + + + + + + + ch.qos.logback + logback-classic + + + org.slf4j + jcl-over-slf4j + + + org.slf4j + jul-to-slf4j + + + org.slf4j + log4j-over-slf4j + + + org.slf4j + slf4j-api + compile + + + org.apache.commons + commons-lang3 + + + org.apache.nifi.registry + nifi-registry-utils + + + org.apache.nifi.registry + nifi-registry-bootstrap + + + org.apache.nifi.registry + nifi-registry-runtime + + + org.apache.nifi.registry + nifi-registry-security-api + + + org.apache.nifi.registry + nifi-registry-provider-api + + + org.apache.nifi.registry + nifi-registry-web-ui + war + + + org.apache.nifi.registry + nifi-registry-web-api + war + + + org.apache.nifi.registry + nifi-registry-web-docs + war + 1.14.0-SNAPSHOT + + + org.apache.nifi.registry + nifi-registry-resources + resources + runtime + zip + + + org.apache.nifi.registry + nifi-registry-docs + 1.14.0-SNAPSHOT + resources + runtime + zip + + + + + jakarta.xml.bind + jakarta.xml.bind-api + + + org.glassfish.jaxb + jaxb-runtime + + + + + + ./lib + + 18080 + + + ./work/jetty + 200 + true + + + + + + + + + + + ./conf/authorizers.xml + managed-authorizer + ./conf/identity-providers.xml + + + + ./conf/providers.xml + + + ./conf/registry-aliases.xml + + + ./work/extensions + + + + + + + + jdbc:h2:./database/nifi-registry-primary;AUTOCOMMIT=OFF;DB_CLOSE_ON_EXIT=FALSE;LOCK_MODE=3;LOCK_TIMEOUT=25000;WRITE_DELAY=0;AUTO_SERVER=FALSE + org.h2.Driver + + nifireg + nifireg + 5 + false + + + + + + 12 hours + + + + + + + + + + + false + + + + + + rpm + + false + + + nifi + + + + + maven-dependency-plugin + + + unpack-shared-resources + + unpack-dependencies + + generate-resources + + ${project.build.directory}/generated-resources + nifi-registry-resources + org.apache.nifi.registry + false + + + + unpack-docs + + unpack-dependencies + + generate-resources + + ${project.build.directory}/generated-docs + nifi-registry-docs + org.apache.nifi.registry + false + LICENSE,NOTICE + + + + + + org.codehaus.mojo + rpm-maven-plugin + + nifi-registry + Apache NiFi Registry + A sub-project of Apache NiFi that provides a central location for storage and management of shared resources across one or more instances of NiFi and/or MiNiFi. + Apache License, Version 2.0 and others (see included LICENSE file) + https://nifi.apache.org/registry.html + Utilities + /opt/nifi-registry + + _use_internal_dependency_generator 0 + + 750 + 640 + nifi + nifi + + + + + + + + + + + + build-bin-rpm + + attached-rpm + + + Apache NiFI + bin + + nifi-registry + + + + /opt/nifi-registry/nifi-registry-${project.version} + + + /opt/nifi-registry/nifi-registry-${project.version} + + + ./LICENSE + + + ./NOTICE + + + ./README.md + README + + + + + /opt/nifi-registry + + + /opt/nifi-registry/nifi-registry-${project.version}/bin + 750 + + + ${project.build.directory}/generated-resources/bin/nifi-registry.sh + nifi-registry.sh + true + + + ${project.build.directory}/generated-resources/bin/nifi-registry-env.sh + nifi-registry-env.sh + true + + + + + /opt/nifi-registry/nifi-registry-${project.version}/conf + true + + + ${project.build.directory}/generated-resources/conf + true + + + + + /opt/nifi-registry/nifi-registry-${project.version}/lib + + + + /opt/nifi-registry/nifi-registry-${project.version}/lib + + + org.apache.commons:commons-lang3 + org.apache.nifi.registry:nifi-registry-utils + org.apache.nifi.registry:nifi-registry-bootstrap + org.apache.nifi.registry:nifi-registry-docs + + + + + /opt/nifi-registry/nifi-registry-${project.version}/lib/bootstrap + + + org.slf4j:slf4j-api + ch.qos.logback:logback-classic + ch.qos.logback:logback-core + org.apache.nifi.registry:nifi-registry-bootstrap + + + org.apache.commons:commons-lang3 + org.apache.nifi.registry:nifi-registry-utils + + + + + /opt/nifi-registry/nifi-registry-${project.version}/lib/shared + + + org.apache.commons:commons-lang3 + org.apache.nifi.registry:nifi-registry-utils + + + + + /opt/nifi-registry/nifi-registry-${project.version}/docs + + + ${project.build.directory}/generated-docs + + + + + + + + + + + + + include-ranger + + false + + + + org.apache.nifi.registry + nifi-registry-ranger-assembly + 1.14.0-SNAPSHOT + bin + runtime + zip + + + + + + maven-dependency-plugin + + + unpack-ranger-extensions + + unpack-dependencies + + generate-resources + + ${project.build.directory}/ext/ranger + org.apache.nifi.registry + nifi-registry-ranger-assembly + false + + + + + + + + + include-aws + + + !skipAws + + + + ./ext/aws/lib + + + + org.apache.nifi.registry + nifi-registry-aws-assembly + 1.14.0-SNAPSHOT + bin + runtime + zip + + + + + + maven-dependency-plugin + + + unpack-aws-extensions + + unpack-dependencies + + generate-resources + + ${project.build.directory}/ext/aws + org.apache.nifi.registry + nifi-registry-aws-assembly + false + + + + + + + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-assembly/src/main/assembly/dependencies.xml b/nifi-registry/nifi-registry-assembly/src/main/assembly/dependencies.xml new file mode 100644 index 0000000000..0f0d39cda1 --- /dev/null +++ b/nifi-registry/nifi-registry-assembly/src/main/assembly/dependencies.xml @@ -0,0 +1,187 @@ + + + + bin + + dir + zip + tar.gz + + true + nifi-registry-${project.version} + + + + + runtime + false + lib/shared + 0770 + 0664 + true + + nifi-registry-utils + commons-lang3 + + + + + + runtime + false + lib/bootstrap + 0770 + 0664 + true + + nifi-registry-bootstrap + slf4j-api + logback-classic + + + + + + + runtime + false + lib/java11 + 0770 + 0664 + true + + jakarta.xml.bind:jakarta.xml.bind-api + org.glassfish.jaxb:jaxb-runtime + + + + + + runtime + false + lib + 0770 + 0664 + true + + nifi-registry-resources + nifi-registry-bootstrap + nifi-registry-utils + nifi-registry-docs + nifi-registry-ranger-assembly + nifi-registry-aws-assembly + + + + jakarta.xml.bind:jakarta.xml.bind-api + org.glassfish.jaxb:jaxb-runtime + + + + + + runtime + false + ./ + 0770 + 0664 + true + + nifi-registry-resources + + true + + true + + conf/* + + + + + + + runtime + false + ./ + 0770 + 0770 + true + + nifi-registry-resources + + true + + true + + bin/* + + + + + + + runtime + false + docs/ + true + + nifi-registry-docs + + true + + false + + + LICENSE + NOTICE + + + + + + + + ./README.md + ./ + README + 0644 + true + + + ./LICENSE + ./ + LICENSE + 0644 + true + + + ./NOTICE + ./ + NOTICE + 0644 + true + + + + + + + ${project.build.directory}/ext + ext + + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bootstrap/pom.xml b/nifi-registry/nifi-registry-core/nifi-registry-bootstrap/pom.xml new file mode 100644 index 0000000000..eb5d3d8fa0 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-bootstrap/pom.xml @@ -0,0 +1,40 @@ + + + + 4.0.0 + + org.apache.nifi.registry + nifi-registry-core + 1.14.0-SNAPSHOT + + + nifi-registry-bootstrap + jar + + + + org.apache.nifi.registry + nifi-registry-utils + 1.14.0-SNAPSHOT + + + org.apache.commons + commons-lang3 + + + net.java.dev.jna + jna-platform + 4.4.0 + + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/BootstrapCodec.java b/nifi-registry/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/BootstrapCodec.java new file mode 100644 index 0000000000..a273e074fc --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/BootstrapCodec.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.bootstrap; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.util.Arrays; + +import org.apache.nifi.registry.bootstrap.exception.InvalidCommandException; + +public class BootstrapCodec { + + private final RunNiFiRegistry runner; + private final BufferedReader reader; + private final BufferedWriter writer; + + public BootstrapCodec(final RunNiFiRegistry runner, final InputStream in, final OutputStream out) { + this.runner = runner; + this.reader = new BufferedReader(new InputStreamReader(in)); + this.writer = new BufferedWriter(new OutputStreamWriter(out)); + } + + public void communicate() throws IOException { + final String line = reader.readLine(); + final String[] splits = line.split(" "); + if (splits.length < 0) { + throw new IOException("Received invalid command from NiFi Registry: " + line); + } + + final String cmd = splits[0]; + final String[] args; + if (splits.length == 1) { + args = new String[0]; + } else { + args = Arrays.copyOfRange(splits, 1, splits.length); + } + + try { + processRequest(cmd, args); + } catch (final InvalidCommandException ice) { + throw new IOException("Received invalid command from NiFi Registry: " + line + (ice.getMessage() == null ? "" : " - Details: " + ice.toString())); + } + } + + private void processRequest(final String cmd, final String[] args) throws InvalidCommandException, IOException { + switch (cmd) { + case "PORT": { + if (args.length != 2) { + throw new InvalidCommandException(); + } + + final int port; + try { + port = Integer.parseInt(args[0]); + } catch (final NumberFormatException nfe) { + throw new InvalidCommandException("Invalid Port number; should be integer between 1 and 65535"); + } + + if (port < 1 || port > 65535) { + throw new InvalidCommandException("Invalid Port number; should be integer between 1 and 65535"); + } + + final String secretKey = args[1]; + + runner.setNiFiRegistryCommandControlPort(port, secretKey); + writer.write("OK"); + writer.newLine(); + writer.flush(); + } + break; + case "STARTED": { + if (args.length != 1) { + throw new InvalidCommandException("STARTED command must contain a status argument"); + } + + if (!"true".equals(args[0]) && !"false".equals(args[0])) { + throw new InvalidCommandException("Invalid status for STARTED command; should be true or false, but was '" + args[0] + "'"); + } + + final boolean started = Boolean.parseBoolean(args[0]); + runner.setNiFiRegistryStarted(started); + writer.write("OK"); + writer.newLine(); + writer.flush(); + } + break; + } + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/NiFiRegistryListener.java b/nifi-registry/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/NiFiRegistryListener.java new file mode 100644 index 0000000000..f2ead2e4ec --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/NiFiRegistryListener.java @@ -0,0 +1,141 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.bootstrap; + +import org.apache.nifi.registry.bootstrap.util.LimitingInputStream; + +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +public class NiFiRegistryListener { + + private ServerSocket serverSocket; + private volatile Listener listener; + + int start(final RunNiFiRegistry runner) throws IOException { + serverSocket = new ServerSocket(); + serverSocket.bind(new InetSocketAddress("localhost", 0)); + + final int localPort = serverSocket.getLocalPort(); + listener = new Listener(serverSocket, runner); + final Thread listenThread = new Thread(listener); + listenThread.setName("Listen to NiFi Registry"); + listenThread.setDaemon(true); + listenThread.start(); + return localPort; + } + + public void stop() throws IOException { + final Listener listener = this.listener; + if (listener == null) { + return; + } + + listener.stop(); + } + + private class Listener implements Runnable { + + private final ServerSocket serverSocket; + private final ExecutorService executor; + private final RunNiFiRegistry runner; + private volatile boolean stopped = false; + + public Listener(final ServerSocket serverSocket, final RunNiFiRegistry runner) { + this.serverSocket = serverSocket; + this.executor = Executors.newFixedThreadPool(2, new ThreadFactory() { + @Override + public Thread newThread(final Runnable runnable) { + final Thread t = Executors.defaultThreadFactory().newThread(runnable); + t.setDaemon(true); + t.setName("NiFi Registry Bootstrap Command Listener"); + return t; + } + }); + + this.runner = runner; + } + + public void stop() throws IOException { + stopped = true; + + executor.shutdown(); + try { + executor.awaitTermination(3, TimeUnit.SECONDS); + } catch (final InterruptedException ie) { + } + + serverSocket.close(); + } + + @Override + public void run() { + while (!serverSocket.isClosed()) { + try { + if (stopped) { + return; + } + + final Socket socket; + try { + socket = serverSocket.accept(); + } catch (final IOException ioe) { + if (stopped) { + return; + } + + throw ioe; + } + + executor.submit(new Runnable() { + @Override + public void run() { + try { + // we want to ensure that we don't try to read data from an InputStream directly + // by a BufferedReader because any user on the system could open a socket and send + // a multi-gigabyte file without any new lines in order to crash the Bootstrap, + // which in turn may cause the Shutdown Hook to shutdown NiFi. + // So we will limit the amount of data to read to 4 KB + final InputStream limitingIn = new LimitingInputStream(socket.getInputStream(), 4096); + final BootstrapCodec codec = new BootstrapCodec(runner, limitingIn, socket.getOutputStream()); + codec.communicate(); + } catch (final Throwable t) { + System.out.println("Failed to communicate with NiFi Registry due to " + t); + t.printStackTrace(); + } finally { + try { + socket.close(); + } catch (final IOException ioe) { + } + } + } + }); + } catch (final Throwable t) { + System.err.println("Failed to receive information from NiFi Registry due to " + t); + t.printStackTrace(); + } + } + } + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/RunNiFiRegistry.java b/nifi-registry/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/RunNiFiRegistry.java new file mode 100644 index 0000000000..b73dcfcaf4 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/RunNiFiRegistry.java @@ -0,0 +1,1290 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.bootstrap; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.bootstrap.util.OSUtils; +import org.apache.nifi.registry.util.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.Reader; +import java.lang.reflect.Method; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.attribute.PosixFilePermission; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + *

+ * The class which bootstraps Apache NiFi Registry. This class looks for the + * bootstrap.conf file by looking in the following places (in order):

+ *
    + *
  1. Java System Property named + * {@code org.apache.nifi.registry.bootstrap.config.file}
  2. + *
  3. ${NIFI_HOME}/./conf/bootstrap.conf, where ${NIFI_REGISTRY_HOME} references an + * environment variable {@code NIFI_REGISTRY_HOME}
  4. + *
  5. ./conf/bootstrap.conf, where {@code ./} represents the working + * directory.
  6. + *
+ *

+ * If the {@code bootstrap.conf} file cannot be found, throws a {@code FileNotFoundException}. + */ +public class RunNiFiRegistry { + + public static final String DEFAULT_CONFIG_FILE = "./conf/bootstrap.conf"; + public static final String DEFAULT_JAVA_CMD = "java"; + public static final String DEFAULT_PID_DIR = "bin"; + public static final String DEFAULT_LOG_DIR = "./logs"; + public static final String DEFAULT_DOCS_DIR = "./docs"; + + public static final String GRACEFUL_SHUTDOWN_PROP = "graceful.shutdown.seconds"; + public static final String DEFAULT_GRACEFUL_SHUTDOWN_VALUE = "20"; + + public static final String NIFI_REGISTRY_PID_DIR_PROP = "org.apache.nifi.registry.bootstrap.config.pid.dir"; + public static final String NIFI_REGISTRY_PID_FILE_NAME = "nifi-registry.pid"; + public static final String NIFI_REGISTRY_STATUS_FILE_NAME = "nifi-registry.status"; + public static final String NIFI_REGISTRY_LOCK_FILE_NAME = "nifi-registry.lock"; + public static final String NIFI_REGISTRY_BOOTSTRAP_SENSITIVE_KEY = "nifi.registry.bootstrap.sensitive.key"; + + public static final String PID_KEY = "pid"; + + public static final int STARTUP_WAIT_SECONDS = 60; + + public static final String SHUTDOWN_CMD = "SHUTDOWN"; + public static final String PING_CMD = "PING"; + public static final String DUMP_CMD = "DUMP"; + + private static final int UNINITIALIZED_CC_PORT = -1; + + private volatile boolean autoRestartNiFiRegistry = true; + private volatile int ccPort = UNINITIALIZED_CC_PORT; + private volatile long nifiRegistryPid = -1L; + private volatile String secretKey; + private volatile ShutdownHook shutdownHook; + private volatile boolean nifiRegistryStarted; + + private final Lock startedLock = new ReentrantLock(); + private final Lock lock = new ReentrantLock(); + private final Condition startupCondition = lock.newCondition(); + + private final File bootstrapConfigFile; + + // used for logging initial info; these will be logged to console by default when the app is started + private final Logger cmdLogger = LoggerFactory.getLogger("org.apache.nifi.registry.bootstrap.Command"); + // used for logging all info. These by default will be written to the log file + private final Logger defaultLogger = LoggerFactory.getLogger(RunNiFiRegistry.class); + + + private final ExecutorService loggingExecutor; + private volatile Set> loggingFutures = new HashSet<>(2); + + public RunNiFiRegistry(final File bootstrapConfigFile, final boolean verbose) throws IOException { + this.bootstrapConfigFile = bootstrapConfigFile; + + loggingExecutor = Executors.newFixedThreadPool(2, new ThreadFactory() { + @Override + public Thread newThread(final Runnable runnable) { + final Thread t = Executors.defaultThreadFactory().newThread(runnable); + t.setDaemon(true); + t.setName("NiFi logging handler"); + return t; + } + }); + } + + private static void printUsage() { + System.out.println("Usage:"); + System.out.println(); + System.out.println("java org.apache.nifi.bootstrap.RunNiFiRegistry [<-verbose>] [options]"); + System.out.println(); + System.out.println("Valid commands include:"); + System.out.println(""); + System.out.println("Start : Start a new instance of Apache NiFi Registry"); + System.out.println("Stop : Stop a running instance of Apache NiFi Registry"); + System.out.println("Restart : Stop Apache NiFi Registry, if it is running, and then start a new instance"); + System.out.println("Status : Determine if there is a running instance of Apache NiFi Registry"); + System.out.println("Dump : Write a Thread Dump to the file specified by [options], or to the log if no file is given"); + System.out.println("Run : Start a new instance of Apache NiFi Registry and monitor the Process, restarting if the instance dies"); + System.out.println(); + } + + private static String[] shift(final String[] orig) { + return Arrays.copyOfRange(orig, 1, orig.length); + } + + public static void main(String[] args) throws IOException, InterruptedException { + if (args.length < 1 || args.length > 3) { + printUsage(); + return; + } + + File dumpFile = null; + boolean verbose = false; + if (args[0].equals("-verbose")) { + verbose = true; + args = shift(args); + } + + final String cmd = args[0]; + if (cmd.equals("dump")) { + if (args.length > 1) { + dumpFile = new File(args[1]); + } else { + dumpFile = null; + } + } + + switch (cmd.toLowerCase()) { + case "start": + case "run": + case "stop": + case "status": + case "dump": + case "restart": + case "env": + break; + default: + printUsage(); + return; + } + + final File configFile = getDefaultBootstrapConfFile(); + final RunNiFiRegistry runNiFiRegistry = new RunNiFiRegistry(configFile, verbose); + + Integer exitStatus = null; + switch (cmd.toLowerCase()) { + case "start": + runNiFiRegistry.start(); + break; + case "run": + runNiFiRegistry.start(); + break; + case "stop": + runNiFiRegistry.stop(); + break; + case "status": + exitStatus = runNiFiRegistry.status(); + break; + case "restart": + runNiFiRegistry.stop(); + runNiFiRegistry.start(); + break; + case "dump": + runNiFiRegistry.dump(dumpFile); + break; + case "env": + runNiFiRegistry.env(); + break; + } + if (exitStatus != null) { + System.exit(exitStatus); + } + } + + private static File getDefaultBootstrapConfFile() { + String configFilename = System.getProperty("org.apache.nifi.registry.bootstrap.config.file"); + + if (configFilename == null) { + final String nifiRegistryHome = System.getenv("NIFI_REGISTRY_HOME"); + if (nifiRegistryHome != null) { + final File nifiRegistryHomeFile = new File(nifiRegistryHome.trim()); + final File configFile = new File(nifiRegistryHomeFile, DEFAULT_CONFIG_FILE); + configFilename = configFile.getAbsolutePath(); + } + } + + if (configFilename == null) { + configFilename = DEFAULT_CONFIG_FILE; + } + + final File configFile = new File(configFilename); + return configFile; + } + + protected File getBootstrapFile(final Logger logger, String directory, String defaultDirectory, String fileName) throws IOException { + + final File confDir = bootstrapConfigFile.getParentFile(); + final File nifiHome = confDir.getParentFile(); + + String confFileDir = System.getProperty(directory); + + final File fileDir; + + if (confFileDir != null) { + fileDir = new File(confFileDir.trim()); + } else { + fileDir = new File(nifiHome, defaultDirectory); + } + + FileUtils.ensureDirectoryExistAndCanAccess(fileDir); + final File statusFile = new File(fileDir, fileName); + logger.debug("Status File: {}", statusFile); + return statusFile; + } + + protected File getPidFile(final Logger logger) throws IOException { + return getBootstrapFile(logger, NIFI_REGISTRY_PID_DIR_PROP, DEFAULT_PID_DIR, NIFI_REGISTRY_PID_FILE_NAME); + } + + protected File getStatusFile(final Logger logger) throws IOException { + return getBootstrapFile(logger, NIFI_REGISTRY_PID_DIR_PROP, DEFAULT_PID_DIR, NIFI_REGISTRY_STATUS_FILE_NAME); + } + + protected File getLockFile(final Logger logger) throws IOException { + return getBootstrapFile(logger, NIFI_REGISTRY_PID_DIR_PROP, DEFAULT_PID_DIR, NIFI_REGISTRY_LOCK_FILE_NAME); + } + + protected File getStatusFile() throws IOException { + return getStatusFile(defaultLogger); + } + + private Properties loadProperties(final Logger logger) throws IOException { + final Properties props = new Properties(); + final File statusFile = getStatusFile(logger); + if (statusFile == null || !statusFile.exists()) { + logger.debug("No status file to load properties from"); + return props; + } + + try (final FileInputStream fis = new FileInputStream(getStatusFile(logger))) { + props.load(fis); + } + + final Map modified = new HashMap<>(props); + modified.remove("secret.key"); + logger.debug("Properties: {}", modified); + + return props; + } + + private synchronized void savePidProperties(final Properties pidProperties, final Logger logger) throws IOException { + final String pid = pidProperties.getProperty(PID_KEY); + if (!StringUtils.isBlank(pid)) { + writePidFile(pid, logger); + } + + final File statusFile = getStatusFile(logger); + if (statusFile.exists() && !statusFile.delete()) { + logger.warn("Failed to delete {}", statusFile); + } + + if (!statusFile.createNewFile()) { + throw new IOException("Failed to create file " + statusFile); + } + + try { + final Set perms = new HashSet<>(); + perms.add(PosixFilePermission.OWNER_READ); + perms.add(PosixFilePermission.OWNER_WRITE); + Files.setPosixFilePermissions(statusFile.toPath(), perms); + } catch (final Exception e) { + logger.warn("Failed to set permissions so that only the owner can read status file {}; " + + "this may allows others to have access to the key needed to communicate with NiFi Registry. " + + "Permissions should be changed so that only the owner can read this file", statusFile); + } + + try (final FileOutputStream fos = new FileOutputStream(statusFile)) { + pidProperties.store(fos, null); + fos.getFD().sync(); + } + + logger.debug("Saved Properties {} to {}", new Object[]{pidProperties, statusFile}); + } + + private synchronized void writePidFile(final String pid, final Logger logger) throws IOException { + final File pidFile = getPidFile(logger); + if (pidFile.exists() && !pidFile.delete()) { + logger.warn("Failed to delete {}", pidFile); + } + + if (!pidFile.createNewFile()) { + throw new IOException("Failed to create file " + pidFile); + } + + try { + final Set perms = new HashSet<>(); + perms.add(PosixFilePermission.OWNER_WRITE); + perms.add(PosixFilePermission.OWNER_READ); + perms.add(PosixFilePermission.GROUP_READ); + perms.add(PosixFilePermission.OTHERS_READ); + Files.setPosixFilePermissions(pidFile.toPath(), perms); + } catch (final Exception e) { + logger.warn("Failed to set permissions so that only the owner can read pid file {}; " + + "this may allows others to have access to the key needed to communicate with NiFi Registry. " + + "Permissions should be changed so that only the owner can read this file", pidFile); + } + + try (final FileOutputStream fos = new FileOutputStream(pidFile)) { + fos.write(pid.getBytes(StandardCharsets.UTF_8)); + fos.getFD().sync(); + } + + logger.debug("Saved Pid {} to {}", new Object[]{pid, pidFile}); + } + + private boolean isPingSuccessful(final int port, final String secretKey, final Logger logger) { + logger.debug("Pinging {}", port); + + try (final Socket socket = new Socket("localhost", port)) { + final OutputStream out = socket.getOutputStream(); + out.write((PING_CMD + " " + secretKey + "\n").getBytes(StandardCharsets.UTF_8)); + out.flush(); + + logger.debug("Sent PING command"); + socket.setSoTimeout(5000); + final InputStream in = socket.getInputStream(); + final BufferedReader reader = new BufferedReader(new InputStreamReader(in)); + final String response = reader.readLine(); + logger.debug("PING response: {}", response); + out.close(); + reader.close(); + + return PING_CMD.equals(response); + } catch (final IOException ioe) { + return false; + } + } + + private Integer getCurrentPort(final Logger logger) throws IOException { + final Properties props = loadProperties(logger); + final String portVal = props.getProperty("port"); + if (portVal == null) { + logger.debug("No Port found in status file"); + return null; + } else { + logger.debug("Port defined in status file: {}", portVal); + } + + final int port = Integer.parseInt(portVal); + final boolean success = isPingSuccessful(port, props.getProperty("secret.key"), logger); + if (success) { + logger.debug("Successful PING on port {}", port); + return port; + } + + final String pid = props.getProperty(PID_KEY); + logger.debug("PID in status file is {}", pid); + if (pid != null) { + final boolean procRunning = isProcessRunning(pid, logger); + if (procRunning) { + return port; + } else { + return null; + } + } + + return null; + } + + private boolean isProcessRunning(final String pid, final Logger logger) { + try { + // We use the "ps" command to check if the process is still running. + final ProcessBuilder builder = new ProcessBuilder(); + + builder.command("ps", "-p", pid); + final Process proc = builder.start(); + + // Look for the pid in the output of the 'ps' command. + boolean running = false; + String line; + try (final InputStream in = proc.getInputStream(); + final Reader streamReader = new InputStreamReader(in); + final BufferedReader reader = new BufferedReader(streamReader)) { + + while ((line = reader.readLine()) != null) { + if (line.trim().startsWith(pid)) { + running = true; + } + } + } + + // If output of the ps command had our PID, the process is running. + if (running) { + logger.debug("Process with PID {} is running", pid); + } else { + logger.debug("Process with PID {} is not running", pid); + } + + return running; + } catch (final IOException ioe) { + System.err.println("Failed to determine if Process " + pid + " is running; assuming that it is not"); + return false; + } + } + + private Status getStatus(final Logger logger) { + final Properties props; + try { + props = loadProperties(logger); + } catch (final IOException ioe) { + return new Status(null, null, false, false); + } + + if (props == null) { + return new Status(null, null, false, false); + } + + final String portValue = props.getProperty("port"); + final String pid = props.getProperty(PID_KEY); + final String secretKey = props.getProperty("secret.key"); + + if (portValue == null && pid == null) { + return new Status(null, null, false, false); + } + + Integer port = null; + boolean pingSuccess = false; + if (portValue != null) { + try { + port = Integer.parseInt(portValue); + pingSuccess = isPingSuccessful(port, secretKey, logger); + } catch (final NumberFormatException nfe) { + return new Status(null, null, false, false); + } + } + + if (pingSuccess) { + return new Status(port, pid, true, true); + } + + final boolean alive = pid != null && isProcessRunning(pid, logger); + return new Status(port, pid, pingSuccess, alive); + } + + public int status() throws IOException { + final Logger logger = cmdLogger; + final Status status = getStatus(logger); + if (status.isRespondingToPing()) { + logger.info("Apache NiFi Registry is currently running, listening to Bootstrap on port {}, PID={}", + new Object[]{status.getPort(), status.getPid() == null ? "unknown" : status.getPid()}); + return 0; + } + + if (status.isProcessRunning()) { + logger.info("Apache NiFi Registry is running at PID {} but is not responding to ping requests", status.getPid()); + return 4; + } + + if (status.getPort() == null) { + logger.info("Apache NiFi Registry is not running"); + return 3; + } + + if (status.getPid() == null) { + logger.info("Apache NiFi Registry is not responding to Ping requests. The process may have died or may be hung"); + } else { + logger.info("Apache NiFi Registry is not running"); + } + return 3; + } + + public void env() { + final Logger logger = cmdLogger; + final Status status = getStatus(logger); + if (status.getPid() == null) { + logger.info("Apache NiFi Registry is not running"); + return; + } + final Class virtualMachineClass; + try { + virtualMachineClass = Class.forName("com.sun.tools.attach.VirtualMachine"); + } catch (final ClassNotFoundException cnfe) { + logger.error("Seems tools.jar (Linux / Windows JDK) or classes.jar (Mac OS) is not available in classpath"); + return; + } + final Method attachMethod; + final Method detachMethod; + + try { + attachMethod = virtualMachineClass.getMethod("attach", String.class); + detachMethod = virtualMachineClass.getDeclaredMethod("detach"); + } catch (final Exception e) { + logger.error("Methods required for getting environment not available", e); + return; + } + + final Object virtualMachine; + try { + virtualMachine = attachMethod.invoke(null, status.getPid()); + } catch (final Throwable t) { + logger.error("Problem attaching to NiFi", t); + return; + } + + try { + final Method getSystemPropertiesMethod = virtualMachine.getClass().getMethod("getSystemProperties"); + + final Properties sysProps = (Properties) getSystemPropertiesMethod.invoke(virtualMachine); + for (Entry syspropEntry : sysProps.entrySet()) { + logger.info(syspropEntry.getKey().toString() + " = " + syspropEntry.getValue().toString()); + } + } catch (Throwable t) { + throw new RuntimeException(t); + } finally { + try { + detachMethod.invoke(virtualMachine); + } catch (final Exception e) { + logger.warn("Caught exception detaching from process", e); + } + } + } + + /** + * Writes a NiFi thread dump to the given file; if file is null, logs at + * INFO level instead. + * + * @param dumpFile the file to write the dump content to + * @throws IOException if any issues occur while writing the dump file + */ + public void dump(final File dumpFile) throws IOException { + final Logger logger = defaultLogger; // dump to bootstrap log file by default + final Integer port = getCurrentPort(logger); + if (port == null) { + logger.info("Apache NiFi Registry is not currently running"); + return; + } + + final Properties nifiRegistryProps = loadProperties(logger); + final String secretKey = nifiRegistryProps.getProperty("secret.key"); + + final StringBuilder sb = new StringBuilder(); + try (final Socket socket = new Socket()) { + logger.debug("Connecting to NiFi Registry instance"); + socket.setSoTimeout(60000); + socket.connect(new InetSocketAddress("localhost", port)); + logger.debug("Established connection to NiFi Registry instance."); + socket.setSoTimeout(60000); + + logger.debug("Sending DUMP Command to port {}", port); + final OutputStream out = socket.getOutputStream(); + out.write((DUMP_CMD + " " + secretKey + "\n").getBytes(StandardCharsets.UTF_8)); + out.flush(); + + final InputStream in = socket.getInputStream(); + try (final BufferedReader reader = new BufferedReader(new InputStreamReader(in))) { + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + } + } + + final String dump = sb.toString(); + if (dumpFile == null) { + logger.info(dump); + } else { + try (final FileOutputStream fos = new FileOutputStream(dumpFile)) { + fos.write(dump.getBytes(StandardCharsets.UTF_8)); + } + // we want to log to the console (by default) that we wrote the thread dump to the specified file + cmdLogger.info("Successfully wrote thread dump to {}", dumpFile.getAbsolutePath()); + } + } + + public void notifyStop() { + final String hostname = getHostname(); + final SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS"); + final String now = sdf.format(System.currentTimeMillis()); + String user = System.getProperty("user.name"); + if (user == null || user.trim().isEmpty()) { + user = "Unknown User"; + } + } + + public void stop() throws IOException { + final Logger logger = cmdLogger; + final Integer port = getCurrentPort(logger); + if (port == null) { + logger.info("Apache NiFi Registry is not currently running"); + return; + } + + // indicate that a stop command is in progress + final File lockFile = getLockFile(logger); + if (!lockFile.exists()) { + lockFile.createNewFile(); + } + + final Properties nifiRegistryProps = loadProperties(logger); + final String secretKey = nifiRegistryProps.getProperty("secret.key"); + final String pid = nifiRegistryProps.getProperty(PID_KEY); + final File statusFile = getStatusFile(logger); + final File pidFile = getPidFile(logger); + + try (final Socket socket = new Socket()) { + logger.debug("Connecting to NiFi Registry instance"); + socket.setSoTimeout(10000); + socket.connect(new InetSocketAddress("localhost", port)); + logger.debug("Established connection to NiFi Registry instance."); + socket.setSoTimeout(10000); + + logger.debug("Sending SHUTDOWN Command to port {}", port); + final OutputStream out = socket.getOutputStream(); + out.write((SHUTDOWN_CMD + " " + secretKey + "\n").getBytes(StandardCharsets.UTF_8)); + out.flush(); + socket.shutdownOutput(); + + final InputStream in = socket.getInputStream(); + int lastChar; + final StringBuilder sb = new StringBuilder(); + while ((lastChar = in.read()) > -1) { + sb.append((char) lastChar); + } + final String response = sb.toString().trim(); + + logger.debug("Received response to SHUTDOWN command: {}", response); + + if (SHUTDOWN_CMD.equals(response)) { + logger.info("Apache NiFi Registry has accepted the Shutdown Command and is shutting down now"); + + if (pid != null) { + final Properties bootstrapProperties = new Properties(); + try (final FileInputStream fis = new FileInputStream(bootstrapConfigFile)) { + bootstrapProperties.load(fis); + } + + String gracefulShutdown = bootstrapProperties.getProperty(GRACEFUL_SHUTDOWN_PROP, DEFAULT_GRACEFUL_SHUTDOWN_VALUE); + int gracefulShutdownSeconds; + try { + gracefulShutdownSeconds = Integer.parseInt(gracefulShutdown); + } catch (final NumberFormatException nfe) { + gracefulShutdownSeconds = Integer.parseInt(DEFAULT_GRACEFUL_SHUTDOWN_VALUE); + } + + notifyStop(); + final long startWait = System.nanoTime(); + while (isProcessRunning(pid, logger)) { + logger.info("Waiting for Apache NiFi Registry to finish shutting down..."); + final long waitNanos = System.nanoTime() - startWait; + final long waitSeconds = TimeUnit.NANOSECONDS.toSeconds(waitNanos); + if (waitSeconds >= gracefulShutdownSeconds && gracefulShutdownSeconds > 0) { + if (isProcessRunning(pid, logger)) { + logger.warn("NiFi Registry has not finished shutting down after {} seconds. Killing process.", gracefulShutdownSeconds); + try { + killProcessTree(pid, logger); + } catch (final IOException ioe) { + logger.error("Failed to kill Process with PID {}", pid); + } + } + break; + } else { + try { + Thread.sleep(2000L); + } catch (final InterruptedException ie) { + } + } + } + + if (statusFile.exists() && !statusFile.delete()) { + logger.error("Failed to delete status file {}; this file should be cleaned up manually", statusFile); + } + + if (pidFile.exists() && !pidFile.delete()) { + logger.error("Failed to delete pid file {}; this file should be cleaned up manually", pidFile); + } + + logger.info("NiFi Registry has finished shutting down."); + } + } else { + logger.error("When sending SHUTDOWN command to NiFi Registry , got unexpected response {}", response); + } + } catch (final IOException ioe) { + if (pid == null) { + logger.error("Failed to send shutdown command to port {} due to {}. No PID found for the NiFi Registry process, so unable to kill process; " + + "the process should be killed manually.", new Object[]{port, ioe.toString()}); + } else { + logger.error("Failed to send shutdown command to port {} due to {}. Will kill the NiFi Registry Process with PID {}.", port, ioe.toString(), pid); + notifyStop(); + killProcessTree(pid, logger); + if (statusFile.exists() && !statusFile.delete()) { + logger.error("Failed to delete status file {}; this file should be cleaned up manually", statusFile); + } + } + } finally { + if (lockFile.exists() && !lockFile.delete()) { + logger.error("Failed to delete lock file {}; this file should be cleaned up manually", lockFile); + } + } + } + + private static List getChildProcesses(final String ppid) throws IOException { + final Process proc = Runtime.getRuntime().exec(new String[]{"ps", "-o", "pid", "--no-headers", "--ppid", ppid}); + final List childPids = new ArrayList<>(); + try (final InputStream in = proc.getInputStream(); + final BufferedReader reader = new BufferedReader(new InputStreamReader(in))) { + + String line; + while ((line = reader.readLine()) != null) { + childPids.add(line.trim()); + } + } + + return childPids; + } + + private void killProcessTree(final String pid, final Logger logger) throws IOException { + logger.debug("Killing Process Tree for PID {}", pid); + + final List children = getChildProcesses(pid); + logger.debug("Children of PID {}: {}", new Object[]{pid, children}); + + for (final String childPid : children) { + killProcessTree(childPid, logger); + } + + Runtime.getRuntime().exec(new String[]{"kill", "-9", pid}); + } + + public static boolean isAlive(final Process process) { + try { + process.exitValue(); + return false; + } catch (final IllegalStateException | IllegalThreadStateException itse) { + return true; + } + } + + private String getHostname() { + String hostname = "Unknown Host"; + String ip = "Unknown IP Address"; + try { + final InetAddress localhost = InetAddress.getLocalHost(); + hostname = localhost.getHostName(); + ip = localhost.getHostAddress(); + } catch (final Exception e) { + defaultLogger.warn("Failed to obtain hostname for notification due to:", e); + } + + return hostname + " (" + ip + ")"; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public void start() throws IOException, InterruptedException { + final Integer port = getCurrentPort(cmdLogger); + if (port != null) { + cmdLogger.info("Apache NiFi Registry is already running, listening to Bootstrap on port " + port); + return; + } + + final File prevLockFile = getLockFile(cmdLogger); + if (prevLockFile.exists() && !prevLockFile.delete()) { + cmdLogger.warn("Failed to delete previous lock file {}; this file should be cleaned up manually", prevLockFile); + } + + final ProcessBuilder builder = new ProcessBuilder(); + + if (!bootstrapConfigFile.exists()) { + throw new FileNotFoundException(bootstrapConfigFile.getAbsolutePath()); + } + + final Properties properties = new Properties(); + try (final FileInputStream fis = new FileInputStream(bootstrapConfigFile)) { + properties.load(fis); + } + + final Map props = new HashMap<>(); + props.putAll((Map) properties); + + // Determine the working dir for launching the NiFi Registry process + // The order of precedence is: + // 1) Specified in bootstrap.conf via working.dir + // 2) NIFI_REGISTRY_HOME env variable + // 3) Parent of bootstrap config file's parent + + final File workingDir; + final String specifiedWorkingDir = props.get("working.dir"); + final String nifiRegistryHome = System.getenv("NIFI_REGISTRY_HOME"); + final File bootstrapConfigAbsoluteFile = bootstrapConfigFile.getAbsoluteFile(); + + if (!StringUtils.isBlank(specifiedWorkingDir)) { + workingDir = new File(specifiedWorkingDir.trim()); + } else if (!StringUtils.isBlank(nifiRegistryHome)) { + workingDir = new File(nifiRegistryHome.trim()); + } else { + final File binDir = bootstrapConfigAbsoluteFile.getParentFile(); + workingDir = binDir.getParentFile(); + } + + builder.directory(workingDir); + + final String nifiRegistryLogDir = replaceNull(System.getProperty("org.apache.nifi.registry.bootstrap.config.log.dir"), DEFAULT_LOG_DIR).trim(); + + final String nifiRegistryDocsDir = replaceNull(props.get("docs.dir"), DEFAULT_DOCS_DIR).trim(); + + final String libFilename = replaceNull(props.get("lib.dir"), "./lib").trim(); + File libDir = getFile(libFilename, workingDir); + File libSharedDir = getFile(libFilename + "/shared", workingDir); + + final String confFilename = replaceNull(props.get("conf.dir"), "./conf").trim(); + File confDir = getFile(confFilename, workingDir); + + String nifiRegistryPropsFilename = props.get("props.file"); + if (nifiRegistryPropsFilename == null) { + if (confDir.exists()) { + nifiRegistryPropsFilename = new File(confDir, "nifi-registry.properties").getAbsolutePath(); + } else { + nifiRegistryPropsFilename = DEFAULT_CONFIG_FILE; + } + } + + nifiRegistryPropsFilename = nifiRegistryPropsFilename.trim(); + + final List javaAdditionalArgs = new ArrayList<>(); + for (final Map.Entry entry : props.entrySet()) { + final String key = entry.getKey(); + final String value = entry.getValue(); + + if (key.startsWith("java.arg")) { + javaAdditionalArgs.add(value); + } + } + + final File[] libSharedFiles = libSharedDir.listFiles(new FilenameFilter() { + @Override + public boolean accept(final File dir, final String filename) { + return filename.toLowerCase().endsWith(".jar"); + } + }); + + if (libSharedFiles == null || libSharedFiles.length == 0) { + throw new RuntimeException("Could not find lib shared directory at " + libSharedDir.getAbsolutePath()); + } + + final File[] libFiles = libDir.listFiles(new FilenameFilter() { + @Override + public boolean accept(final File dir, final String filename) { + return filename.toLowerCase().endsWith(".jar"); + } + }); + + if (libFiles == null || libFiles.length == 0) { + throw new RuntimeException("Could not find lib directory at " + libDir.getAbsolutePath()); + } + + final File[] confFiles = confDir.listFiles(); + if (confFiles == null || confFiles.length == 0) { + throw new RuntimeException("Could not find conf directory at " + confDir.getAbsolutePath()); + } + + final List cpFiles = new ArrayList<>(confFiles.length + libFiles.length + libSharedFiles.length); + cpFiles.add(confDir.getAbsolutePath()); + for (final File file : libSharedFiles) { + cpFiles.add(file.getAbsolutePath()); + } + for (final File file : libFiles) { + cpFiles.add(file.getAbsolutePath()); + } + + final String runtimeJavaVersion = System.getProperty("java.version"); + defaultLogger.info("Runtime Java version: {}", runtimeJavaVersion); + if (Integer.parseInt(runtimeJavaVersion.substring(0, runtimeJavaVersion.indexOf('.'))) >= 11) { + // If running on Java 11 or greater, add lib/java11 to the classpath. + // TODO: Once the minimum Java version requirement of NiFi Registry is 11, this processing should be removed. + final String libJava11Filename = replaceNull(props.get("lib.dir"), "./lib").trim() + "/java11"; + final File libJava11Dir = getFile(libJava11Filename, workingDir); + if (libJava11Dir.exists()) { + for (final File file : Objects.requireNonNull(libJava11Dir.listFiles((dir, filename) -> filename.toLowerCase().endsWith(".jar")))) { + cpFiles.add(file.getAbsolutePath()); + } + } + } + + final StringBuilder classPathBuilder = new StringBuilder(); + for (int i = 0; i < cpFiles.size(); i++) { + final String filename = cpFiles.get(i); + classPathBuilder.append(filename); + if (i < cpFiles.size() - 1) { + classPathBuilder.append(File.pathSeparatorChar); + } + } + + final String classPath = classPathBuilder.toString(); + String javaCmd = props.get("java"); + if (javaCmd == null) { + javaCmd = DEFAULT_JAVA_CMD; + } + if (javaCmd.equals(DEFAULT_JAVA_CMD)) { + String javaHome = System.getenv("JAVA_HOME"); + if (javaHome != null) { + String fileExtension = isWindows() ? ".exe" : ""; + File javaFile = new File(javaHome + File.separatorChar + "bin" + + File.separatorChar + "java" + fileExtension); + if (javaFile.exists() && javaFile.canExecute()) { + javaCmd = javaFile.getAbsolutePath(); + } + } + } + + final NiFiRegistryListener listener = new NiFiRegistryListener(); + final int listenPort = listener.start(this); + + final List cmd = new ArrayList<>(); + + cmd.add(javaCmd); + cmd.add("-classpath"); + cmd.add(classPath); + cmd.addAll(javaAdditionalArgs); + cmd.add("-Dnifi.registry.properties.file.path=" + nifiRegistryPropsFilename); + cmd.add("-Dnifi.registry.bootstrap.config.file.path=" + bootstrapConfigFile.getAbsolutePath()); + cmd.add("-Dnifi.registry.bootstrap.listen.port=" + listenPort); + cmd.add("-Dnifi.registry.bootstrap.config.docs.dir=" + nifiRegistryDocsDir); + cmd.add("-Dapp=NiFiRegistry"); + cmd.add("-Dorg.apache.nifi.registry.bootstrap.config.log.dir=" + nifiRegistryLogDir); + + if (runtimeJavaVersion.startsWith("9") || runtimeJavaVersion.startsWith("10")) { + // running on Java 9+, java.xml.bind module must be made available + // running on Java 9 or 10, internal module java.xml.bind module must be made available + cmd.add("--add-modules=java.xml.bind"); + } + + cmd.add("org.apache.nifi.registry.NiFiRegistry"); + + builder.command(cmd); + + final StringBuilder cmdBuilder = new StringBuilder(); + for (final String s : cmd) { + cmdBuilder.append(s).append(" "); + } + + cmdLogger.info("Starting Apache NiFi Registry..."); + cmdLogger.info("Working Directory: {}", workingDir.getAbsolutePath()); + cmdLogger.info("Command: {}", cmdBuilder.toString()); + + String gracefulShutdown = props.get(GRACEFUL_SHUTDOWN_PROP); + if (gracefulShutdown == null) { + gracefulShutdown = DEFAULT_GRACEFUL_SHUTDOWN_VALUE; + } + + final int gracefulShutdownSeconds; + try { + gracefulShutdownSeconds = Integer.parseInt(gracefulShutdown); + } catch (final NumberFormatException nfe) { + throw new NumberFormatException("The '" + GRACEFUL_SHUTDOWN_PROP + "' property in Bootstrap Config File " + + bootstrapConfigAbsoluteFile.getAbsolutePath() + " has an invalid value. Must be a non-negative integer"); + } + + if (gracefulShutdownSeconds < 0) { + throw new NumberFormatException("The '" + GRACEFUL_SHUTDOWN_PROP + "' property in Bootstrap Config File " + + bootstrapConfigAbsoluteFile.getAbsolutePath() + " has an invalid value. Must be a non-negative integer"); + } + + Process process = builder.start(); + handleLogging(process); + Long pid = OSUtils.getProcessId(process, cmdLogger); + if (pid == null) { + cmdLogger.warn("Launched Apache NiFi Registry but could not determined the Process ID"); + } else { + nifiRegistryPid = pid; + final Properties pidProperties = new Properties(); + pidProperties.setProperty(PID_KEY, String.valueOf(nifiRegistryPid)); + savePidProperties(pidProperties, cmdLogger); + cmdLogger.info("Launched Apache NiFi Registry with Process ID " + pid); + } + + shutdownHook = new ShutdownHook(process, this, secretKey, gracefulShutdownSeconds, loggingExecutor); + final Runtime runtime = Runtime.getRuntime(); + runtime.addShutdownHook(shutdownHook); + + final String hostname = getHostname(); + final SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS"); + String now = sdf.format(System.currentTimeMillis()); + String user = System.getProperty("user.name"); + if (user == null || user.trim().isEmpty()) { + user = "Unknown User"; + } + + while (true) { + final boolean alive = isAlive(process); + + if (alive) { + try { + Thread.sleep(1000L); + } catch (final InterruptedException ie) { + } + } else { + try { + runtime.removeShutdownHook(shutdownHook); + } catch (final IllegalStateException ise) { + // happens when already shutting down + } + + now = sdf.format(System.currentTimeMillis()); + if (autoRestartNiFiRegistry) { + final File statusFile = getStatusFile(defaultLogger); + if (!statusFile.exists()) { + defaultLogger.info("Status File no longer exists. Will not restart NiFi Registry "); + return; + } + + final File lockFile = getLockFile(defaultLogger); + if (lockFile.exists()) { + defaultLogger.info("A shutdown was initiated. Will not restart NiFi Registry "); + return; + } + + final boolean previouslyStarted = getNifiRegistryStarted(); + if (!previouslyStarted) { + defaultLogger.info("NiFi Registry never started. Will not restart NiFi Registry "); + return; + } else { + setNiFiRegistryStarted(false); + } + + defaultLogger.warn("Apache NiFi Registry appears to have died. Restarting..."); + secretKey = null; + process = builder.start(); + handleLogging(process); + + pid = OSUtils.getProcessId(process, defaultLogger); + if (pid == null) { + cmdLogger.warn("Launched Apache NiFi Registry but could not obtain the Process ID"); + } else { + nifiRegistryPid = pid; + final Properties pidProperties = new Properties(); + pidProperties.setProperty(PID_KEY, String.valueOf(nifiRegistryPid)); + savePidProperties(pidProperties, defaultLogger); + cmdLogger.info("Launched Apache NiFi Registry with Process ID " + pid); + } + + shutdownHook = new ShutdownHook(process, this, secretKey, gracefulShutdownSeconds, loggingExecutor); + runtime.addShutdownHook(shutdownHook); + + final boolean started = waitForStart(); + + if (started) { + defaultLogger.info("Successfully started Apache NiFi Registry {}", (pid == null ? "" : " with PID " + pid)); + } else { + defaultLogger.error("Apache NiFi Registry does not appear to have started"); + } + } else { + return; + } + } + } + } + + private void handleLogging(final Process process) { + final Set> existingFutures = loggingFutures; + if (existingFutures != null) { + for (final Future future : existingFutures) { + future.cancel(false); + } + } + + final Future stdOutFuture = loggingExecutor.submit(new Runnable() { + @Override + public void run() { + final Logger stdOutLogger = LoggerFactory.getLogger("org.apache.nifi.registry.StdOut"); + final InputStream in = process.getInputStream(); + try (final BufferedReader reader = new BufferedReader(new InputStreamReader(in))) { + String line; + while ((line = reader.readLine()) != null) { + stdOutLogger.info(line); + } + } catch (IOException e) { + defaultLogger.error("Failed to read from NiFi Registry's Standard Out stream", e); + } + } + }); + + final Future stdErrFuture = loggingExecutor.submit(new Runnable() { + @Override + public void run() { + final Logger stdErrLogger = LoggerFactory.getLogger("org.apache.nifi.registry.StdErr"); + final InputStream in = process.getErrorStream(); + try (final BufferedReader reader = new BufferedReader(new InputStreamReader(in))) { + String line; + while ((line = reader.readLine()) != null) { + stdErrLogger.error(line); + } + } catch (IOException e) { + defaultLogger.error("Failed to read from NiFi Registry's Standard Error stream", e); + } + } + }); + + final Set> futures = new HashSet<>(); + futures.add(stdOutFuture); + futures.add(stdErrFuture); + this.loggingFutures = futures; + } + + + private boolean isWindows() { + final String osName = System.getProperty("os.name"); + return osName != null && osName.toLowerCase().contains("win"); + } + + private boolean waitForStart() { + lock.lock(); + try { + final long startTime = System.nanoTime(); + + while (ccPort < 1) { + try { + startupCondition.await(1, TimeUnit.SECONDS); + } catch (final InterruptedException ie) { + return false; + } + + final long waitNanos = System.nanoTime() - startTime; + final long waitSeconds = TimeUnit.NANOSECONDS.toSeconds(waitNanos); + if (waitSeconds > STARTUP_WAIT_SECONDS) { + return false; + } + } + } finally { + lock.unlock(); + } + return true; + } + + private File getFile(final String filename, final File workingDir) { + File file = new File(filename); + if (!file.isAbsolute()) { + file = new File(workingDir, filename); + } + + return file; + } + + private String replaceNull(final String value, final String replacement) { + return (value == null) ? replacement : value; + } + + void setAutoRestartNiFiRegistry(final boolean restart) { + this.autoRestartNiFiRegistry = restart; + } + + void setNiFiRegistryCommandControlPort(final int port, final String secretKey) throws IOException { + + if (this.secretKey != null && this.ccPort != UNINITIALIZED_CC_PORT) { + defaultLogger.warn("Blocking attempt to change NiFi Registry command port and secret after they have already been initialized. requestedPort={}", port); + return; + } + + this.ccPort = port; + this.secretKey = secretKey; + + if (shutdownHook != null) { + shutdownHook.setSecretKey(secretKey); + } + + final File statusFile = getStatusFile(defaultLogger); + + final Properties nifiProps = new Properties(); + if (nifiRegistryPid != -1) { + nifiProps.setProperty(PID_KEY, String.valueOf(nifiRegistryPid)); + } + nifiProps.setProperty("port", String.valueOf(ccPort)); + nifiProps.setProperty("secret.key", secretKey); + + try { + savePidProperties(nifiProps, defaultLogger); + } catch (final IOException ioe) { + defaultLogger.warn("Apache NiFi Registry has started but failed to persist NiFi Registry Port information to {} due to {}", new Object[]{statusFile.getAbsolutePath(), ioe}); + } + + defaultLogger.info("Apache NiFi Registry now running and listening for Bootstrap requests on port {}", port); + } + + int getNiFiRegistryCommandControlPort() { + return this.ccPort; + } + + void setNiFiRegistryStarted(final boolean nifiStarted) { + startedLock.lock(); + try { + this.nifiRegistryStarted = nifiStarted; + } finally { + startedLock.unlock(); + } + } + + boolean getNifiRegistryStarted() { + startedLock.lock(); + try { + return nifiRegistryStarted; + } finally { + startedLock.unlock(); + } + } + + private static class Status { + + private final Integer port; + private final String pid; + + private final Boolean respondingToPing; + private final Boolean processRunning; + + public Status(final Integer port, final String pid, final Boolean respondingToPing, final Boolean processRunning) { + this.port = port; + this.pid = pid; + this.respondingToPing = respondingToPing; + this.processRunning = processRunning; + } + + public String getPid() { + return pid; + } + + public Integer getPort() { + return port; + } + + public boolean isRespondingToPing() { + return Boolean.TRUE.equals(respondingToPing); + } + + public boolean isProcessRunning() { + return Boolean.TRUE.equals(processRunning); + } + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/ShutdownHook.java b/nifi-registry/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/ShutdownHook.java new file mode 100644 index 0000000000..ba370e6578 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/ShutdownHook.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.bootstrap; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; + +public class ShutdownHook extends Thread { + + private final Process nifiRegistryProcess; + private final RunNiFiRegistry runner; + private final int gracefulShutdownSeconds; + private final ExecutorService executor; + + private volatile String secretKey; + + public ShutdownHook(final Process nifiRegistryProcess, final RunNiFiRegistry runner, final String secretKey, final int gracefulShutdownSeconds, final ExecutorService executor) { + this.nifiRegistryProcess = nifiRegistryProcess; + this.runner = runner; + this.secretKey = secretKey; + this.gracefulShutdownSeconds = gracefulShutdownSeconds; + this.executor = executor; + } + + void setSecretKey(final String secretKey) { + this.secretKey = secretKey; + } + + @Override + public void run() { + executor.shutdown(); + runner.setAutoRestartNiFiRegistry(false); + final int ccPort = runner.getNiFiRegistryCommandControlPort(); + if (ccPort > 0) { + System.out.println("Initiating Shutdown of NiFi Registry..."); + + try { + final Socket socket = new Socket("localhost", ccPort); + final OutputStream out = socket.getOutputStream(); + out.write(("SHUTDOWN " + secretKey + "\n").getBytes(StandardCharsets.UTF_8)); + out.flush(); + + socket.close(); + } catch (final IOException ioe) { + System.out.println("Failed to Shutdown NiFi Registry due to " + ioe); + } + } + + runner.notifyStop(); + System.out.println("Waiting for Apache NiFi Registry to finish shutting down..."); + final long startWait = System.nanoTime(); + while (RunNiFiRegistry.isAlive(nifiRegistryProcess)) { + final long waitNanos = System.nanoTime() - startWait; + final long waitSeconds = TimeUnit.NANOSECONDS.toSeconds(waitNanos); + if (waitSeconds >= gracefulShutdownSeconds && gracefulShutdownSeconds > 0) { + if (RunNiFiRegistry.isAlive(nifiRegistryProcess)) { + System.out.println("NiFi Registry has not finished shutting down after " + gracefulShutdownSeconds + " seconds. Killing process."); + nifiRegistryProcess.destroy(); + } + break; + } else { + try { + Thread.sleep(1000L); + } catch (final InterruptedException ie) { + } + } + } + + try { + final File statusFile = runner.getStatusFile(); + if (!statusFile.delete()) { + System.err.println("Failed to delete status file " + statusFile.getAbsolutePath() + "; this file should be cleaned up manually"); + } + }catch (IOException ex){ + System.err.println("Failed to retrieve status file " + ex); + } + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/exception/InvalidCommandException.java b/nifi-registry/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/exception/InvalidCommandException.java new file mode 100644 index 0000000000..6c51c08aba --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/exception/InvalidCommandException.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.bootstrap.exception; + +public class InvalidCommandException extends Exception { + + private static final long serialVersionUID = 1L; + + public InvalidCommandException() { + super(); + } + + public InvalidCommandException(final String message) { + super(message); + } + + public InvalidCommandException(final Throwable t) { + super(t); + } + + public InvalidCommandException(final String message, final Throwable t) { + super(message, t); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/util/LimitingInputStream.java b/nifi-registry/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/util/LimitingInputStream.java new file mode 100644 index 0000000000..79af09e63b --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/util/LimitingInputStream.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.bootstrap.util; + +import java.io.IOException; +import java.io.InputStream; + +public class LimitingInputStream extends InputStream { + + private final InputStream in; + private final long limit; + private long bytesRead = 0; + + public LimitingInputStream(final InputStream in, final long limit) { + this.in = in; + this.limit = limit; + } + + @Override + public int read() throws IOException { + if (bytesRead >= limit) { + return -1; + } + + final int val = in.read(); + if (val > -1) { + bytesRead++; + } + return val; + } + + @Override + public int read(final byte[] b) throws IOException { + if (bytesRead >= limit) { + return -1; + } + + final int maxToRead = (int) Math.min(b.length, limit - bytesRead); + + final int val = in.read(b, 0, maxToRead); + if (val > 0) { + bytesRead += val; + } + return val; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (bytesRead >= limit) { + return -1; + } + + final int maxToRead = (int) Math.min(len, limit - bytesRead); + + final int val = in.read(b, off, maxToRead); + if (val > 0) { + bytesRead += val; + } + return val; + } + + @Override + public long skip(final long n) throws IOException { + final long skipped = in.skip(Math.min(n, limit - bytesRead)); + bytesRead += skipped; + return skipped; + } + + @Override + public int available() throws IOException { + return in.available(); + } + + @Override + public void close() throws IOException { + in.close(); + } + + @Override + public void mark(int readlimit) { + in.mark(readlimit); + } + + @Override + public boolean markSupported() { + return in.markSupported(); + } + + @Override + public void reset() throws IOException { + in.reset(); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/util/OSUtils.java b/nifi-registry/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/util/OSUtils.java new file mode 100644 index 0000000000..4b70866a81 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/util/OSUtils.java @@ -0,0 +1,134 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.bootstrap.util; + +import com.sun.jna.Pointer; +import com.sun.jna.platform.win32.Kernel32; +import com.sun.jna.platform.win32.WinNT; +import org.slf4j.Logger; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * OS specific utilities with generic method interfaces + */ +public final class OSUtils { + /** + * @param process NiFi Process Reference + * @param logger Logger Reference for Debug + * @return Returns pid or null in-case pid could not be determined + * This method takes {@link Process} and {@link Logger} and returns + * the platform specific ProcessId for Unix like systems, a.k.a pid + * In-case it fails to determine the pid, it will return Null. + * Purpose for the Logger is to log any interaction for debugging. + */ + private static Long getUnicesPid(final Process process, final Logger logger) { + try { + final Class procClass = process.getClass(); + final Field pidField = procClass.getDeclaredField("pid"); + pidField.setAccessible(true); + final Object pidObject = pidField.get(process); + + logger.debug("PID Object = {}", pidObject); + + if (pidObject instanceof Number) { + return ((Number) pidObject).longValue(); + } + return null; + } catch (final IllegalAccessException | NoSuchFieldException nsfe) { + logger.debug("Could not find PID for child process due to {}", nsfe); + return null; + } + } + + /** + * @param process NiFi Registry Process Reference + * @param logger Logger Reference for Debug + * @return Returns pid or null in-case pid could not be determined + * This method takes {@link Process} and {@link Logger} and returns + * the platform specific Handle for Win32 Systems, a.k.a pid + * In-case it fails to determine the pid, it will return Null. + * Purpose for the Logger is to log any interaction for debugging. + */ + private static Long getWindowsProcessId(final Process process, final Logger logger) { + /* determine the pid on windows plattforms */ + try { + Field f = process.getClass().getDeclaredField("handle"); + f.setAccessible(true); + long handl = f.getLong(process); + + Kernel32 kernel = Kernel32.INSTANCE; + WinNT.HANDLE handle = new WinNT.HANDLE(); + handle.setPointer(Pointer.createConstant(handl)); + int ret = kernel.GetProcessId(handle); + logger.debug("Detected pid: {}", ret); + return Long.valueOf(ret); + } catch (final IllegalAccessException | NoSuchFieldException nsfe) { + logger.debug("Could not find PID for child process due to {}", nsfe); + } + return null; + } + + /** + * @param process NiFi Process Reference + * @param logger Logger Reference for Debug + * @return Returns pid or null in-case pid could not be determined + * This method takes {@link Process} and {@link Logger} and returns + * the platform specific ProcessId for Unix like systems or Handle for Win32 Systems, a.k.a pid + * In-case it fails to determine the pid, it will return Null. + * Purpose for the Logger is to log any interaction for debugging. + */ + public static Long getProcessId(final Process process, final Logger logger) { + /* + * NiFi Registry built with Java 1.8 and running on Java 9. Reflectively invoke Process.pid() on the given process + * instance to get the PID of this Java process. Reflection is required in this scenario due to NiFi Registry being + * compiled on Java 1.8, which does not have the Process API improvements available in Java 9. + * + * Otherwise, if NiFi is running on Java 1.8, attempt to get PID using capabilities available on Java 1.8. + * + * TODO: When minimum Java version updated to Java 9+, this class should be removed with the addition + * of the pid method to the Process API. + */ + Long pid = null; + if (!System.getProperty("java.version").startsWith("1.")) { + try { + Method pidMethod = process.getClass().getMethod("pid"); + pidMethod.setAccessible(true); + Object pidMethodResult = pidMethod.invoke(process); + if (Long.class.isAssignableFrom(pidMethodResult.getClass())) { + pid = (Long) pidMethodResult; + } else { + logger.debug("Could not determine PID for child process because returned PID was not " + + "assignable to type " + Long.class.getName()); + } + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + logger.debug("Could not find PID for child process due to {}", e); + } + } else if (process.getClass().getName().equals("java.lang.UNIXProcess")) { + pid = getUnicesPid(process, logger); + } else if (process.getClass().getName().equals("java.lang.Win32Process") + || process.getClass().getName().equals("java.lang.ProcessImpl")) { + pid = getWindowsProcessId(process, logger); + } + + return pid; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/pom.xml b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/pom.xml new file mode 100644 index 0000000000..5b5762d1c5 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/pom.xml @@ -0,0 +1,52 @@ + + + + 4.0.0 + + org.apache.nifi.registry + nifi-registry-core + 1.14.0-SNAPSHOT + + + nifi-registry-bundle-utils + jar + + + + org.apache.nifi.registry + nifi-registry-data-model + 1.14.0-SNAPSHOT + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + ${jackson.version} + + + + + + + jigsaw + + (1.8,) + + + + jakarta.xml.bind + jakarta.xml.bind-api + + + + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/extract/BundleException.java b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/extract/BundleException.java new file mode 100644 index 0000000000..3d58906bb2 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/extract/BundleException.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.bundle.extract; + +/** + * Exception to be thrown from a BundleExtractor when an issue occurs during extraction. + */ +public class BundleException extends RuntimeException { + + public BundleException(String message) { + super(message); + } + + public BundleException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/extract/BundleExtractor.java b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/extract/BundleExtractor.java new file mode 100644 index 0000000000..4dc042b701 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/extract/BundleExtractor.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.bundle.extract; + +import org.apache.nifi.registry.bundle.model.BundleDetails; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Extracts the bundle metadata from the given InputStream. + */ +public interface BundleExtractor { + + /** + * @param inputStream the input stream of the binary bundle + * @return the bundle metadata extracted from the input stream + * @throws IOException if an error occurs reading from the InputStream + */ + BundleDetails extract(InputStream inputStream) throws IOException; + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/extract/minificpp/MiNiFiCppBundleExtractor.java b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/extract/minificpp/MiNiFiCppBundleExtractor.java new file mode 100644 index 0000000000..d4bc22c87c --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/extract/minificpp/MiNiFiCppBundleExtractor.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.bundle.extract.minificpp; + +import org.apache.nifi.registry.bundle.model.BundleDetails; +import org.apache.nifi.registry.bundle.extract.BundleExtractor; + +import java.io.IOException; +import java.io.InputStream; + +/** + * ExtensionBundleExtractor for MiNiFi CPP extensions. + */ +public class MiNiFiCppBundleExtractor implements BundleExtractor { + + @Override + public BundleDetails extract(final InputStream inputStream) throws IOException { + // TODO implement + throw new UnsupportedOperationException("Minifi CPP extensions are not yet supported"); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/extract/nar/NarBundleExtractor.java b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/extract/nar/NarBundleExtractor.java new file mode 100644 index 0000000000..35d11cdc3f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/extract/nar/NarBundleExtractor.java @@ -0,0 +1,234 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.bundle.extract.nar; + +import org.apache.nifi.registry.bundle.extract.BundleException; +import org.apache.nifi.registry.bundle.extract.BundleExtractor; +import org.apache.nifi.registry.bundle.extract.nar.docs.ExtensionManifestParser; +import org.apache.nifi.registry.bundle.extract.nar.docs.JacksonExtensionManifestParser; +import org.apache.nifi.registry.bundle.model.BundleIdentifier; +import org.apache.nifi.registry.bundle.model.BundleDetails; +import org.apache.nifi.registry.extension.bundle.BuildInfo; +import org.apache.nifi.registry.extension.component.manifest.ExtensionManifest; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; +import java.util.jar.Manifest; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Implementation of ExtensionBundleExtractor for NAR bundles. + */ +public class NarBundleExtractor implements BundleExtractor { + + /** + * The name of the JarEntry that contains the extension-docs.xml file. + */ + private static String EXTENSION_DESCRIPTOR_ENTRY = "META-INF/docs/extension-manifest.xml"; + + /** + * The pattern of a JarEntry for additionalDetails.html entries. + */ + private static Pattern ADDITIONAL_DETAILS_ENTRY_PATTERN = + Pattern.compile("META-INF\\/docs\\/additional-details\\/(.+)\\/additionalDetails.html"); + + /** + * The format of the date string in the NAR MANIFEST for Built-Timestamp. + */ + private static String BUILT_TIMESTAMP_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + + /** + * Used in place of any build info that is not present. + */ + static String NA = "N/A"; + + + @Override + public BundleDetails extract(final InputStream inputStream) throws IOException { + try (final JarInputStream jarInputStream = new JarInputStream(inputStream)) { + final Manifest manifest = jarInputStream.getManifest(); + if (manifest == null) { + throw new BundleException("NAR bundles must contain a valid MANIFEST"); + } + + final Attributes attributes = manifest.getMainAttributes(); + final BundleIdentifier bundleIdentifier = getBundleCoordinate(attributes); + final BundleIdentifier dependencyCoordinate = getDependencyBundleCoordinate(attributes); + final BuildInfo buildInfo = getBuildInfo(attributes); + + final BundleDetails.Builder builder = new BundleDetails.Builder() + .coordinate(bundleIdentifier) + .addDependencyCoordinate(dependencyCoordinate) + .buildInfo(buildInfo); + + parseExtensionDocs(jarInputStream, builder); + + return builder.build(); + } + } + + private BundleIdentifier getBundleCoordinate(final Attributes attributes) { + try { + final String groupId = attributes.getValue(NarManifestEntry.NAR_GROUP.getManifestName()); + final String artifactId = attributes.getValue(NarManifestEntry.NAR_ID.getManifestName()); + final String version = attributes.getValue(NarManifestEntry.NAR_VERSION.getManifestName()); + + return new BundleIdentifier(groupId, artifactId, version); + } catch (Exception e) { + throw new BundleException("Unable to obtain bundle coordinate due to: " + e.getMessage(), e); + } + } + + private BundleIdentifier getDependencyBundleCoordinate(final Attributes attributes) { + try { + final String dependencyGroupId = attributes.getValue(NarManifestEntry.NAR_DEPENDENCY_GROUP.getManifestName()); + final String dependencyArtifactId = attributes.getValue(NarManifestEntry.NAR_DEPENDENCY_ID.getManifestName()); + final String dependencyVersion = attributes.getValue(NarManifestEntry.NAR_DEPENDENCY_VERSION.getManifestName()); + + final BundleIdentifier dependencyCoordinate; + if (dependencyArtifactId != null) { + dependencyCoordinate = new BundleIdentifier(dependencyGroupId, dependencyArtifactId, dependencyVersion); + } else { + dependencyCoordinate = null; + } + return dependencyCoordinate; + } catch (Exception e) { + throw new BundleException("Unable to obtain bundle coordinate for dependency due to: " + e.getMessage(), e); + } + } + + private BuildInfo getBuildInfo(final Attributes attributes) { + final String buildBranch = attributes.getValue(NarManifestEntry.BUILD_BRANCH.getManifestName()); + final String buildTag = attributes.getValue(NarManifestEntry.BUILD_TAG.getManifestName()); + final String buildRevision = attributes.getValue(NarManifestEntry.BUILD_REVISION.getManifestName()); + final String buildTimestamp = attributes.getValue(NarManifestEntry.BUILD_TIMESTAMP.getManifestName()); + final String buildJdk = attributes.getValue(NarManifestEntry.BUILD_JDK.getManifestName()); + final String builtBy = attributes.getValue(NarManifestEntry.BUILT_BY.getManifestName()); + + final SimpleDateFormat simpleDateFormat = new SimpleDateFormat(BUILT_TIMESTAMP_FORMAT); + try { + final Date buildDate = simpleDateFormat.parse(buildTimestamp); + + final BuildInfo buildInfo = new BuildInfo(); + buildInfo.setBuildTool(isBlank(buildJdk) ? NA : buildJdk); + buildInfo.setBuildBranch(isBlank(buildBranch) ? NA : buildBranch); + buildInfo.setBuildTag(isBlank(buildTag) ? NA : buildTag); + buildInfo.setBuildRevision(isBlank(buildRevision) ? NA : buildRevision); + buildInfo.setBuilt(buildDate.getTime()); + buildInfo.setBuiltBy(isBlank(builtBy) ? NA : builtBy); + buildInfo.setBuildFlags(NA); + return buildInfo; + + } catch (ParseException e) { + throw new BundleException("Unable to parse " + NarManifestEntry.BUILD_TIMESTAMP.getManifestName(), e); + } catch (Exception e) { + throw new BundleException("Unable to create build info for bundle due to: " + e.getMessage(), e); + } + } + + public boolean isBlank(String value) { + return (value == null || value.trim().isEmpty()); + } + + private void parseExtensionDocs(final JarInputStream jarInputStream, final BundleDetails.Builder builder) throws IOException { + JarEntry jarEntry; + boolean foundExtensionDocs = false; + while((jarEntry = jarInputStream.getNextJarEntry()) != null) { + final String jarEntryName = jarEntry.getName(); + if (EXTENSION_DESCRIPTOR_ENTRY.equals(jarEntryName)) { + try { + final byte[] rawDocsContent = toByteArray(jarInputStream); + final ExtensionManifestParser docsParser = new JacksonExtensionManifestParser(); + final InputStream inputStream = new NonCloseableInputStream(new ByteArrayInputStream(rawDocsContent)); + + final ExtensionManifest extensionManifest = docsParser.parse(inputStream); + builder.addExtensions(extensionManifest.getExtensions()); + builder.systemApiVersion(extensionManifest.getSystemApiVersion()); + + foundExtensionDocs = true; + } catch (Exception e) { + throw new BundleException("Unable to obtain extension info for bundle due to: " + e.getMessage(), e); + } + } else { + final Matcher matcher = ADDITIONAL_DETAILS_ENTRY_PATTERN.matcher(jarEntryName); + if (matcher.matches()) { + final String extensionName = matcher.group(1); + final String additionalDetailsContent = new String(toByteArray(jarInputStream), StandardCharsets.UTF_8); + builder.addAdditionalDetails(extensionName, additionalDetailsContent); + } + } + } + + if (!foundExtensionDocs) { + throw new BundleException("Unable to find descriptor at '" + EXTENSION_DESCRIPTOR_ENTRY + "'. " + + "This NAR may need to be rebuilt with the latest version of the NiFi NAR Maven Plugin."); + } + } + + private byte[] toByteArray(final InputStream input) throws IOException { + final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + int nRead; + byte[] data = new byte[16384]; + while ((nRead = input.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, nRead); + } + + return buffer.toByteArray(); + } + + private static class NonCloseableInputStream extends FilterInputStream { + + private final InputStream toWrap; + + public NonCloseableInputStream(final InputStream toWrap) { + super(toWrap); + this.toWrap = toWrap; + } + + @Override + public int read() throws IOException { + return toWrap.read(); + } + + @Override + public int read(byte[] b) throws IOException { + return toWrap.read(b); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return toWrap.read(b, off, len); + } + + @Override + public void close() throws IOException { + // do nothing + } + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/extract/nar/NarManifestEntry.java b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/extract/nar/NarManifestEntry.java new file mode 100644 index 0000000000..56504778ff --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/extract/nar/NarManifestEntry.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.bundle.extract.nar; + +/** + * Enumeration of entries that will be in a NAR MANIFEST file. + */ +public enum NarManifestEntry { + + NAR_GROUP("Nar-Group"), + NAR_ID("Nar-Id"), + NAR_VERSION("Nar-Version"), + NAR_DEPENDENCY_GROUP("Nar-Dependency-Group"), + NAR_DEPENDENCY_ID("Nar-Dependency-Id"), + NAR_DEPENDENCY_VERSION("Nar-Dependency-Version"), + BUILD_TAG("Build-Tag"), + BUILD_REVISION("Build-Revision"), + BUILD_BRANCH("Build-Branch"), + BUILD_TIMESTAMP("Build-Timestamp"), + BUILD_JDK("Build-Jdk"), + BUILT_BY("Built-By"), + ; + + final String manifestName; + + NarManifestEntry(String manifestName) { + this.manifestName = manifestName; + } + + public String getManifestName() { + return manifestName; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/extract/nar/docs/ExtensionManifestParser.java b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/extract/nar/docs/ExtensionManifestParser.java new file mode 100644 index 0000000000..a4260a9f4d --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/extract/nar/docs/ExtensionManifestParser.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.bundle.extract.nar.docs; + +import org.apache.nifi.registry.extension.component.manifest.ExtensionManifest; + +import java.io.InputStream; + +/** + * Parses an InputStream that is expected to contain the content of META-INF/docs/extensions-manifest.xml from a NAR. + */ +public interface ExtensionManifestParser { + + ExtensionManifest parse(InputStream inputStream); + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/extract/nar/docs/JacksonExtensionManifestParser.java b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/extract/nar/docs/JacksonExtensionManifestParser.java new file mode 100644 index 0000000000..a785bae4f3 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/extract/nar/docs/JacksonExtensionManifestParser.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.bundle.extract.nar.docs; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule; +import org.apache.nifi.registry.bundle.extract.BundleException; +import org.apache.nifi.registry.extension.component.manifest.ExtensionManifest; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Implementation of {@link ExtensionManifestParser} that uses Jackson XML to unmarshall the extension-manifest.xml content. + */ +public class JacksonExtensionManifestParser implements ExtensionManifestParser { + + private final ObjectMapper mapper; + + public JacksonExtensionManifestParser() { + this.mapper = new XmlMapper(); + this.mapper.registerModule(new JaxbAnnotationModule()); + this.mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); + } + + @Override + public ExtensionManifest parse(InputStream inputStream) { + try { + return mapper.readValue(inputStream, ExtensionManifest.class); + } catch (IOException e) { + throw new BundleException("Unable to parse extension manifest due to: " + e.getMessage(), e); + } + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/model/BundleDetails.java b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/model/BundleDetails.java new file mode 100644 index 0000000000..7a083ac325 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/model/BundleDetails.java @@ -0,0 +1,145 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.bundle.model; + + +import org.apache.nifi.registry.bundle.extract.BundleExtractor; +import org.apache.nifi.registry.extension.bundle.BuildInfo; +import org.apache.nifi.registry.extension.component.manifest.Extension; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.apache.nifi.registry.bundle.util.BundleUtils.validateNotNull; + +/** + * Details for a given bundle which are obtained from a given {@link BundleExtractor}. + */ +public class BundleDetails { + + private final BundleIdentifier bundleIdentifier; + private final Set dependencies; + + private final String systemApiVersion; + + private final Set extensions; + private final Map additionalDetails; + + private final BuildInfo buildInfo; + + private BundleDetails(final Builder builder) { + this.bundleIdentifier = builder.bundleIdentifier; + this.dependencies = Collections.unmodifiableSet(new HashSet<>(builder.dependencies)); + this.extensions = Collections.unmodifiableSet(new HashSet<>(builder.extensions)); + this.additionalDetails = Collections.unmodifiableMap(new HashMap<>(builder.additionalDetails)); + this.systemApiVersion = builder.systemApiVersion; + this.buildInfo = builder.buildInfo; + + validateNotNull("Bundle Coordinate", this.bundleIdentifier); + validateNotNull("Dependency Coordinates", this.dependencies); + validateNotNull("Extension Details", this.extensions); + validateNotNull("System API Version", this.systemApiVersion); + validateNotNull("Build Details", this.buildInfo); + } + + public BundleIdentifier getBundleIdentifier() { + return bundleIdentifier; + } + + public Set getDependencies() { + return dependencies; + } + + public String getSystemApiVersion() { + return systemApiVersion; + } + + public Set getExtensions() { + return extensions; + } + + public Map getAdditionalDetails() { + return additionalDetails; + } + + public BuildInfo getBuildInfo() { + return buildInfo; + } + + /** + * Builder for creating instances of BundleDetails. + */ + public static class Builder { + + private BundleIdentifier bundleIdentifier; + private Set dependencies = new HashSet<>(); + private Set extensions = new HashSet<>(); + private Map additionalDetails = new HashMap<>(); + private BuildInfo buildInfo; + private String systemApiVersion; + + public Builder coordinate(final BundleIdentifier bundleIdentifier) { + this.bundleIdentifier = bundleIdentifier; + return this; + } + + public Builder addDependencyCoordinate(final BundleIdentifier dependencyCoordinate) { + if (dependencyCoordinate != null) { + this.dependencies.add(dependencyCoordinate); + } + return this; + } + + public Builder systemApiVersion(final String systemApiVersion) { + this.systemApiVersion = systemApiVersion; + return this; + } + + public Builder addExtension(final Extension extension) { + if (extension != null) { + this.extensions.add(extension); + } + return this; + } + + public Builder addExtensions(final List extensions) { + if (extensions != null) { + this.extensions.addAll(extensions); + } + return this; + } + + public Builder addAdditionalDetails(final String extensionName, final String additionalDetails) { + this.additionalDetails.put(extensionName, additionalDetails); + return this; + } + + public Builder buildInfo(final BuildInfo buildInfo) { + this.buildInfo = buildInfo; + return this; + } + + public BundleDetails build() { + return new BundleDetails(this); + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/model/BundleIdentifier.java b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/model/BundleIdentifier.java new file mode 100644 index 0000000000..21d8ca617d --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/model/BundleIdentifier.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.bundle.model; + +import static org.apache.nifi.registry.bundle.util.BundleUtils.validateNotBlank; + +/** + * The identifier of an extension bundle (i.e group + artifact + version). + */ +public class BundleIdentifier { + + private final String groupId; + private final String artifactId; + private final String version; + + private final String identifier; + + public BundleIdentifier(final String groupId, final String artifactId, final String version) { + this.groupId = groupId; + this.artifactId = artifactId; + this.version = version; + validateNotBlank("Group Id", this.groupId); + validateNotBlank("Artifact Id", this.artifactId); + validateNotBlank("Version", this.version); + + this.identifier = this.groupId + ":" + this.artifactId + ":" + this.version; + } + + public String getGroupId() { + return groupId; + } + + public String getArtifactId() { + return artifactId; + } + + public String getVersion() { + return version; + } + + public final String getIdentifier() { + return identifier; + } + + @Override + public String toString() { + return identifier; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + + if (!(obj instanceof BundleIdentifier)) { + return false; + } + + final BundleIdentifier other = (BundleIdentifier) obj; + return getIdentifier().equals(other.getIdentifier()); + } + + @Override + public int hashCode() { + return 37 * this.identifier.hashCode(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/util/BundleUtils.java b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/util/BundleUtils.java new file mode 100644 index 0000000000..2640b23ee2 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/bundle/util/BundleUtils.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.bundle.util; + +public class BundleUtils { + + public static boolean isBlank(final String value) { + return (value == null || value.trim().isEmpty()); + } + + public static void validateNotNull(String fieldName, Object value) { + if (value == null) { + throw new IllegalArgumentException(fieldName + " is required"); + } + } + + public static void validateNotBlank(String fieldName, String value) { + if (isBlank(value)) { + throw new IllegalArgumentException(fieldName + " is required"); + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/java/org/apache/nifi/registry/bundle/extract/nar/TestNarBundleExtractor.java b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/java/org/apache/nifi/registry/bundle/extract/nar/TestNarBundleExtractor.java new file mode 100644 index 0000000000..afa8a1a48d --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/java/org/apache/nifi/registry/bundle/extract/nar/TestNarBundleExtractor.java @@ -0,0 +1,154 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.bundle.extract.nar; + +import org.apache.nifi.registry.bundle.extract.BundleException; +import org.apache.nifi.registry.bundle.extract.BundleExtractor; +import org.apache.nifi.registry.bundle.model.BundleIdentifier; +import org.apache.nifi.registry.bundle.model.BundleDetails; +import org.apache.nifi.registry.extension.bundle.BuildInfo; +import org.junit.Before; +import org.junit.Test; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class TestNarBundleExtractor { + + private BundleExtractor extractor; + + @Before + public void setup() { + this.extractor = new NarBundleExtractor(); + } + + @Test + public void testExtractFromGoodNarNoDependencies() throws IOException { + try (final InputStream in = new FileInputStream("src/test/resources/nars/nifi-framework-nar.nar")) { + final BundleDetails bundleDetails = extractor.extract(in); + assertNotNull(bundleDetails); + assertNotNull(bundleDetails.getBundleIdentifier()); + assertNotNull(bundleDetails.getDependencies()); + assertEquals(0, bundleDetails.getDependencies().size()); + + final BundleIdentifier bundleIdentifier = bundleDetails.getBundleIdentifier(); + assertEquals("org.apache.nifi", bundleIdentifier.getGroupId()); + assertEquals("nifi-framework-nar", bundleIdentifier.getArtifactId()); + assertEquals("1.8.0", bundleIdentifier.getVersion()); + + assertNotNull(bundleDetails.getExtensions()); + assertEquals(0, bundleDetails.getExtensions().size()); + assertEquals("1.8.0", bundleDetails.getSystemApiVersion()); + } + } + + @Test + public void testExtractFromGoodNarWithDependencies() throws IOException { + try (final InputStream in = new FileInputStream("src/test/resources/nars/nifi-foo-nar.nar")) { + final BundleDetails bundleDetails = extractor.extract(in); + assertNotNull(bundleDetails); + assertNotNull(bundleDetails.getBundleIdentifier()); + assertNotNull(bundleDetails.getDependencies()); + assertEquals(1, bundleDetails.getDependencies().size()); + + final BundleIdentifier bundleIdentifier = bundleDetails.getBundleIdentifier(); + assertEquals("org.apache.nifi", bundleIdentifier.getGroupId()); + assertEquals("nifi-foo-nar", bundleIdentifier.getArtifactId()); + assertEquals("1.8.0", bundleIdentifier.getVersion()); + + final BundleIdentifier dependencyCoordinate = bundleDetails.getDependencies().stream().findFirst().get(); + assertEquals("org.apache.nifi", dependencyCoordinate.getGroupId()); + assertEquals("nifi-bar-nar", dependencyCoordinate.getArtifactId()); + assertEquals("2.0.0", dependencyCoordinate.getVersion()); + + final Map additionalDetails = bundleDetails.getAdditionalDetails(); + assertNotNull(additionalDetails); + assertEquals(0, additionalDetails.size()); + } + } + + @Test(expected = BundleException.class) + public void testExtractFromNarMissingRequiredManifestEntries() throws IOException { + try (final InputStream in = new FileInputStream("src/test/resources/nars/nifi-missing-manifest-entries.nar")) { + extractor.extract(in); + fail("Should have thrown exception"); + } + } + + @Test(expected = BundleException.class) + public void testExtractFromNarMissingManifest() throws IOException { + try (final InputStream in = new FileInputStream("src/test/resources/nars/nifi-missing-manifest.nar")) { + extractor.extract(in); + fail("Should have thrown exception"); + } + } + + @Test(expected = BundleException.class) + public void testExtractFromNarMissingExtensionDescriptor() throws IOException { + try (final InputStream in = new FileInputStream("src/test/resources/nars/nifi-foo-nar-missing-extension-descriptor.nar")) { + extractor.extract(in); + fail("Should have thrown exception"); + } + } + + @Test + public void testExtractFromNarWithDescriptorAndAdditionalDetails() throws IOException { + try (final InputStream in = new FileInputStream("src/test/resources/nars/nifi-hadoop-nar.nar")) { + final BundleDetails bundleDetails = extractor.extract(in); + assertNotNull(bundleDetails); + assertNotNull(bundleDetails.getBundleIdentifier()); + assertNotNull(bundleDetails.getDependencies()); + assertEquals(1, bundleDetails.getDependencies().size()); + + final BundleIdentifier bundleIdentifier = bundleDetails.getBundleIdentifier(); + assertEquals("org.apache.nifi", bundleIdentifier.getGroupId()); + assertEquals("nifi-hadoop-nar", bundleIdentifier.getArtifactId()); + assertEquals("1.9.0-SNAPSHOT", bundleIdentifier.getVersion()); + + final BuildInfo buildDetails = bundleDetails.getBuildInfo(); + assertNotNull(buildDetails); + assertEquals("1.8.0_162", buildDetails.getBuildTool()); + assertEquals(NarBundleExtractor.NA, buildDetails.getBuildFlags()); + assertEquals("master", buildDetails.getBuildBranch()); + assertEquals("HEAD", buildDetails.getBuildTag()); + assertEquals("1a937b6", buildDetails.getBuildRevision()); + assertEquals("jsmith", buildDetails.getBuiltBy()); + assertNotNull(buildDetails.getBuilt()); + + assertEquals("1.10.0-SNAPSHOT", bundleDetails.getSystemApiVersion()); + assertNotNull(bundleDetails.getExtensions()); + assertEquals(10, bundleDetails.getExtensions().size()); + + final Map additionalDetails = bundleDetails.getAdditionalDetails(); + assertNotNull(additionalDetails); + assertEquals(3, additionalDetails.size()); + + final String listHdfsKey = "org.apache.nifi.processors.hadoop.ListHDFS"; + assertTrue(additionalDetails.containsKey(listHdfsKey)); + assertTrue(additionalDetails.get(listHdfsKey).startsWith("")); + assertTrue(additionalDetails.get(listHdfsKey).trim().endsWith("")); + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/java/org/apache/nifi/registry/bundle/extract/nar/docs/TestJacksonExtensionManifestParser.java b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/java/org/apache/nifi/registry/bundle/extract/nar/docs/TestJacksonExtensionManifestParser.java new file mode 100644 index 0000000000..6f57ea9909 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/java/org/apache/nifi/registry/bundle/extract/nar/docs/TestJacksonExtensionManifestParser.java @@ -0,0 +1,161 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.bundle.extract.nar.docs; + +import org.apache.nifi.registry.extension.component.manifest.Extension; +import org.apache.nifi.registry.extension.component.manifest.ExtensionManifest; +import org.apache.nifi.registry.extension.component.manifest.ExtensionType; +import org.apache.nifi.registry.extension.component.manifest.ProvidedServiceAPI; +import org.apache.nifi.registry.extension.component.manifest.Restriction; +import org.junit.Before; +import org.junit.Test; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class TestJacksonExtensionManifestParser { + + private ExtensionManifestParser parser; + + @Before + public void setup() { + parser = new JacksonExtensionManifestParser(); + } + + @Test + public void testDocsWithProcessors() throws IOException { + final ExtensionManifest extensionManifest = parse("src/test/resources/descriptors/extension-manifest-hadoop-nar.xml"); + assertNotNull(extensionManifest); + assertEquals("1.10.0-SNAPSHOT", extensionManifest.getSystemApiVersion()); + + final List extensionDetails = extensionManifest.getExtensions(); + assertEquals(10, extensionDetails.size()); + + final Extension putHdfsExtension = extensionDetails.stream() + .filter(e -> e.getName().equals("org.apache.nifi.processors.hadoop.PutHDFS")) + .findFirst() + .orElse(null); + + assertNotNull(putHdfsExtension); + assertEquals(ExtensionType.PROCESSOR, putHdfsExtension.getType()); + assertEquals("Write FlowFile data to Hadoop Distributed File System (HDFS)", putHdfsExtension.getDescription()); + assertEquals(5, putHdfsExtension.getTags().size()); + assertTrue(putHdfsExtension.getTags().contains("hadoop")); + assertTrue(putHdfsExtension.getTags().contains("HDFS")); + assertTrue(putHdfsExtension.getTags().contains("put")); + assertTrue(putHdfsExtension.getTags().contains("copy")); + assertTrue(putHdfsExtension.getTags().contains("filesystem")); + assertNull(putHdfsExtension.getProvidedServiceAPIs()); + + assertNotNull(putHdfsExtension.getProperties()); + assertEquals(15, putHdfsExtension.getProperties().size()); + + assertNull(putHdfsExtension.getRestricted().getGeneralRestrictionExplanation()); + + final List restrictions = putHdfsExtension.getRestricted().getRestrictions(); + assertNotNull(restrictions); + assertEquals(1, restrictions.size()); + + final Restriction restriction = restrictions.stream().findFirst().orElse(null); + assertEquals("write filesystem", restriction.getRequiredPermission()); + assertEquals("Provides operator the ability to delete any file that NiFi has access to in HDFS or\n" + + " the local filesystem.", restriction.getExplanation().trim()); + } + + @Test + public void testDocsWithControllerService() throws IOException { + final ExtensionManifest extensionManifest = parse("src/test/resources/descriptors/extension-manifest-dbcp-service-nar.xml"); + assertNotNull(extensionManifest); + assertEquals("1.10.0-SNAPSHOT", extensionManifest.getSystemApiVersion()); + + final List extensions = extensionManifest.getExtensions(); + assertEquals(2, extensions.size()); + + final Extension dbcpPoolExtension = extensions.stream() + .filter(e -> e.getName().equals("org.apache.nifi.dbcp.DBCPConnectionPool")) + .findFirst() + .orElse(null); + + assertNotNull(dbcpPoolExtension); + assertEquals(ExtensionType.CONTROLLER_SERVICE, dbcpPoolExtension.getType()); + assertEquals("Provides Database Connection Pooling Service. Connections can be asked from pool and returned\n" + + " after usage.", dbcpPoolExtension.getDescription().trim()); + assertEquals(6, dbcpPoolExtension.getTags().size()); + assertEquals(1, dbcpPoolExtension.getProvidedServiceAPIs().size()); + + final ProvidedServiceAPI providedServiceApi = dbcpPoolExtension.getProvidedServiceAPIs().iterator().next(); + assertNotNull(providedServiceApi); + assertEquals("org.apache.nifi.dbcp.DBCPService", providedServiceApi.getClassName()); + assertEquals("org.apache.nifi", providedServiceApi.getGroupId()); + assertEquals("nifi-standard-services-api-nar", providedServiceApi.getArtifactId()); + assertEquals("1.10.0-SNAPSHOT", providedServiceApi.getVersion()); + } + + @Test + public void testDocsWithReportingTask() throws IOException { + final ExtensionManifest extensionManifest = parse("src/test/resources/descriptors/extension-manifest-ambari-nar.xml"); + assertNotNull(extensionManifest); + assertEquals("1.10.0-SNAPSHOT", extensionManifest.getSystemApiVersion()); + + final List extensions = extensionManifest.getExtensions(); + assertEquals(1, extensions.size()); + + final Extension reportingTask = extensions.stream() + .filter(e -> e.getName().equals("org.apache.nifi.reporting.ambari.AmbariReportingTask")) + .findFirst() + .orElse(null); + + assertNotNull(reportingTask); + assertEquals(ExtensionType.REPORTING_TASK, reportingTask.getType()); + assertNotNull(reportingTask.getDescription()); + assertEquals(3, reportingTask.getTags().size()); + assertTrue(reportingTask.getTags().contains("reporting")); + assertTrue(reportingTask.getTags().contains("metrics")); + assertTrue(reportingTask.getTags().contains("ambari")); + assertNull(reportingTask.getProvidedServiceAPIs()); + } + + @Test + public void testDocsForTestComponents() throws IOException { + final ExtensionManifest extensionManifest = parse("src/test/resources/descriptors/extension-manifest-test-components.xml"); + assertNotNull(extensionManifest); + assertEquals("1.8.0", extensionManifest.getSystemApiVersion()); + + final List extensionDetails = extensionManifest.getExtensions(); + assertEquals(4, extensionDetails.size()); + + } + + @Test + public void testDocsForMissingSystemApi() throws IOException { + final ExtensionManifest extensionManifest = parse("src/test/resources/descriptors/extension-manifest-missing-sys-api.xml"); + assertNotNull(extensionManifest); + } + + private ExtensionManifest parse(final String file) throws IOException { + try (final InputStream inputStream = new FileInputStream(file)) { + return parser.parse(inputStream); + } + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/descriptors/extension-manifest-ambari-nar.xml b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/descriptors/extension-manifest-ambari-nar.xml new file mode 100644 index 0000000000..4cfe3ea087 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/descriptors/extension-manifest-ambari-nar.xml @@ -0,0 +1,84 @@ + + 1.10.0-SNAPSHOT + + + org.apache.nifi.reporting.ambari.AmbariReportingTask + REPORTING_TASK + + Publishes metrics from NiFi to Ambari Metrics Service (AMS). Due to how the Ambari Metrics + Service works, this reporting task should be scheduled to run every 60 seconds. Each iteration it will + send the metrics from the previous iteration, and calculate the current metrics to be sent on next + iteration. Scheduling this reporting task at a frequency other than 60 seconds may produce unexpected + results. + + + reporting + ambari + metrics + + + + Metrics Collector URL + Metrics Collector URL + The URL of the Ambari Metrics Collector Service + http://localhost:6188/ws/v1/timeline/metrics + + true + false + true + VARIABLE_REGISTRY + false + false + + + Application ID + Application ID + The Application ID to be included in the metrics sent to Ambari + nifi + + true + false + true + VARIABLE_REGISTRY + false + false + + + Hostname + Hostname + The Hostname of this NiFi instance to be included in the metrics sent to Ambari + + ${hostname(true)} + + true + false + true + VARIABLE_REGISTRY + false + false + + + Process Group ID + Process Group ID + If specified, the reporting task will send metrics about this process group only. If + not, the root process group is used and global metrics are sent. + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + + + + + + + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/descriptors/extension-manifest-dbcp-service-nar.xml b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/descriptors/extension-manifest-dbcp-service-nar.xml new file mode 100644 index 0000000000..672ee75874 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/descriptors/extension-manifest-dbcp-service-nar.xml @@ -0,0 +1,325 @@ + + 1.10.0-SNAPSHOT + + + org.apache.nifi.dbcp.DBCPConnectionPoolLookup + CONTROLLER_SERVICE + + Provides a DBCPService that can be used to dynamically select another DBCPService. This service + requires an attribute named 'database.name' to be passed in when asking for a connection, and will throw + an exception if the attribute is missing. The value of 'database.name' will be used to select the + DBCPService that has been registered with that name. This will allow multiple DBCPServices to be defined + and registered, and then selected dynamically at runtime by tagging flow files with the appropriate + 'database.name' attribute. + + + dbcp + jdbc + database + connection + pooling + store + + + + + The + JDBC property value + + false + NONE + + + + + + + + + + org.apache.nifi.dbcp.DBCPService + org.apache.nifi + nifi-standard-services-api-nar + 1.10.0-SNAPSHOT + + + + + org.apache.nifi.dbcp.DBCPConnectionPool + CONTROLLER_SERVICE + + Provides Database Connection Pooling Service. Connections can be asked from pool and returned + after usage. + + + dbcp + jdbc + database + connection + pooling + store + + + + Database Connection URL + Database Connection URL + A database connection URL used to connect to a database. May contain database system + name, host, port, database name and some parameters. The exact syntax of a database connection + URL is specified by your DBMS. + + + + true + false + true + VARIABLE_REGISTRY + false + false + + + Database Driver Class Name + Database Driver Class Name + Database driver class name + + + true + false + true + VARIABLE_REGISTRY + false + false + + + database-driver-locations + Database Driver Location(s) + Comma-separated list of files/folders and/or URLs containing the driver JAR and its + dependencies (if any). For example '/var/tmp/mariadb-java-client-1.1.7.jar' + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + kerberos-credentials-service + Kerberos Credentials Service + Specifies the Kerberos Credentials Controller Service that should be used for + authenticating with Kerberos + + + + org.apache.nifi.kerberos.KerberosCredentialsService + org.apache.nifi + org.apache.nifi:nifi-standard-services-api-nar:nar:1.10.0-SNAPSHOT + 1.10.0-SNAPSHOT + + + false + false + false + NONE + false + false + + + Database User + Database User + Database user name + + + false + false + true + VARIABLE_REGISTRY + false + false + + + Password + Password + The password for the database user + + + false + true + true + VARIABLE_REGISTRY + false + false + + + Max Wait Time + Max Wait Time + The maximum amount of time that the pool will wait (when there are no available + connections) for a connection to be returned before failing, or -1 to wait indefinitely. + + 500 millis + + true + false + false + NONE + false + false + + + Max Total Connections + Max Total Connections + The maximum number of active connections that can be allocated from this pool at the + same time, or negative for no limit. + + 8 + + true + false + false + NONE + false + false + + + Validation-query + Validation query + Validation query used to validate connections before returning them. When connection is + invalid, it get's dropped and new valid connection will be returned. Note!! Using validation + might have some performance penalty. + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + dbcp-min-idle-conns + Minimum Idle Connections + The minimum number of connections that can remain idle in the pool, without extra ones + being created, or zero to create none. + + 0 + + false + false + true + VARIABLE_REGISTRY + false + false + + + dbcp-max-idle-conns + Max Idle Connections + The maximum number of connections that can remain idle in the pool, without extra ones + being released, or negative for no limit. + + 8 + + false + false + true + VARIABLE_REGISTRY + false + false + + + dbcp-max-conn-lifetime + Max Connection Lifetime + The maximum lifetime in milliseconds of a connection. After this time is exceeded the + connection will fail the next activation, passivation or validation test. A value of zero or + less means the connection has an infinite lifetime. + + -1 + + false + false + true + VARIABLE_REGISTRY + false + false + + + dbcp-time-between-eviction-runs + Time Between Eviction Runs + The number of milliseconds to sleep between runs of the idle connection evictor thread. + When non-positive, no idle connection evictor thread will be run. + + -1 + + false + false + true + VARIABLE_REGISTRY + false + false + + + dbcp-min-evictable-idle-time + Minimum Evictable Idle Time + The minimum amount of time a connection may sit idle in the pool before it is eligible + for eviction. + + 30 mins + + false + false + true + VARIABLE_REGISTRY + false + false + + + dbcp-soft-min-evictable-idle-time + Soft Minimum Evictable Idle Time + The minimum amount of time a connection may sit idle in the pool before it is eligible + for eviction by the idle connection evictor, with the extra condition that at least a minimum + number of idle connections remain in the pool. When the not-soft version of this option is set + to a positive value, it is examined first by the idle connection evictor: when idle connections + are visited by the evictor, idle time is first compared against it (without considering the + number of idle connections in the pool) and then against this soft option, including the minimum + idle connections constraint. + + -1 + + false + false + true + VARIABLE_REGISTRY + false + false + + + + + JDBC property name + JDBC property value + Specifies a property name and value to be set on the JDBC connection(s). If Expression + Language is used, evaluation will be performed upon the controller service being enabled. Note + that no flow file input (attributes, e.g.) is available for use in Expression Language + constructs for these properties. + + false + VARIABLE_REGISTRY + + + + + + + + + + org.apache.nifi.dbcp.DBCPService + org.apache.nifi + nifi-standard-services-api-nar + 1.10.0-SNAPSHOT + + + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/descriptors/extension-manifest-hadoop-nar.xml b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/descriptors/extension-manifest-hadoop-nar.xml new file mode 100644 index 0000000000..d4a91bce63 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/descriptors/extension-manifest-hadoop-nar.xml @@ -0,0 +1,3274 @@ + + 1.10.0-SNAPSHOT + + + org.apache.nifi.processors.hadoop.MoveHDFS + PROCESSOR + + Rename existing files or a directory of files (non-recursive) on Hadoop Distributed File System + (HDFS). + + + hadoop + HDFS + put + move + filesystem + moveHDFS + + + + Hadoop Configuration Resources + Hadoop Configuration Resources + A file or comma separated list of files which contains the Hadoop file system + configuration. Without this, Hadoop will search the classpath for a 'core-site.xml' and + 'hdfs-site.xml' file or will revert to a default configuration. To use swebhdfs, see 'Additional + Details' section of PutHDFS's documentation. + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + kerberos-credentials-service + Kerberos Credentials Service + Specifies the Kerberos Credentials Controller Service that should be used for + authenticating with Kerberos + + + + org.apache.nifi.kerberos.KerberosCredentialsService + org.apache.nifi + org.apache.nifi:nifi-standard-services-api-nar:nar:1.10.0-SNAPSHOT + 1.10.0-SNAPSHOT + + + false + false + false + NONE + false + false + + + Kerberos Principal + Kerberos Principal + Kerberos principal to authenticate as. Requires nifi.kerberos.krb5.file to be set in + your nifi.properties + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + Kerberos Keytab + Kerberos Keytab + Kerberos keytab associated with the principal. Requires nifi.kerberos.krb5.file to be + set in your nifi.properties + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + Kerberos Relogin Period + Kerberos Relogin Period + Period of time which should pass before attempting a kerberos relogin. + + This property has been deprecated, and has no effect on processing. Relogins now occur + automatically. + + 4 hours + + false + false + true + VARIABLE_REGISTRY + false + false + + + Additional Classpath Resources + Additional Classpath Resources + A comma-separated list of paths to files and/or directories that will be added to the + classpath. When specifying a directory, all files with in the directory will be added to the + classpath, but further sub-directories will not be included. + + + + false + false + false + NONE + true + false + + + Conflict Resolution Strategy + Conflict Resolution Strategy + Indicates what should happen when a file with the same name already exists in the + output directory + + fail + + + replace + replace + Replaces the existing file if any. + + + ignore + ignore + Failed rename operation stops processing and routes to success. + + + fail + fail + Failing to rename a file routes to failure. + + + true + false + false + NONE + false + false + + + Input Directory or File + Input Directory or File + The HDFS directory from which files should be read, or a single file to read. + + ${path} + + true + false + true + FLOWFILE_ATTRIBUTES + false + false + + + Output Directory + Output Directory + The HDFS directory where the files will be moved to + + + true + false + true + VARIABLE_REGISTRY + false + false + + + HDFS Operation + HDFS Operation + The operation that will be performed on the source file + move + + + move + move + + + + copy + copy + + + + true + false + false + NONE + false + false + + + File Filter Regex + File Filter Regex + A Java Regular Expression for filtering Filenames; if a filter is supplied then only + files whose names match that Regular Expression will be fetched, otherwise all files will be + fetched + + + + false + false + false + NONE + false + false + + + Ignore Dotted Files + Ignore Dotted Files + If true, files whose names begin with a dot (".") will be ignored + true + + + true + true + + + + false + false + + + + true + false + false + NONE + false + false + + + Remote Owner + Remote Owner + Changes the owner of the HDFS file to this value after it is written. This only works + if NiFi is running as a user that has HDFS super user privilege to change owner + + + + false + false + false + NONE + false + false + + + Remote Group + Remote Group + Changes the group of the HDFS file to this value after it is written. This only works + if NiFi is running as a user that has HDFS super user privilege to change group + + + + false + false + false + NONE + false + false + + + + + + success + Files that have been successfully renamed on HDFS are transferred to this + relationship + + false + + + failure + Files that could not be renamed on HDFS are transferred to this relationship + + false + + + + + + filename + The name of the file written to HDFS comes from the value of this attribute. + + + + + + filename + The name of the file written to HDFS is stored in this attribute. + + + absolute.hdfs.path + The absolute path to the file on HDFS is stored in this attribute. + + + + + + + read filesystem + Provides operator the ability to retrieve any file that NiFi has access to in HDFS + or the local filesystem. + + + + write filesystem + Provides operator the ability to delete any file that NiFi has access to in HDFS or + the local filesystem. + + + + + INPUT_ALLOWED + + + org.apache.nifi.processors.hadoop.PutHDFS + org.apache.nifi.processors.hadoop.GetHDFS + + + + org.apache.nifi.processors.hadoop.GetHDFS + PROCESSOR + + Fetch files from Hadoop Distributed File System (HDFS) into FlowFiles. This Processor will + delete the file from HDFS after fetching it. + + + hadoop + HDFS + get + fetch + ingest + source + filesystem + + + + Hadoop Configuration Resources + Hadoop Configuration Resources + A file or comma separated list of files which contains the Hadoop file system + configuration. Without this, Hadoop will search the classpath for a 'core-site.xml' and + 'hdfs-site.xml' file or will revert to a default configuration. To use swebhdfs, see 'Additional + Details' section of PutHDFS's documentation. + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + kerberos-credentials-service + Kerberos Credentials Service + Specifies the Kerberos Credentials Controller Service that should be used for + authenticating with Kerberos + + + + org.apache.nifi.kerberos.KerberosCredentialsService + org.apache.nifi + org.apache.nifi:nifi-standard-services-api-nar:nar:1.10.0-SNAPSHOT + 1.10.0-SNAPSHOT + + + false + false + false + NONE + false + false + + + Kerberos Principal + Kerberos Principal + Kerberos principal to authenticate as. Requires nifi.kerberos.krb5.file to be set in + your nifi.properties + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + Kerberos Keytab + Kerberos Keytab + Kerberos keytab associated with the principal. Requires nifi.kerberos.krb5.file to be + set in your nifi.properties + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + Kerberos Relogin Period + Kerberos Relogin Period + Period of time which should pass before attempting a kerberos relogin. + + This property has been deprecated, and has no effect on processing. Relogins now occur + automatically. + + 4 hours + + false + false + true + VARIABLE_REGISTRY + false + false + + + Additional Classpath Resources + Additional Classpath Resources + A comma-separated list of paths to files and/or directories that will be added to the + classpath. When specifying a directory, all files with in the directory will be added to the + classpath, but further sub-directories will not be included. + + + + false + false + false + NONE + true + false + + + Directory + Directory + The HDFS directory from which files should be read + + + true + false + true + FLOWFILE_ATTRIBUTES + false + false + + + Recurse Subdirectories + Recurse Subdirectories + Indicates whether to pull files from subdirectories of the HDFS directory + true + + + true + true + + + + false + false + + + + true + false + false + NONE + false + false + + + Keep Source File + Keep Source File + Determines whether to delete the file from HDFS after it has been successfully + transferred. If true, the file will be fetched repeatedly. This is intended for testing only. + + false + + + true + true + + + + false + false + + + + true + false + false + NONE + false + false + + + File Filter Regex + File Filter Regex + A Java Regular Expression for filtering Filenames; if a filter is supplied then only + files whose names match that Regular Expression will be fetched, otherwise all files will be + fetched + + + + false + false + false + NONE + false + false + + + Filter Match Name Only + Filter Match Name Only + If true then File Filter Regex will match on just the filename, otherwise subdirectory + names will be included with filename in the regex comparison + + true + + + true + true + + + + false + false + + + + true + false + false + NONE + false + false + + + Ignore Dotted Files + Ignore Dotted Files + If true, files whose names begin with a dot (".") will be ignored + true + + + true + true + + + + false + false + + + + true + false + false + NONE + false + false + + + Minimum File Age + Minimum File Age + The minimum age that a file must be in order to be pulled; any file younger than this + amount of time (based on last modification date) will be ignored + + 0 sec + + true + false + false + NONE + false + false + + + Maximum File Age + Maximum File Age + The maximum age that a file must be in order to be pulled; any file older than this + amount of time (based on last modification date) will be ignored + + + + false + false + false + NONE + false + false + + + Polling Interval + Polling Interval + Indicates how long to wait between performing directory listings + 0 sec + + true + false + false + NONE + false + false + + + Batch Size + Batch Size + The maximum number of files to pull in each iteration, based on run schedule. + + 100 + + true + false + false + NONE + false + false + + + IO Buffer Size + IO Buffer Size + Amount of memory to use to buffer file contents during IO. This overrides the Hadoop + Configuration + + + + false + false + false + NONE + false + false + + + Compression codec + Compression codec + + NONE + + + NONE + NONE + No compression + + + DEFAULT + DEFAULT + Default ZLIB compression + + + BZIP + BZIP + BZIP compression + + + GZIP + GZIP + GZIP compression + + + LZ4 + LZ4 + LZ4 compression + + + LZO + LZO + LZO compression - it assumes LD_LIBRARY_PATH has been set and jar is + available + + + + SNAPPY + SNAPPY + Snappy compression + + + AUTOMATIC + AUTOMATIC + Will attempt to automatically detect the compression codec. + + + true + false + false + NONE + false + false + + + + + + success + All files retrieved from HDFS are transferred to this relationship + false + + + + + + + filename + The name of the file that was read from HDFS. + + + path + The path is set to the relative path of the file's directory on HDFS. For example, if + the Directory property is set to /tmp, then files picked up from /tmp will have the path + attribute set to "./". If the Recurse Subdirectories property is set to true and a file is + picked up from /tmp/abc/1/2/3, then the path attribute will be set to "abc/1/2/3". + + + + + + + + read filesystem + Provides operator the ability to retrieve any file that NiFi has access to in HDFS + or the local filesystem. + + + + write filesystem + Provides operator the ability to delete any file that NiFi has access to in HDFS or + the local filesystem. + + + + + INPUT_FORBIDDEN + + + org.apache.nifi.processors.hadoop.PutHDFS + org.apache.nifi.processors.hadoop.ListHDFS + + + + org.apache.nifi.processors.hadoop.PutHDFS + PROCESSOR + + Write FlowFile data to Hadoop Distributed File System (HDFS) + + hadoop + HDFS + put + copy + filesystem + + + + Hadoop Configuration Resources + Hadoop Configuration Resources + A file or comma separated list of files which contains the Hadoop file system + configuration. Without this, Hadoop will search the classpath for a 'core-site.xml' and + 'hdfs-site.xml' file or will revert to a default configuration. To use swebhdfs, see 'Additional + Details' section of PutHDFS's documentation. + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + kerberos-credentials-service + Kerberos Credentials Service + Specifies the Kerberos Credentials Controller Service that should be used for + authenticating with Kerberos + + + + org.apache.nifi.kerberos.KerberosCredentialsService + org.apache.nifi + org.apache.nifi:nifi-standard-services-api-nar:nar:1.10.0-SNAPSHOT + 1.10.0-SNAPSHOT + + + false + false + false + NONE + false + false + + + Kerberos Principal + Kerberos Principal + Kerberos principal to authenticate as. Requires nifi.kerberos.krb5.file to be set in + your nifi.properties + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + Kerberos Keytab + Kerberos Keytab + Kerberos keytab associated with the principal. Requires nifi.kerberos.krb5.file to be + set in your nifi.properties + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + Kerberos Relogin Period + Kerberos Relogin Period + Period of time which should pass before attempting a kerberos relogin. + + This property has been deprecated, and has no effect on processing. Relogins now occur + automatically. + + 4 hours + + false + false + true + VARIABLE_REGISTRY + false + false + + + Additional Classpath Resources + Additional Classpath Resources + A comma-separated list of paths to files and/or directories that will be added to the + classpath. When specifying a directory, all files with in the directory will be added to the + classpath, but further sub-directories will not be included. + + + + false + false + false + NONE + true + false + + + Directory + Directory + The parent HDFS directory to which files should be written. The directory will be + created if it doesn't exist. + + + + true + false + true + FLOWFILE_ATTRIBUTES + false + false + + + Conflict Resolution Strategy + Conflict Resolution Strategy + Indicates what should happen when a file with the same name already exists in the + output directory + + fail + + + replace + replace + Replaces the existing file if any. + + + ignore + ignore + Ignores the flow file and routes it to success. + + + fail + fail + Penalizes the flow file and routes it to failure. + + + append + append + Appends to the existing file if any, creates a new file otherwise. + + + + true + false + false + NONE + false + false + + + Block Size + Block Size + Size of each block as written to HDFS. This overrides the Hadoop Configuration + + + + false + false + false + NONE + false + false + + + IO Buffer Size + IO Buffer Size + Amount of memory to use to buffer file contents during IO. This overrides the Hadoop + Configuration + + + + false + false + false + NONE + false + false + + + Replication + Replication + Number of times that HDFS will replicate each file. This overrides the Hadoop + Configuration + + + + false + false + false + NONE + false + false + + + Permissions umask + Permissions umask + A umask represented as an octal number which determines the permissions of files + written to HDFS. This overrides the Hadoop property "fs.permission.umask-mode". If this property + and "fs.permission.umask-mode" are undefined, the Hadoop default "022" will be used. + + + + false + false + false + NONE + false + false + + + Remote Owner + Remote Owner + Changes the owner of the HDFS file to this value after it is written. This only works + if NiFi is running as a user that has HDFS super user privilege to change owner + + + + false + false + true + FLOWFILE_ATTRIBUTES + false + false + + + Remote Group + Remote Group + Changes the group of the HDFS file to this value after it is written. This only works + if NiFi is running as a user that has HDFS super user privilege to change group + + + + false + false + true + FLOWFILE_ATTRIBUTES + false + false + + + Compression codec + Compression codec + + NONE + + + NONE + NONE + No compression + + + DEFAULT + DEFAULT + Default ZLIB compression + + + BZIP + BZIP + BZIP compression + + + GZIP + GZIP + GZIP compression + + + LZ4 + LZ4 + LZ4 compression + + + LZO + LZO + LZO compression - it assumes LD_LIBRARY_PATH has been set and jar is + available + + + + SNAPPY + SNAPPY + Snappy compression + + + AUTOMATIC + AUTOMATIC + Will attempt to automatically detect the compression codec. + + + true + false + false + NONE + false + false + + + + + + success + Files that have been successfully written to HDFS are transferred to this + relationship + + false + + + failure + Files that could not be written to HDFS for some reason are transferred to this + relationship + + false + + + + + + filename + The name of the file written to HDFS comes from the value of this attribute. + + + + + + filename + The name of the file written to HDFS is stored in this attribute. + + + absolute.hdfs.path + The absolute path to the file on HDFS is stored in this attribute. + + + + + + + write filesystem + Provides operator the ability to delete any file that NiFi has access to in HDFS or + the local filesystem. + + + + + INPUT_REQUIRED + + + org.apache.nifi.processors.hadoop.GetHDFS + + + + org.apache.nifi.processors.hadoop.inotify.GetHDFSEvents + PROCESSOR + + This processor polls the notification events provided by the HdfsAdmin API. Since this uses the + HdfsAdmin APIs it is required to run as an HDFS super user. Currently there are six types of events + (append, close, create, metadata, rename, and unlink). Please see org.apache.hadoop.hdfs.inotify.Event + documentation for full explanations of each event. This processor will poll for new events based on a + defined duration. For each event received a new flow file will be created with the expected attributes + and the event itself serialized to JSON and written to the flow file's content. For example, if + event.type is APPEND then the content of the flow file will contain a JSON file containing the + information about the append event. If successful the flow files are sent to the 'success' relationship. + Be careful of where the generated flow files are stored. If the flow files are stored in one of + processor's watch directories there will be a never ending flow of events. It is also important to be + aware that this processor must consume all events. The filtering must happen within the processor. This + is because the HDFS admin's event notifications API does not have filtering. + + + hadoop + events + inotify + notifications + filesystem + + + + Hadoop Configuration Resources + Hadoop Configuration Resources + A file or comma separated list of files which contains the Hadoop file system + configuration. Without this, Hadoop will search the classpath for a 'core-site.xml' and + 'hdfs-site.xml' file or will revert to a default configuration. To use swebhdfs, see 'Additional + Details' section of PutHDFS's documentation. + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + kerberos-credentials-service + Kerberos Credentials Service + Specifies the Kerberos Credentials Controller Service that should be used for + authenticating with Kerberos + + + + org.apache.nifi.kerberos.KerberosCredentialsService + org.apache.nifi + org.apache.nifi:nifi-standard-services-api-nar:nar:1.10.0-SNAPSHOT + 1.10.0-SNAPSHOT + + + false + false + false + NONE + false + false + + + Kerberos Principal + Kerberos Principal + Kerberos principal to authenticate as. Requires nifi.kerberos.krb5.file to be set in + your nifi.properties + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + Kerberos Keytab + Kerberos Keytab + Kerberos keytab associated with the principal. Requires nifi.kerberos.krb5.file to be + set in your nifi.properties + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + Kerberos Relogin Period + Kerberos Relogin Period + Period of time which should pass before attempting a kerberos relogin. + + This property has been deprecated, and has no effect on processing. Relogins now occur + automatically. + + 4 hours + + false + false + true + VARIABLE_REGISTRY + false + false + + + Additional Classpath Resources + Additional Classpath Resources + A comma-separated list of paths to files and/or directories that will be added to the + classpath. When specifying a directory, all files with in the directory will be added to the + classpath, but further sub-directories will not be included. + + + + false + false + false + NONE + true + false + + + Poll Duration + Poll Duration + The time before the polling method returns with the next batch of events if they exist. + It may exceed this amount of time by up to the time required for an RPC to the NameNode. + + 1 second + + true + false + false + NONE + false + false + + + HDFS Path to Watch + HDFS Path to Watch + The HDFS path to get event notifications for. This property accepts both expression + language and regular expressions. This will be evaluated during the OnScheduled phase. + + + + true + false + true + VARIABLE_REGISTRY + false + false + + + Ignore Hidden Files + Ignore Hidden Files + If true and the final component of the path associated with a given event starts with a + '.' then that event will not be processed. + + false + + + true + true + + + + false + false + + + + true + false + false + NONE + false + false + + + Event Types to Filter On + Event Types to Filter On + A comma-separated list of event types to process. Valid event types are: append, close, + create, metadata, rename, and unlink. Case does not matter. + + append, close, create, metadata, rename, unlink + + true + false + false + NONE + false + false + + + IOException Retries During Event Polling + IOException Retries During Event Polling + According to the HDFS admin API for event polling it is good to retry at least a few + times. This number defines how many times the poll will be retried if it throws an IOException. + + 3 + + true + false + false + NONE + false + false + + + + + + success + A flow file with updated information about a specific event will be sent to this + relationship. + + false + + + + + + + mime.type + This is always application/json. + + + hdfs.inotify.event.type + This will specify the specific HDFS notification event type. Currently there are six + types of events (append, close, create, metadata, rename, and unlink). + + + + hdfs.inotify.event.path + The specific path that the event is tied to. + + + + The last used transaction id is stored. This is used + + CLUSTER + + + + INPUT_FORBIDDEN + + + org.apache.nifi.processors.hadoop.GetHDFS + org.apache.nifi.processors.hadoop.FetchHDFS + org.apache.nifi.processors.hadoop.PutHDFS + org.apache.nifi.processors.hadoop.ListHDFS + + + + org.apache.nifi.processors.hadoop.CreateHadoopSequenceFile + PROCESSOR + + Creates Hadoop Sequence Files from incoming flow files + + hadoop + sequence file + create + sequencefile + + + + Hadoop Configuration Resources + Hadoop Configuration Resources + A file or comma separated list of files which contains the Hadoop file system + configuration. Without this, Hadoop will search the classpath for a 'core-site.xml' and + 'hdfs-site.xml' file or will revert to a default configuration. To use swebhdfs, see 'Additional + Details' section of PutHDFS's documentation. + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + kerberos-credentials-service + Kerberos Credentials Service + Specifies the Kerberos Credentials Controller Service that should be used for + authenticating with Kerberos + + + + org.apache.nifi.kerberos.KerberosCredentialsService + org.apache.nifi + org.apache.nifi:nifi-standard-services-api-nar:nar:1.10.0-SNAPSHOT + 1.10.0-SNAPSHOT + + + false + false + false + NONE + false + false + + + Kerberos Principal + Kerberos Principal + Kerberos principal to authenticate as. Requires nifi.kerberos.krb5.file to be set in + your nifi.properties + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + Kerberos Keytab + Kerberos Keytab + Kerberos keytab associated with the principal. Requires nifi.kerberos.krb5.file to be + set in your nifi.properties + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + Kerberos Relogin Period + Kerberos Relogin Period + Period of time which should pass before attempting a kerberos relogin. + + This property has been deprecated, and has no effect on processing. Relogins now occur + automatically. + + 4 hours + + false + false + true + VARIABLE_REGISTRY + false + false + + + Additional Classpath Resources + Additional Classpath Resources + A comma-separated list of paths to files and/or directories that will be added to the + classpath. When specifying a directory, all files with in the directory will be added to the + classpath, but further sub-directories will not be included. + + + + false + false + false + NONE + true + false + + + compression type + Compression type + Type of compression to use when creating Sequence File + + + + NONE + NONE + + + + RECORD + RECORD + + + + BLOCK + BLOCK + + + + false + false + false + NONE + false + false + + + Compression codec + Compression codec + + NONE + + + NONE + NONE + No compression + + + DEFAULT + DEFAULT + Default ZLIB compression + + + BZIP + BZIP + BZIP compression + + + GZIP + GZIP + GZIP compression + + + LZ4 + LZ4 + LZ4 compression + + + LZO + LZO + LZO compression - it assumes LD_LIBRARY_PATH has been set and jar is + available + + + + SNAPPY + SNAPPY + Snappy compression + + + AUTOMATIC + AUTOMATIC + Will attempt to automatically detect the compression codec. + + + true + false + false + NONE + false + false + + + + + + success + Generated Sequence Files are sent to this relationship + false + + + failure + Incoming files that failed to generate a Sequence File are sent to this relationship + + false + + + + + + + + INPUT_REQUIRED + + + org.apache.nifi.processors.hadoop.PutHDFS + + + + org.apache.nifi.processors.hadoop.FetchHDFS + PROCESSOR + + Retrieves a file from HDFS. The content of the incoming FlowFile is replaced by the content of + the file in HDFS. The file in HDFS is left intact without any changes being made to it. + + + hadoop + hdfs + get + ingest + fetch + source + + + + Hadoop Configuration Resources + Hadoop Configuration Resources + A file or comma separated list of files which contains the Hadoop file system + configuration. Without this, Hadoop will search the classpath for a 'core-site.xml' and + 'hdfs-site.xml' file or will revert to a default configuration. To use swebhdfs, see 'Additional + Details' section of PutHDFS's documentation. + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + kerberos-credentials-service + Kerberos Credentials Service + Specifies the Kerberos Credentials Controller Service that should be used for + authenticating with Kerberos + + + + org.apache.nifi.kerberos.KerberosCredentialsService + org.apache.nifi + org.apache.nifi:nifi-standard-services-api-nar:nar:1.10.0-SNAPSHOT + 1.10.0-SNAPSHOT + + + false + false + false + NONE + false + false + + + Kerberos Principal + Kerberos Principal + Kerberos principal to authenticate as. Requires nifi.kerberos.krb5.file to be set in + your nifi.properties + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + Kerberos Keytab + Kerberos Keytab + Kerberos keytab associated with the principal. Requires nifi.kerberos.krb5.file to be + set in your nifi.properties + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + Kerberos Relogin Period + Kerberos Relogin Period + Period of time which should pass before attempting a kerberos relogin. + + This property has been deprecated, and has no effect on processing. Relogins now occur + automatically. + + 4 hours + + false + false + true + VARIABLE_REGISTRY + false + false + + + Additional Classpath Resources + Additional Classpath Resources + A comma-separated list of paths to files and/or directories that will be added to the + classpath. When specifying a directory, all files with in the directory will be added to the + classpath, but further sub-directories will not be included. + + + + false + false + false + NONE + true + false + + + HDFS Filename + HDFS Filename + The name of the HDFS file to retrieve + ${path}/${filename} + + true + false + true + FLOWFILE_ATTRIBUTES + false + false + + + Compression codec + Compression codec + + NONE + + + NONE + NONE + No compression + + + DEFAULT + DEFAULT + Default ZLIB compression + + + BZIP + BZIP + BZIP compression + + + GZIP + GZIP + GZIP compression + + + LZ4 + LZ4 + LZ4 compression + + + LZO + LZO + LZO compression - it assumes LD_LIBRARY_PATH has been set and jar is + available + + + + SNAPPY + SNAPPY + Snappy compression + + + AUTOMATIC + AUTOMATIC + Will attempt to automatically detect the compression codec. + + + true + false + false + NONE + false + false + + + + + + success + FlowFiles will be routed to this relationship once they have been updated with the + content of the HDFS file + + false + + + comms.failure + FlowFiles will be routed to this relationship if the content of the HDFS file cannot be + retrieve due to a communications failure. This generally indicates that the Fetch should be + tried again. + + false + + + failure + FlowFiles will be routed to this relationship if the content of the HDFS file cannot be + retrieved and trying again will likely not be helpful. This would occur, for instance, if the + file is not found or if there is a permissions issue + + false + + + + + + + hdfs.failure.reason + When a FlowFile is routed to 'failure', this attribute is added indicating why the file + could not be fetched from HDFS + + + + + + + + read filesystem + Provides operator the ability to retrieve any file that NiFi has access to in HDFS + or the local filesystem. + + + + + INPUT_REQUIRED + + + org.apache.nifi.processors.hadoop.ListHDFS + org.apache.nifi.processors.hadoop.GetHDFS + org.apache.nifi.processors.hadoop.PutHDFS + + + + org.apache.nifi.processors.hadoop.DeleteHDFS + PROCESSOR + + Deletes one or more files or directories from HDFS. The path can be provided as an attribute + from an incoming FlowFile, or a statically set path that is periodically removed. If this processor has + an incoming connection, itwill ignore running on a periodic basis and instead rely on incoming FlowFiles + to trigger a delete. Note that you may use a wildcard character to match multiple files or directories. + If there are no incoming connections no flowfiles will be transfered to any output relationships. If + there is an incoming flowfile then provided there are no detected failures it will be transferred to + success otherwise it will be sent to false. If knowledge of globbed files deleted is necessary use + ListHDFS first to produce a specific list of files to delete. + + + hadoop + HDFS + delete + remove + filesystem + + + + Hadoop Configuration Resources + Hadoop Configuration Resources + A file or comma separated list of files which contains the Hadoop file system + configuration. Without this, Hadoop will search the classpath for a 'core-site.xml' and + 'hdfs-site.xml' file or will revert to a default configuration. To use swebhdfs, see 'Additional + Details' section of PutHDFS's documentation. + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + kerberos-credentials-service + Kerberos Credentials Service + Specifies the Kerberos Credentials Controller Service that should be used for + authenticating with Kerberos + + + + org.apache.nifi.kerberos.KerberosCredentialsService + org.apache.nifi + org.apache.nifi:nifi-standard-services-api-nar:nar:1.10.0-SNAPSHOT + 1.10.0-SNAPSHOT + + + false + false + false + NONE + false + false + + + Kerberos Principal + Kerberos Principal + Kerberos principal to authenticate as. Requires nifi.kerberos.krb5.file to be set in + your nifi.properties + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + Kerberos Keytab + Kerberos Keytab + Kerberos keytab associated with the principal. Requires nifi.kerberos.krb5.file to be + set in your nifi.properties + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + Kerberos Relogin Period + Kerberos Relogin Period + Period of time which should pass before attempting a kerberos relogin. + + This property has been deprecated, and has no effect on processing. Relogins now occur + automatically. + + 4 hours + + false + false + true + VARIABLE_REGISTRY + false + false + + + Additional Classpath Resources + Additional Classpath Resources + A comma-separated list of paths to files and/or directories that will be added to the + classpath. When specifying a directory, all files with in the directory will be added to the + classpath, but further sub-directories will not be included. + + + + false + false + false + NONE + true + false + + + file_or_directory + Path + The HDFS file or directory to delete. A wildcard expression may be used to only delete + certain files + + + + true + false + true + FLOWFILE_ATTRIBUTES + false + false + + + recursive + Recursive + Remove contents of a non-empty directory recursively + true + + + true + true + + + + false + false + + + + true + false + false + NONE + false + false + + + + + + success + When an incoming flowfile is used then if there are no errors invoking delete the + flowfile will route here. + + false + + + failure + When an incoming flowfile is used and there is a failure while deleting then the + flowfile will route here. + + false + + + + + + + hdfs.filename + HDFS file to be deleted. If multiple files are deleted, then only the last filename is + set. + + + + hdfs.path + HDFS Path specified in the delete request. If multiple paths are deleted, then only the + last path is set. + + + + hdfs.error.message + HDFS error message related to the hdfs.error.code + + + + + + + write filesystem + Provides operator the ability to delete any file that NiFi has access to in HDFS or + the local filesystem. + + + + + INPUT_ALLOWED + + + org.apache.nifi.processors.hadoop.ListHDFS + org.apache.nifi.processors.hadoop.PutHDFS + + + + org.apache.nifi.processors.hadoop.GetHDFSSequenceFile + PROCESSOR + + Fetch sequence files from Hadoop Distributed File System (HDFS) into FlowFiles + + hadoop + HDFS + get + fetch + ingest + source + sequence file + + + + Hadoop Configuration Resources + Hadoop Configuration Resources + A file or comma separated list of files which contains the Hadoop file system + configuration. Without this, Hadoop will search the classpath for a 'core-site.xml' and + 'hdfs-site.xml' file or will revert to a default configuration. To use swebhdfs, see 'Additional + Details' section of PutHDFS's documentation. + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + kerberos-credentials-service + Kerberos Credentials Service + Specifies the Kerberos Credentials Controller Service that should be used for + authenticating with Kerberos + + + + org.apache.nifi.kerberos.KerberosCredentialsService + org.apache.nifi + org.apache.nifi:nifi-standard-services-api-nar:nar:1.10.0-SNAPSHOT + 1.10.0-SNAPSHOT + + + false + false + false + NONE + false + false + + + Kerberos Principal + Kerberos Principal + Kerberos principal to authenticate as. Requires nifi.kerberos.krb5.file to be set in + your nifi.properties + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + Kerberos Keytab + Kerberos Keytab + Kerberos keytab associated with the principal. Requires nifi.kerberos.krb5.file to be + set in your nifi.properties + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + Kerberos Relogin Period + Kerberos Relogin Period + Period of time which should pass before attempting a kerberos relogin. + + This property has been deprecated, and has no effect on processing. Relogins now occur + automatically. + + 4 hours + + false + false + true + VARIABLE_REGISTRY + false + false + + + Additional Classpath Resources + Additional Classpath Resources + A comma-separated list of paths to files and/or directories that will be added to the + classpath. When specifying a directory, all files with in the directory will be added to the + classpath, but further sub-directories will not be included. + + + + false + false + false + NONE + true + false + + + Directory + Directory + The HDFS directory from which files should be read + + + true + false + true + FLOWFILE_ATTRIBUTES + false + false + + + Recurse Subdirectories + Recurse Subdirectories + Indicates whether to pull files from subdirectories of the HDFS directory + true + + + true + true + + + + false + false + + + + true + false + false + NONE + false + false + + + Keep Source File + Keep Source File + Determines whether to delete the file from HDFS after it has been successfully + transferred. If true, the file will be fetched repeatedly. This is intended for testing only. + + false + + + true + true + + + + false + false + + + + true + false + false + NONE + false + false + + + File Filter Regex + File Filter Regex + A Java Regular Expression for filtering Filenames; if a filter is supplied then only + files whose names match that Regular Expression will be fetched, otherwise all files will be + fetched + + + + false + false + false + NONE + false + false + + + Filter Match Name Only + Filter Match Name Only + If true then File Filter Regex will match on just the filename, otherwise subdirectory + names will be included with filename in the regex comparison + + true + + + true + true + + + + false + false + + + + true + false + false + NONE + false + false + + + Ignore Dotted Files + Ignore Dotted Files + If true, files whose names begin with a dot (".") will be ignored + true + + + true + true + + + + false + false + + + + true + false + false + NONE + false + false + + + Minimum File Age + Minimum File Age + The minimum age that a file must be in order to be pulled; any file younger than this + amount of time (based on last modification date) will be ignored + + 0 sec + + true + false + false + NONE + false + false + + + Maximum File Age + Maximum File Age + The maximum age that a file must be in order to be pulled; any file older than this + amount of time (based on last modification date) will be ignored + + + + false + false + false + NONE + false + false + + + Polling Interval + Polling Interval + Indicates how long to wait between performing directory listings + 0 sec + + true + false + false + NONE + false + false + + + Batch Size + Batch Size + The maximum number of files to pull in each iteration, based on run schedule. + + 100 + + true + false + false + NONE + false + false + + + IO Buffer Size + IO Buffer Size + Amount of memory to use to buffer file contents during IO. This overrides the Hadoop + Configuration + + + + false + false + false + NONE + false + false + + + Compression codec + Compression codec + + NONE + + + NONE + NONE + No compression + + + DEFAULT + DEFAULT + Default ZLIB compression + + + BZIP + BZIP + BZIP compression + + + GZIP + GZIP + GZIP compression + + + LZ4 + LZ4 + LZ4 compression + + + LZO + LZO + LZO compression - it assumes LD_LIBRARY_PATH has been set and jar is + available + + + + SNAPPY + SNAPPY + Snappy compression + + + AUTOMATIC + AUTOMATIC + Will attempt to automatically detect the compression codec. + + + true + false + false + NONE + false + false + + + FlowFile Content + FlowFile Content + Indicate if the content is to be both the key and value of the Sequence File, or just + the value. + + VALUE ONLY + + + VALUE ONLY + VALUE ONLY + + + + KEY VALUE PAIR + KEY VALUE PAIR + + + + true + false + false + NONE + false + false + + + + + + success + All files retrieved from HDFS are transferred to this relationship + false + + + + + + + filename + The name of the file that was read from HDFS. + + + path + The path is set to the relative path of the file's directory on HDFS. For example, if + the Directory property is set to /tmp, then files picked up from /tmp will have the path + attribute set to "./". If the Recurse Subdirectories property is set to true and a file is + picked up from /tmp/abc/1/2/3, then the path attribute will be set to "abc/1/2/3". + + + + + + + + read filesystem + Provides operator the ability to retrieve any file that NiFi has access to in HDFS + or the local filesystem. + + + + write filesystem + Provides operator the ability to delete any file that NiFi has access to in HDFS or + the local filesystem. + + + + + INPUT_FORBIDDEN + + + org.apache.nifi.processors.hadoop.PutHDFS + + + + org.apache.nifi.processors.hadoop.GetHDFSFileInfo + PROCESSOR + + Retrieves a listing of files and directories from HDFS. This processor creates a FlowFile(s) + that represents the HDFS file/dir with relevant information. Main purpose of this processor to provide + functionality similar to HDFS Client, i.e. count, du, ls, test, etc. Unlike ListHDFS, this processor is + stateless, supports incoming connections and provides information on a dir level. + + + hadoop + HDFS + get + list + ingest + source + filesystem + + + + Hadoop Configuration Resources + Hadoop Configuration Resources + A file or comma separated list of files which contains the Hadoop file system + configuration. Without this, Hadoop will search the classpath for a 'core-site.xml' and + 'hdfs-site.xml' file or will revert to a default configuration. To use swebhdfs, see 'Additional + Details' section of PutHDFS's documentation. + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + kerberos-credentials-service + Kerberos Credentials Service + Specifies the Kerberos Credentials Controller Service that should be used for + authenticating with Kerberos + + + + org.apache.nifi.kerberos.KerberosCredentialsService + org.apache.nifi + org.apache.nifi:nifi-standard-services-api-nar:nar:1.10.0-SNAPSHOT + 1.10.0-SNAPSHOT + + + false + false + false + NONE + false + false + + + Kerberos Principal + Kerberos Principal + Kerberos principal to authenticate as. Requires nifi.kerberos.krb5.file to be set in + your nifi.properties + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + Kerberos Keytab + Kerberos Keytab + Kerberos keytab associated with the principal. Requires nifi.kerberos.krb5.file to be + set in your nifi.properties + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + Kerberos Relogin Period + Kerberos Relogin Period + Period of time which should pass before attempting a kerberos relogin. + + This property has been deprecated, and has no effect on processing. Relogins now occur + automatically. + + 4 hours + + false + false + true + VARIABLE_REGISTRY + false + false + + + Additional Classpath Resources + Additional Classpath Resources + A comma-separated list of paths to files and/or directories that will be added to the + classpath. When specifying a directory, all files with in the directory will be added to the + classpath, but further sub-directories will not be included. + + + + false + false + false + NONE + true + false + + + gethdfsfileinfo-full-path + Full path + A directory to start listing from, or a file's full path. + + + true + false + true + FLOWFILE_ATTRIBUTES + false + false + + + gethdfsfileinfo-recurse-subdirs + Recurse Subdirectories + Indicates whether to list files from subdirectories of the HDFS directory + true + + + true + true + + + + false + false + + + + true + false + false + NONE + false + false + + + gethdfsfileinfo-dir-filter + Directory Filter + Regex. Only directories whose names match the given regular expression will be picked + up. If not provided, any filter would be apply (performance considerations). + + + + false + false + true + FLOWFILE_ATTRIBUTES + false + false + + + gethdfsfileinfo-file-filter + File Filter + Regex. Only files whose names match the given regular expression will be picked up. If + not provided, any filter would be apply (performance considerations). + + + + false + false + true + FLOWFILE_ATTRIBUTES + false + false + + + gethdfsfileinfo-file-exclude-filter + Exclude Files + Regex. Files whose names match the given regular expression will not be picked up. If + not provided, any filter won't be apply (performance considerations). + + + + false + false + true + FLOWFILE_ATTRIBUTES + false + false + + + gethdfsfileinfo-ignore-dotted-dirs + Ignore Dotted Directories + If true, directories whose names begin with a dot (".") will be ignored + true + + + true + true + + + + false + false + + + + true + false + false + NONE + false + false + + + gethdfsfileinfo-ignore-dotted-files + Ignore Dotted Files + If true, files whose names begin with a dot (".") will be ignored + true + + + true + true + + + + false + false + + + + true + false + false + NONE + false + false + + + gethdfsfileinfo-group + Group Results + Groups HDFS objects + gethdfsfileinfo-group-all + + + All + gethdfsfileinfo-group-all + Group all results into a single flowfile. + + + Parent Directory + gethdfsfileinfo-group-parent-dir + Group HDFS objects by their parent directories only. Processor will generate + flowfile for each directory (if recursive). If 'Recurse Subdirectories' property set to + 'false', then will have the same effect as 'All' + + + + None + gethdfsfileinfo-group-none + Don't group results. Generate flowfile per each HDFS object. + + + true + false + false + NONE + false + false + + + gethdfsfileinfo-destination + Destination + Sets the destination for the resutls. When set to 'Content', attributes of flowfile + won't be used for storing results. + + gethdfsfileinfo-dest-content + + + Attributes + gethdfsfileinfo-dest-attr + Details of given HDFS object will be stored in attributes of flowfile. WARNING: + In case when scan finds thousands or millions of objects, having huge values in + attribute could impact flow file repo and GC/heap usage. Use content destination for + such cases. + + + + Content + gethdfsfileinfo-dest-content + Details of given HDFS object will be stored in a content in JSON format + + + + true + false + false + NONE + false + false + + + + + + success + All successfully generated FlowFiles are transferred to this relationship + false + + + not found + If no objects are found, original FlowFile are transferred to this relationship + + false + + + failure + All failed attempts to access HDFS will be routed to this relationship + false + + + original + Original FlowFiles are transferred to this relationship + false + + + + + + + hdfs.objectName + The name of the file/dir found on HDFS. + + + hdfs.path + The path is set to the absolute path of the object's parent directory on HDFS. For + example, if an object is a directory 'foo', under directory '/bar' then 'hdfs.objectName' will + have value 'foo', and 'hdfs.path' will be '/bar' + + + + hdfs.type + The type of an object. Possible values: directory, file, link + + + hdfs.owner + The user that owns the object in HDFS + + + hdfs.group + The group that owns the object in HDFS + + + hdfs.lastModified + The timestamp of when the object in HDFS was last modified, as milliseconds since + midnight Jan 1, 1970 UTC + + + + hdfs.length + In case of files: The number of bytes in the file in HDFS. In case of dirs: Retuns + storage space consumed by directory. + + + + hdfs.count.files + In case of type='directory' will represent total count of files under this dir. Won't + be populated to other types of HDFS objects. + + + + hdfs.count.dirs + In case of type='directory' will represent total count of directories under this dir + (including itself). Won't be populated to other types of HDFS objects. + + + + hdfs.replication + The number of HDFS replicas for the file + + + hdfs.permissions + The permissions for the object in HDFS. This is formatted as 3 characters for the + owner, 3 for the group, and 3 for other users. For example rw-rw-r-- + + + + hdfs.status + The status contains comma separated list of file/dir paths, which couldn't be + listed/accessed. Status won't be set if no errors occured. + + + + hdfs.full.tree + When destination is 'attribute', will be populated with full tree of HDFS directory in + JSON format.WARNING: In case when scan finds thousands or millions of objects, having huge + values in attribute could impact flow file repo and GC/heap usage. Use content destination for + such cases + + + + + + INPUT_ALLOWED + + + org.apache.nifi.processors.hadoop.ListHDFS + org.apache.nifi.processors.hadoop.GetHDFS + org.apache.nifi.processors.hadoop.FetchHDFS + org.apache.nifi.processors.hadoop.PutHDFS + + + + org.apache.nifi.processors.hadoop.ListHDFS + PROCESSOR + + Retrieves a listing of files from HDFS. Each time a listing is performed, the files with the + latest timestamp will be excluded and picked up during the next execution of the processor. This is done + to ensure that we do not miss any files, or produce duplicates, in the cases where files with the same + timestamp are written immediately before and after a single execution of the processor. For each file + that is listed in HDFS, this processor creates a FlowFile that represents the HDFS file to be fetched in + conjunction with FetchHDFS. This Processor is designed to run on Primary Node only in a cluster. If the + primary node changes, the new Primary Node will pick up where the previous node left off without + duplicating all of the data. Unlike GetHDFS, this Processor does not delete any data from HDFS. + + + hadoop + HDFS + get + list + ingest + source + filesystem + + + + Hadoop Configuration Resources + Hadoop Configuration Resources + A file or comma separated list of files which contains the Hadoop file system + configuration. Without this, Hadoop will search the classpath for a 'core-site.xml' and + 'hdfs-site.xml' file or will revert to a default configuration. To use swebhdfs, see 'Additional + Details' section of PutHDFS's documentation. + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + kerberos-credentials-service + Kerberos Credentials Service + Specifies the Kerberos Credentials Controller Service that should be used for + authenticating with Kerberos + + + + org.apache.nifi.kerberos.KerberosCredentialsService + org.apache.nifi + org.apache.nifi:nifi-standard-services-api-nar:nar:1.10.0-SNAPSHOT + 1.10.0-SNAPSHOT + + + false + false + false + NONE + false + false + + + Kerberos Principal + Kerberos Principal + Kerberos principal to authenticate as. Requires nifi.kerberos.krb5.file to be set in + your nifi.properties + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + Kerberos Keytab + Kerberos Keytab + Kerberos keytab associated with the principal. Requires nifi.kerberos.krb5.file to be + set in your nifi.properties + + + + false + false + true + VARIABLE_REGISTRY + false + false + + + Kerberos Relogin Period + Kerberos Relogin Period + Period of time which should pass before attempting a kerberos relogin. + + This property has been deprecated, and has no effect on processing. Relogins now occur + automatically. + + 4 hours + + false + false + true + VARIABLE_REGISTRY + false + false + + + Additional Classpath Resources + Additional Classpath Resources + A comma-separated list of paths to files and/or directories that will be added to the + classpath. When specifying a directory, all files with in the directory will be added to the + classpath, but further sub-directories will not be included. + + + + false + false + false + NONE + true + false + + + Distributed Cache Service + Distributed Cache Service + This property is ignored. State will be stored in the LOCAL or CLUSTER scope by the + State Manager based on NiFi's configuration. + + + + org.apache.nifi.distributed.cache.client.DistributedMapCacheClient + org.apache.nifi + org.apache.nifi:nifi-standard-services-api-nar:nar:1.10.0-SNAPSHOT + 1.10.0-SNAPSHOT + + + false + false + false + NONE + false + false + + + Directory + Directory + The HDFS directory from which files should be read + + + true + false + true + FLOWFILE_ATTRIBUTES + false + false + + + Recurse Subdirectories + Recurse Subdirectories + Indicates whether to list files from subdirectories of the HDFS directory + true + + + true + true + + + + false + false + + + + true + false + false + NONE + false + false + + + File Filter + File Filter + Only files whose names match the given regular expression will be picked up + + [^\.].* + + true + false + false + NONE + false + false + + + file-filter-mode + File Filter Mode + Determines how the regular expression in File Filter will be used when retrieving + listings. + + filter-mode-directories-and-files + + + Directories and Files + filter-mode-directories-and-files + Filtering will be applied to the names of directories and files. If Recurse + Subdirectories is set to true, only subdirectories with a matching name will be searched + for files that match the regular expression defined in File Filter. + + + + Files Only + filter-mode-files-only + Filtering will only be applied to the names of files. If Recurse Subdirectories + is set to true, the entire subdirectory tree will be searched for files that match the + regular expression defined in File Filter. + + + + Full Path + filter-mode-full-path + Filtering will be applied to the full path of files. If Recurse Subdirectories + is set to true, the entire subdirectory tree will be searched for files in which the + full path of the file matches the regular expression defined in File Filter. + + + + true + false + false + NONE + false + false + + + minimum-file-age + Minimum File Age + The minimum age that a file must be in order to be pulled; any file younger than this + amount of time (based on last modification date) will be ignored + + + + false + false + false + NONE + false + false + + + maximum-file-age + Maximum File Age + The maximum age that a file must be in order to be pulled; any file older than this + amount of time (based on last modification date) will be ignored. Minimum value is 100ms. + + + + false + false + false + NONE + false + false + + + + + + success + All FlowFiles are transferred to this relationship + false + + + + + + + filename + The name of the file that was read from HDFS. + + + path + The path is set to the absolute path of the file's directory on HDFS. For example, if + the Directory property is set to /tmp, then files picked up from /tmp will have the path + attribute set to "./". If the Recurse Subdirectories property is set to true and a file is + picked up from /tmp/abc/1/2/3, then the path attribute will be set to "/tmp/abc/1/2/3". + + + + hdfs.owner + The user that owns the file in HDFS + + + hdfs.group + The group that owns the file in HDFS + + + hdfs.lastModified + The timestamp of when the file in HDFS was last modified, as milliseconds since + midnight Jan 1, 1970 UTC + + + + hdfs.length + The number of bytes in the file in HDFS + + + hdfs.replication + The number of HDFS replicas for hte file + + + hdfs.permissions + The permissions for the file in HDFS. This is formatted as 3 characters for the owner, + 3 for the group, and 3 for other users. For example rw-rw-r-- + + + + + After performing a listing of HDFS files, the latest timestamp of all the files listed and + the latest timestamp of all the files transferred are both stored. This allows the Processor to list + only files that have been added or modified after this date the next time that the Processor is run, + without having to store all of the actual filenames/paths which could lead to performance problems. + State is stored across the cluster so that this Processor can be run on Primary Node only and if a + new Primary Node is selected, the new node can pick up where the previous node left off, without + duplicating the data. + + + CLUSTER + + + + INPUT_FORBIDDEN + + + org.apache.nifi.processors.hadoop.GetHDFS + org.apache.nifi.processors.hadoop.FetchHDFS + org.apache.nifi.processors.hadoop.PutHDFS + + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/descriptors/extension-manifest-missing-sys-api.xml b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/descriptors/extension-manifest-missing-sys-api.xml new file mode 100644 index 0000000000..d49069089c --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/descriptors/extension-manifest-missing-sys-api.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/descriptors/extension-manifest-test-components.xml b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/descriptors/extension-manifest-test-components.xml new file mode 100644 index 0000000000..7783e1d645 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/descriptors/extension-manifest-test-components.xml @@ -0,0 +1,56 @@ + + 1.8.0 + + + org.apache.nifi.processors.TestProcessor1 + PROCESSOR + Test processor 1. + + test + processor + + + + org.apache.nifi.processors.TestProcessor2 + PROCESSOR + Test processor 2. + + test + processor + + + + + write filesystem + Test explanation. + + + + + + org.apache.nifi.processors.TestProcessor3 + PROCESSOR + + + + + + org.apache.nifi.service.TestServiceImpl + CONTROLLER_SERVICE + + Test service. + + test + service + + + + org.apache.nifi.service.TestService + org.apache.nifi + nifi-test-service-api-nar + 1.0.0 + + + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/nars/nifi-foo-nar-missing-extension-descriptor.nar b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/nars/nifi-foo-nar-missing-extension-descriptor.nar new file mode 100644 index 0000000000..def9f8af3f Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/nars/nifi-foo-nar-missing-extension-descriptor.nar differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/nars/nifi-foo-nar.nar b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/nars/nifi-foo-nar.nar new file mode 100644 index 0000000000..f61fca603b Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/nars/nifi-foo-nar.nar differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/nars/nifi-framework-nar.nar b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/nars/nifi-framework-nar.nar new file mode 100644 index 0000000000..3089458595 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/nars/nifi-framework-nar.nar differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/nars/nifi-hadoop-nar.nar b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/nars/nifi-hadoop-nar.nar new file mode 100644 index 0000000000..bf0020d3b3 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/nars/nifi-hadoop-nar.nar differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/nars/nifi-missing-manifest-entries.nar b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/nars/nifi-missing-manifest-entries.nar new file mode 100644 index 0000000000..22b8d12ee7 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/nars/nifi-missing-manifest-entries.nar differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/nars/nifi-missing-manifest.nar b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/nars/nifi-missing-manifest.nar new file mode 100644 index 0000000000..bc930c8eb4 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/nars/nifi-missing-manifest.nar differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/pom.xml b/nifi-registry/nifi-registry-core/nifi-registry-client/pom.xml new file mode 100644 index 0000000000..973dea9e8d --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/pom.xml @@ -0,0 +1,66 @@ + + + + 4.0.0 + + org.apache.nifi.registry + nifi-registry-core + 1.14.0-SNAPSHOT + + + nifi-registry-client + jar + + + + org.apache.nifi.registry + nifi-registry-data-model + 1.14.0-SNAPSHOT + + + org.apache.nifi.registry + nifi-registry-security-utils + 1.14.0-SNAPSHOT + + + org.glassfish.jersey.core + jersey-client + ${jersey.client.version} + + + org.glassfish.jersey.media + jersey-media-json-jackson + ${jersey.client.version} + + + org.glassfish.jersey.inject + jersey-hk2 + ${jersey.client.version} + + + org.glassfish.jersey.core + jersey-common + ${jersey.client.version} + + + org.glassfish.jersey.media + jersey-media-multipart + ${jersey.client.version} + + + org.slf4j + slf4j-simple + ${org.slf4j.version} + test + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/AccessClient.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/AccessClient.java new file mode 100644 index 0000000000..23cbcbcf2e --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/AccessClient.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client; + +import java.io.IOException; + +/** + * Client for interacting with the AccessResource. + */ +public interface AccessClient { + + /** + * Get an access token by authenticating with a username and password aginst the configured identity provider. + * + * @param username the username + * @param password the password + * @return the access token + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + String getToken(String username, String password) throws NiFiRegistryException, IOException; + + /** + * Gets an access token via spnego. It is expected that the caller of this method has wrapped the call + * in a {@code doAs()} using a {@link javax.security.auth.Subject}. + * + * @return the token + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + String getTokenFromKerberosTicket() throws NiFiRegistryException, IOException; + + /** + * Performs a logout for the user represented by the given token. + * + * @param token the toke to authenticate with + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + void logout(String token) throws NiFiRegistryException, IOException; + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/BucketClient.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/BucketClient.java new file mode 100644 index 0000000000..7bc256a487 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/BucketClient.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client; + +import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.field.Fields; +import org.apache.nifi.registry.revision.entity.RevisionInfo; + +import java.io.IOException; +import java.util.List; + +/** + * Client for interacting with buckets. + */ +public interface BucketClient { + + /** + * Creates the given bucket. + * + * @param bucket the bucket to create + * @return the created bucket with containing identifier that was generated + */ + Bucket create(Bucket bucket) throws NiFiRegistryException, IOException; + + /** + * Gets the bucket with the given id. + * + * @param bucketId the id of the bucket to retrieve + * @return the bucket with the given id + */ + Bucket get(String bucketId) throws NiFiRegistryException, IOException; + + /** + * Updates the given bucket. Only the name and description can be updated. + * + * @param bucket the bucket with updates, must contain the id + * @return the updated bucket + */ + Bucket update(Bucket bucket) throws NiFiRegistryException, IOException; + + /** + * Deletes the bucket with the given id. + * + * @param bucketId the id of the bucket to delete + * @return the deleted bucket + */ + Bucket delete(String bucketId) throws NiFiRegistryException, IOException; + + /** + * Deletes the bucket with the given id and revision + * + * @param bucketId the id of the bucket to delete + * @param revision the revision info for the bucket being deleted + * @return the deleted bucket + */ + Bucket delete(String bucketId, RevisionInfo revision) throws NiFiRegistryException, IOException; + + /** + * Gets the fields that can be used to sort/search buckets. + * + * @return the bucket fields + */ + Fields getFields() throws NiFiRegistryException, IOException; + + /** + * Gets all buckets. + * + * @return the list of all buckets + */ + List getAll() throws NiFiRegistryException, IOException; + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/BundleClient.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/BundleClient.java new file mode 100644 index 0000000000..b71b22aeac --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/BundleClient.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client; + +import org.apache.nifi.registry.extension.bundle.Bundle; +import org.apache.nifi.registry.extension.bundle.BundleFilterParams; + +import java.io.IOException; +import java.util.List; + +/** + * Client for interacting with extension bundles. + */ +public interface BundleClient { + + /** + * Retrieves all extension bundles located in buckets the current user is authorized for. + * + * @return the list of extension bundles + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + List getAll() throws IOException, NiFiRegistryException; + + /** + * Retrieves all extension bundles matching the specified filters, located in buckets the current user is authorized for. + * + * @param filterParams the filter params + * @return the list of extension bundles + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + List getAll(BundleFilterParams filterParams) throws IOException, NiFiRegistryException; + + /** + * Retrieves the extension bundles located in the given bucket. + * + * @param bucketId the bucket id + * @return the list of bundles in the bucket + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + List getByBucket(String bucketId) throws IOException, NiFiRegistryException; + + /** + * Retrieves the extension bundle with the given id. + * + * @param bundleId the id of the bundle + * @return the bundle with the given id + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + Bundle get(String bundleId) throws IOException, NiFiRegistryException; + + /** + * Deletes the extension bundle with the given id, and all of its versions. + * + * @param bundleId the bundle id + * @return the deleted bundle + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + Bundle delete(String bundleId) throws IOException, NiFiRegistryException; + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/BundleVersionClient.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/BundleVersionClient.java new file mode 100644 index 0000000000..8512fae47b --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/BundleVersionClient.java @@ -0,0 +1,209 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client; + +import org.apache.nifi.registry.extension.bundle.BundleType; +import org.apache.nifi.registry.extension.bundle.BundleVersion; +import org.apache.nifi.registry.extension.bundle.BundleVersionFilterParams; +import org.apache.nifi.registry.extension.bundle.BundleVersionMetadata; +import org.apache.nifi.registry.extension.component.manifest.Extension; +import org.apache.nifi.registry.extension.component.ExtensionMetadata; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +/** + * Client for interacting with extension bundle versions. + */ +public interface BundleVersionClient { + + /** + * Uploads a version of an extension bundle to NiFi Registry where the bundle content comes from an InputStream. + * + * @param bucketId the bucket where the extension bundle will leave + * @param bundleType the type of bundle being uploaded + * @param bundleContentStream the input stream with the binary content of the bundle + * @return the BundleVersion entity + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + BundleVersion create(String bucketId, BundleType bundleType, InputStream bundleContentStream) + throws IOException, NiFiRegistryException; + + /** + * Uploads a version of an extension bundle to NiFi Registry where the bundle content comes from an InputStream. + * + * @param bucketId the bucket where the extension bundle will leave + * @param bundleType the type of bundle being uploaded + * @param bundleContentStream the input stream with the binary content of the bundle + * @param sha256 the optional SHA-256 in hex form + * @return the BundleVersion entity + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + BundleVersion create(String bucketId, BundleType bundleType, InputStream bundleContentStream, String sha256) + throws IOException, NiFiRegistryException; + + /** + * Uploads a version of an extension bundle to NiFi Registry where the bundle content comes from a File. + * + * @param bucketId the bucket where the extension bundle will leave + * @param bundleType the type of bundle being uploaded + * @param bundleFile the file with the binary content of the bundle + * @return the BundleVersion entity + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + BundleVersion create(String bucketId, BundleType bundleType, File bundleFile) + throws IOException, NiFiRegistryException; + + /** + * Uploads a version of an extension bundle to NiFi Registry where the bundle content comes from a File. + * + * @param bucketId the bucket where the extension bundle will leave + * @param bundleType the type of bundle being uploaded + * @param bundleFile the file with the binary content of the bundle + * @param sha256 the optional SHA-256 in hex form + * @return the BundleVersion entity + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + BundleVersion create(String bucketId, BundleType bundleType, File bundleFile, String sha256) + throws IOException, NiFiRegistryException; + + /** + * Retrieves all the extension bundle versions located in buckets the current user is authorized for, and + * matching any of the provided filter params. + * + * @param filterParams the filter params + * @return the list of bundle version metadata + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + List getBundleVersions(BundleVersionFilterParams filterParams) + throws IOException, NiFiRegistryException; + + /** + * Retrieves the metadata about the versions of the given bundle. + * + * @param bundleId the bundle id + * @return the list of version metadata + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + List getBundleVersions(String bundleId) throws IOException, NiFiRegistryException; + + /** + * Retrieves bundle version entity for the given bundle id and version string. + * + * The entity contains all of the information about the version, such as the bucket, bundle, and version metadata. + * + * The binary content of the bundle can be obtained by calling {@method getBundleVersionContent}. + * + * @param bundleId the bundle id + * @param version the bundle version + * @return the BundleVersion entity + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + BundleVersion getBundleVersion(String bundleId, String version) throws IOException, NiFiRegistryException; + + /** + * Retrieves the metadata about the extensions in the given bundle version. + * + * @param bundleId the bundle id + * @param version the bundle version + * @return the list of metadata about the extensions + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + List getExtensions(String bundleId, String version) throws IOException, NiFiRegistryException; + + /** + * Retrieves the full extension info for the extension with the given name in the given bundle version. + * + * @param bundleId the bundle id + * @param version the version of the bundle + * @param name the name of the extension + * @return the extension info + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + Extension getExtension(String bundleId, String version, String name) throws IOException, NiFiRegistryException; + + /** + * Obtains an InputStream for the html docs of the given extension. + * + * @param bundleId the bundle id + * @param version the version of the bundle + * @param name the name of the extensions + * @return the InputStream for the extension docs + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + InputStream getExtensionDocs(String bundleId, String version, String name) throws IOException, NiFiRegistryException; + + /** + * Obtains an InputStream for the binary content for the version of the given bundle. + * + * @param bundleId the bundle id + * @param version the version + * @return the InputStream for the bundle version content + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + InputStream getBundleVersionContent(String bundleId, String version) throws IOException, NiFiRegistryException; + + /** + * Writes the binary content for the version of the given the bundle to the specified directory. + * + * @param bundleId the bundle id + * @param version the bundle version + * @param directory the directory to write to + * @return the File object for the bundle that was written + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + File writeBundleVersionContent(String bundleId, String version, File directory) throws IOException, NiFiRegistryException; + + /** + * Deletes the given extension bundle version. + * + * @param bundleId the bundle id + * @param version the bundle version + * @return the deleted bundle versions + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + BundleVersion delete(String bundleId, String version) throws IOException, NiFiRegistryException; + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ExtensionClient.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ExtensionClient.java new file mode 100644 index 0000000000..b0e3e0ebf2 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ExtensionClient.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client; + +import org.apache.nifi.registry.extension.component.ExtensionFilterParams; +import org.apache.nifi.registry.extension.component.ExtensionMetadataContainer; +import org.apache.nifi.registry.extension.component.TagCount; +import org.apache.nifi.registry.extension.component.manifest.ProvidedServiceAPI; + +import java.io.IOException; +import java.util.List; + +/** + * Client for obtaining information about extensions. + */ +public interface ExtensionClient { + + /** + * Retrieves extensions according to the given filter params. + * + * @param filterParams the filter params + * @return the metadata for the extensions matching the filter params + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + ExtensionMetadataContainer findExtensions(ExtensionFilterParams filterParams) throws IOException, NiFiRegistryException; + + /** + * Retrieves extensions that provide the given service API. + * + * @param providedServiceAPI the service API + * @return the metadata for extensions that provided the service API + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + ExtensionMetadataContainer findExtensions(ProvidedServiceAPI providedServiceAPI) throws IOException, NiFiRegistryException; + + /** + * @return all of the tags known the registry with their corresponding counts + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + List getTagCounts() throws IOException, NiFiRegistryException; + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ExtensionRepoClient.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ExtensionRepoClient.java new file mode 100644 index 0000000000..359dee973d --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ExtensionRepoClient.java @@ -0,0 +1,210 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client; + +import org.apache.nifi.registry.extension.component.manifest.Extension; +import org.apache.nifi.registry.extension.repo.ExtensionRepoArtifact; +import org.apache.nifi.registry.extension.repo.ExtensionRepoBucket; +import org.apache.nifi.registry.extension.repo.ExtensionRepoExtensionMetadata; +import org.apache.nifi.registry.extension.repo.ExtensionRepoGroup; +import org.apache.nifi.registry.extension.repo.ExtensionRepoVersion; +import org.apache.nifi.registry.extension.repo.ExtensionRepoVersionSummary; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Optional; + +/** + * Client for interacting with the extension repository. + */ +public interface ExtensionRepoClient { + + /** + * Gets the buckets in the extension repo. + * + * @return the list of extension repo buckets. + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + List getBuckets() throws IOException, NiFiRegistryException; + + /** + * Gets the extension repo groups in the specified bucket. + * + * @param bucketName the bucket name + * @return the list of groups + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + List getGroups(String bucketName) throws IOException, NiFiRegistryException; + + /** + * Gets the extension repo artifacts in the given bucket and group. + * + * @param bucketName the bucket name + * @param groupId the group id + * @return the list of artifacts + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + List getArtifacts(String bucketName, String groupId) throws IOException, NiFiRegistryException; + + /** + * Gets the extension repo versions for the given bucket, group, artifact. + * + * @param bucketName the bucket name + * @param groupId the group id + * @param artifactId the artifact id + * @return the list of version summaries + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + List getVersions(String bucketName, String groupId, String artifactId) + throws IOException, NiFiRegistryException; + + /** + * Gets the extension repo version for the given bucket, group, artifact, and version. + * + * @param bucketName the bucket name + * @param groupId the group id + * @param artifactId the artifact id + * @param version the version + * @return the extension repo version + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + ExtensionRepoVersion getVersion(String bucketName, String groupId, String artifactId, String version) + throws IOException, NiFiRegistryException; + + /** + * Gets the metadata about the extensions for the given bucket, group, artifact, and version. + * + * @param bucketName the bucket name + * @param groupId the group id + * @param artifactId the artifact id + * @param version the version + * @return the list of extension metadata + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + List getVersionExtensions(String bucketName, String groupId, String artifactId, String version) + throws IOException, NiFiRegistryException; + + /** + * Gets the metadata about the extension with the given name in the given bucket, group, artifact, and version. + * + * @param bucketName the bucket name + * @param groupId the group id + * @param artifactId the artifact id + * @param version the version + * @param extensionName the extension name + * @return the extension info + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + Extension getVersionExtension(String bucketName, String groupId, String artifactId, String version, String extensionName) + throws IOException, NiFiRegistryException; + + /** + * Gets an InputStream for the html docs of the extension with the given name in the given bucket, group, artifact, and version. + * + * @param bucketName the bucket name + * @param groupId the group id + * @param artifactId the artifact id + * @param version the version + * @param extensionName the extension name + * @return the InputStream for the html docs + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + InputStream getVersionExtensionDocs(String bucketName, String groupId, String artifactId, String version, String extensionName) + throws IOException, NiFiRegistryException; + + /** + * Gets an InputStream for the binary content of the specified version. + * + * @param bucketName the bucket name + * @param groupId the group id + * @param artifactId the artifact id + * @param version the version + * @return the input stream + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + InputStream getVersionContent(String bucketName, String groupId, String artifactId, String version) + throws IOException, NiFiRegistryException; + + /** + * Writes the binary content for the version of the given the bundle to the specified directory. + * + * @param bucketName the bucket name + * @param groupId the group id + * @param artifactId the artifact id + * @param version the version + * @param directory the directory to write to + * @return the File object for the bundle that was written + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + File writeBundleVersionContent(String bucketName, String groupId, String artifactId, String version, File directory) + throws IOException, NiFiRegistryException; + + /** + * Gets the hex representation of the SHA-256 hash of the binary content for the given version. + * + * @param bucketName the bucket name + * @param groupId the group id + * @param artifactId the artifact id + * @param version the version + * @return the SHA-256 hex string + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + String getVersionSha256(String bucketName, String groupId, String artifactId, String version) + throws IOException, NiFiRegistryException; + + /** + * Gets the hex representation of the SHA-256 hash of the binary content for the given version. + * + * If the version is a SNAPSHOT version, there may be more than one instance of the SNAPSHOT version in different + * buckets. In this case the instance with the latest created timestamp will be used to obtain the checksum. + * + * @param groupId the group id + * @param artifactId the artifact id + * @param version the version + * @return the SHA-256 hex string + * + * @throws IOException if an I/O error occurs + * @throws NiFiRegistryException if an non I/O error occurs + */ + Optional getVersionSha256(String groupId, String artifactId, String version) + throws IOException, NiFiRegistryException; + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/FlowClient.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/FlowClient.java new file mode 100644 index 0000000000..af8676fa8b --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/FlowClient.java @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client; + +import org.apache.nifi.registry.diff.VersionedFlowDifference; +import org.apache.nifi.registry.field.Fields; +import org.apache.nifi.registry.flow.VersionedFlow; +import org.apache.nifi.registry.revision.entity.RevisionInfo; + +import java.io.IOException; +import java.util.List; + +/** + * Client for interacting with flows. + */ +public interface FlowClient { + + /** + * Create the given flow in the given bucket. + * + * @param flow the flow to create + * @return the created flow with the identifier populated + * @throws NiFiRegistryException if an error is encountered other than IOException + * @throws IOException if an I/O error is encountered + */ + VersionedFlow create(VersionedFlow flow) throws NiFiRegistryException, IOException; + + /** + * Gets the flow with the given id in the given bucket. + * + * The list of snapshot metadata will NOT be populated. + * + * @param bucketId a bucket id + * @param flowId a flow id + * @return the flow with the given id in the given bucket + * @throws NiFiRegistryException if an error is encountered other than IOException + * @throws IOException if an I/O error is encountered + */ + VersionedFlow get(String bucketId, String flowId) throws NiFiRegistryException, IOException; + + /** + * Gets the flow with the given id. + * + * @param flowId a flow id + * @return the flow with the given id + * @throws NiFiRegistryException if an error is encountered other than IOException + * @throws IOException if an I/O error is encountered + */ + VersionedFlow get(String flowId) throws NiFiRegistryException, IOException; + + /** + * Updates the given flow with in the given bucket. + * + * The identifier of the flow must be populated in the flow object, and only the name and description can be updated. + * + * @param bucketId a bucket id + * @param flow the flow with updates + * @return the updated flow + * @throws NiFiRegistryException if an error is encountered other than IOException + * @throws IOException if an I/O error is encountered + */ + VersionedFlow update(String bucketId, VersionedFlow flow) throws NiFiRegistryException, IOException; + + /** + * Deletes the flow with the given id in the given bucket. + * + * @param bucketId a bucket id + * @param flowId the id of the flow to delete + * @return the deleted flow + * @throws NiFiRegistryException if an error is encountered other than IOException + * @throws IOException if an I/O error is encountered + */ + VersionedFlow delete(String bucketId, String flowId) throws NiFiRegistryException, IOException; + + /** + * Deletes the flow with the given id in the given bucket. + * + * @param bucketId a bucket id + * @param flowId the id of the flow to delete + * @param revision the revision information for the entity being deleted + * @return the deleted flow + * @throws NiFiRegistryException if an error is encountered other than IOException + * @throws IOException if an I/O error is encountered + */ + VersionedFlow delete(String bucketId, String flowId, RevisionInfo revision) throws NiFiRegistryException, IOException; + + /** + * Gets the field info for flows. + * + * @return field info for flows + * @throws NiFiRegistryException if an error is encountered other than IOException + * @throws IOException if an I/O error is encountered + */ + Fields getFields() throws NiFiRegistryException, IOException; + + /** + * Gets the flows for a given bucket. + * + * @param bucketId a bucket id + * @return the flows in the given bucket + * @throws NiFiRegistryException if an error is encountered other than IOException + * @throws IOException if an I/O error is encountered + */ + List getByBucket(String bucketId) throws NiFiRegistryException, IOException; + + /** + * + * @param bucketId a bucket id + * @param flowId the flow that is under inspection + * @param versionA the first version to use in the comparison + * @param versionB the second flow to use in the comparison + * @return the list of differences between the 2 flow versions grouped by component + * @throws NiFiRegistryException if an error is encountered other than IOException + * @throws IOException if an I/O error is encountered + */ + VersionedFlowDifference diff(final String bucketId, final String flowId, + final Integer versionA, final Integer versionB) throws NiFiRegistryException, IOException; +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/FlowSnapshotClient.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/FlowSnapshotClient.java new file mode 100644 index 0000000000..edf7beb1e4 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/FlowSnapshotClient.java @@ -0,0 +1,133 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client; + +import org.apache.nifi.registry.flow.VersionedFlowSnapshot; +import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata; + +import java.io.IOException; +import java.util.List; + +/** + * Client for interacting with snapshots. + */ +public interface FlowSnapshotClient { + + /** + * Creates a new snapshot/version for the given flow. + * + * The snapshot object must have the version populated, and will receive an error if the submitted version is + * not the next one-up version. + * + * @param snapshot the new snapshot + * @return the created snapshot + * @throws NiFiRegistryException if an error is encountered other than IOException + * @throws IOException if an I/O error is encountered + */ + VersionedFlowSnapshot create(VersionedFlowSnapshot snapshot) throws NiFiRegistryException, IOException; + + /** + * Gets the snapshot for the given bucket, flow, and version. + * + * @param bucketId the bucket id + * @param flowId the flow id + * @param version the version + * @return the snapshot with the given version of the given flow in the given bucket + * @throws NiFiRegistryException if an error is encountered other than IOException + * @throws IOException if an I/O error is encountered + */ + VersionedFlowSnapshot get(String bucketId, String flowId, int version) throws NiFiRegistryException, IOException; + + /** + * Gets the snapshot for the given flow and version. + * + * @param flowId the flow id + * @param version the version + * @return the snapshot with the given version of the given flow + * @throws NiFiRegistryException if an error is encountered other than IOException + * @throws IOException if an I/O error is encountered + */ + VersionedFlowSnapshot get(String flowId, int version) throws NiFiRegistryException, IOException; + + /** + * Gets the latest snapshot for the given flow. + * + * @param bucketId the bucket id + * @param flowId the flow id + * @return the snapshot with the latest version for the given flow + * @throws NiFiRegistryException if an error is encountered other than IOException + * @throws IOException if an I/O error is encountered + */ + VersionedFlowSnapshot getLatest(String bucketId, String flowId) throws NiFiRegistryException, IOException; + + /** + * Gets the latest snapshot for the given flow. + * + * @param flowId the flow id + * @return the snapshot with the latest version for the given flow + * @throws NiFiRegistryException if an error is encountered other than IOException + * @throws IOException if an I/O error is encountered + */ + VersionedFlowSnapshot getLatest(String flowId) throws NiFiRegistryException, IOException; + + /** + * Gets the latest snapshot metadata for the given flow. + * + * @param bucketId the bucket id + * @param flowId the flow id + * @return the snapshot metadata for the latest version of the given flow + * @throws NiFiRegistryException if an error is encountered other than IOException + * @throws IOException if an I/O error is encountered + */ + VersionedFlowSnapshotMetadata getLatestMetadata(String bucketId, String flowId) throws NiFiRegistryException, IOException; + + /** + * Gets the latest snapshot metadata for the given flow. + * + * @param flowId the flow id + * @return the snapshot metadata for the latest version of the given flow + * @throws NiFiRegistryException if an error is encountered other than IOException + * @throws IOException if an I/O error is encountered + */ + VersionedFlowSnapshotMetadata getLatestMetadata(String flowId) throws NiFiRegistryException, IOException; + + /** + * Gets a list of the metadata for all snapshots of a given flow. + * + * The contents of each snapshot are not part of the response. + * + * @param bucketId the bucket id + * @param flowId the flow id + * @return the list of snapshot metadata + * @throws NiFiRegistryException if an error is encountered other than IOException + * @throws IOException if an I/O error is encountered + */ + List getSnapshotMetadata(String bucketId, String flowId) throws NiFiRegistryException, IOException; + + /** + * Gets a list of the metadata for all snapshots of a given flow. + * + * The contents of each snapshot are not part of the response. + * + * @param flowId the flow id + * @return the list of snapshot metadata + * @throws NiFiRegistryException if an error is encountered other than IOException + * @throws IOException if an I/O error is encountered + */ + List getSnapshotMetadata(String flowId) throws NiFiRegistryException, IOException; + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ItemsClient.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ItemsClient.java new file mode 100644 index 0000000000..96fa801711 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ItemsClient.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client; + +import org.apache.nifi.registry.bucket.BucketItem; +import org.apache.nifi.registry.field.Fields; + +import java.io.IOException; +import java.util.List; + +/** + * Client for interacting with bucket items. + * + * Bucket items contain the common fields across anything stored in the registry. + * + * Each item contains a type field and a link to the URI of the specific item. + * + * i.e. The link field of a flow item would contain the URI to the specific flow. + */ +public interface ItemsClient { + + /** + * Gets all bucket items in the registry. + * + * @return the list of all bucket items + * @throws NiFiRegistryException if an error is encountered other than IOException + * @throws IOException if an I/O error is encountered + */ + List getAll() throws NiFiRegistryException, IOException; + + /** + * Gets all bucket items for the given bucket. + * + * @param bucketId the bucket id + * @return the list of items in the given bucket + * @throws NiFiRegistryException if an error is encountered other than IOException + * @throws IOException if an I/O error is encountered + */ + List getByBucket(String bucketId) throws NiFiRegistryException, IOException; + + /** + * Gets the field info for bucket items. + * + * @return the list of field info + * @throws NiFiRegistryException if an error is encountered other than IOException + * @throws IOException if an I/O error is encountered + */ + Fields getFields() throws NiFiRegistryException, IOException; + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClient.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClient.java new file mode 100644 index 0000000000..2d2d8c8c90 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClient.java @@ -0,0 +1,267 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client; + +import java.io.Closeable; + +/** + * A client for interacting with the REST API of a NiFi registry instance. + */ +public interface NiFiRegistryClient extends Closeable { + + /** + * @return the client for interacting with buckets + */ + BucketClient getBucketClient(); + + /** + * @deprecated use getBucketClient(RequestConfig requestConfig) + * + * @return the client for interacting with buckets on behalf of the given proxied entities + */ + BucketClient getBucketClient(String ... proxiedEntity); + + /** + * @return the client for interacting with buckets using the given request config + */ + BucketClient getBucketClient(RequestConfig requestConfig); + + //------------------------------------------------------------------------------------------- + + /** + * @return the client for interacting with flows + */ + FlowClient getFlowClient(); + + /** + * @deprecated use getFlowClient(RequestConfig requestConfig) + * + * @return the client for interacting with flows on behalf of the given proxied entities + */ + FlowClient getFlowClient(String ... proxiedEntity); + + /** + * @return the client for interacting with flows using the given request config + */ + FlowClient getFlowClient(RequestConfig requestConfig); + + //------------------------------------------------------------------------------------------- + + /** + * @return the client for interacting with flows/snapshots + */ + FlowSnapshotClient getFlowSnapshotClient(); + + /** + * @deprecated use getFlowSnapshotClient(RequestConfig requestConfig) + * + * @return the client for interacting with flows/snapshots on behalf of the given proxied entities + */ + FlowSnapshotClient getFlowSnapshotClient(String ... proxiedEntity); + + /** + * @return the client for interacting with flows/snapshots using the given request config + */ + FlowSnapshotClient getFlowSnapshotClient(RequestConfig requestConfig); + + //------------------------------------------------------------------------------------------- + + /** + * @return the client for interacting with bucket items + */ + ItemsClient getItemsClient(); + + /** + * @deprecated use getItemsClient(RequestConfig requestConfig) + * + * @return the client for interacting with bucket items on behalf of the given proxied entities + */ + ItemsClient getItemsClient(String ... proxiedEntity); + + /** + * @return the client for interacting with bucket items using the given request config + */ + ItemsClient getItemsClient(RequestConfig requestConfig); + + //------------------------------------------------------------------------------------------- + + /** + * @return the client for obtaining information about the current user + */ + UserClient getUserClient(); + + /** + * @deprecated use getUserClient(RequestConfig requestConfig) + * + * @return the client for obtaining information about the current user based on the given proxied entities + */ + UserClient getUserClient(String ... proxiedEntity); + + /** + * @return the client for obtaining information about the current user based on the request config + */ + UserClient getUserClient(RequestConfig requestConfig); + + //------------------------------------------------------------------------------------------- + + /** + * @return the client for interacting with extension bundles + */ + BundleClient getBundleClient(); + + /** + * @deprecated use getBundleClient(RequestConfig requestConfig) + * + * @return the client for interacting with extension bundles on behalf of the given proxied entities + */ + BundleClient getBundleClient(String ... proxiedEntity); + + /** + * @return the client for interacting with extension bundles using the given request config + */ + BundleClient getBundleClient(RequestConfig requestConfig); + + //------------------------------------------------------------------------------------------- + + /** + * @return the client for interacting with extension bundle versions + */ + BundleVersionClient getBundleVersionClient(); + + /** + * @deprecated use getBundleVersionClient(RequestConfig requestConfig) + * + * @return the client for interacting with extension bundle versions on behalf of the given proxied entities + */ + BundleVersionClient getBundleVersionClient(String ... proxiedEntity); + + /** + * @return the client for interacting with extension bundle versions using the given request config + */ + BundleVersionClient getBundleVersionClient(RequestConfig requestConfig); + + //------------------------------------------------------------------------------------------- + + /** + * @return the client for interacting with the extension repository + */ + ExtensionRepoClient getExtensionRepoClient(); + + /** + * @deprecated use getExtensionRepoClient(RequestConfig requestConfig) + * + * @return the client for interacting with the extension repository on behalf of the given proxied entities + */ + ExtensionRepoClient getExtensionRepoClient(String ... proxiedEntity); + + /** + * @return the client for interacting with the extension repository using the given request config + */ + ExtensionRepoClient getExtensionRepoClient(RequestConfig requestConfig); + + //------------------------------------------------------------------------------------------- + + /** + * @return the client for interacting with extensions + */ + ExtensionClient getExtensionClient(); + + /** + * @deprecated use getExtensionClient(RequestConfig requestConfig) + * + * @return the client for interacting with extensions on behalf of the given proxied entities + */ + ExtensionClient getExtensionClient(String ... proxiedEntity); + + /** + * @return the client for interacting with extensions using the given request config + */ + ExtensionClient getExtensionClient(RequestConfig requestConfig); + + //------------------------------------------------------------------------------------------- + + /** + * Returns client for interacting with tenants. + * + * @return the client for interacting with tenants + */ + TenantsClient getTenantsClient(); + + /** + * Returns client for interacting with tenants. + * + * @deprecated use getTenantsClient(RequestConfig requestConfig) + * + * @param proxiedEntity The given proxied entities. + * + * @return the client for interacting with tenants on behalf of the given proxied entities. + */ + TenantsClient getTenantsClient(String ... proxiedEntity); + + /** + * @return the client for interacting with tenants using the given request config + */ + TenantsClient getTenantsClient(RequestConfig requestConfig); + + //------------------------------------------------------------------------------------------- + + /** + * Returns client for interacting with access policies. + * + * @return the client for interacting with access policies + */ + PoliciesClient getPoliciesClient(); + + /** + * Returns client for interacting with access policies. + * + * @deprecated use getPoliciesClient(RequestConfig requestConfig) + * + * @param proxiedEntity The given proxied entities. + * + * @return the client for interacting with access policies on behalf of the given proxied entities. + */ + PoliciesClient getPoliciesClient(String ... proxiedEntity); + + /** + * @return the client for interacting with access policies using the given request config + */ + PoliciesClient getPoliciesClient(RequestConfig requestConfig); + + //------------------------------------------------------------------------------------------- + + /** + * @return the client for obtaining access tokens + */ + AccessClient getAccessClient(); + + //------------------------------------------------------------------------------------------- + + /** + * The builder interface that implementations should provide for obtaining the client. + */ + interface Builder { + + Builder config(NiFiRegistryClientConfig clientConfig); + + NiFiRegistryClientConfig getConfig(); + + NiFiRegistryClient build(); + + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClientConfig.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClientConfig.java new file mode 100644 index 0000000000..784f77fc99 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClientConfig.java @@ -0,0 +1,272 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client; + +import org.apache.nifi.registry.security.util.CertificateUtils; +import org.apache.nifi.registry.security.util.KeyStoreUtils; +import org.apache.nifi.registry.security.util.KeystoreType; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.security.KeyStore; +import java.security.SecureRandom; + +/** + * Configuration for a NiFiRegistryClient. + */ +public class NiFiRegistryClientConfig { + + public static final String DEFAULT_PROTOCOL = CertificateUtils.getHighestCurrentSupportedTlsProtocolVersion(); + + private final String baseUrl; + private final SSLContext sslContext; + private final String keystoreFilename; + private final String keystorePass; + private final String keyPass; + private final KeystoreType keystoreType; + private final String truststoreFilename; + private final String truststorePass; + private final KeystoreType truststoreType; + private final String protocol; + private final HostnameVerifier hostnameVerifier; + private final Integer readTimeout; + private final Integer connectTimeout; + + + private NiFiRegistryClientConfig(final Builder builder) { + this.baseUrl = builder.baseUrl; + this.sslContext = builder.sslContext; + this.keystoreFilename = builder.keystoreFilename; + this.keystorePass = builder.keystorePass; + this.keyPass = builder.keyPass; + this.keystoreType = builder.keystoreType; + this.truststoreFilename = builder.truststoreFilename; + this.truststorePass = builder.truststorePass; + this.truststoreType = builder.truststoreType; + this.protocol = builder.protocol == null ? DEFAULT_PROTOCOL : builder.protocol; + this.hostnameVerifier = builder.hostnameVerifier; + this.readTimeout = builder.readTimeout; + this.connectTimeout = builder.connectTimeout; + } + + public String getBaseUrl() { + return baseUrl; + } + + public SSLContext getSslContext() { + if (sslContext != null) { + return sslContext; + } + + final KeyManagerFactory keyManagerFactory; + if (keystoreFilename != null && keystorePass != null && keystoreType != null) { + try { + // prepare the keystore + final KeyStore keyStore = KeyStoreUtils.getKeyStore(keystoreType.name()); + try (final InputStream keyStoreStream = new FileInputStream(new File(keystoreFilename))) { + keyStore.load(keyStoreStream, keystorePass.toCharArray()); + } + keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + + if (keyPass == null) { + keyManagerFactory.init(keyStore, keystorePass.toCharArray()); + } else { + keyManagerFactory.init(keyStore, keyPass.toCharArray()); + } + } catch (final Exception e) { + throw new IllegalStateException("Failed to load Keystore", e); + } + } else { + keyManagerFactory = null; + } + + final TrustManagerFactory trustManagerFactory; + if (truststoreFilename != null && truststorePass != null && truststoreType != null) { + try { + // prepare the truststore + final KeyStore trustStore = KeyStoreUtils.getKeyStore(truststoreType.name()); + try (final InputStream trustStoreStream = new FileInputStream(new File(truststoreFilename))) { + trustStore.load(trustStoreStream, truststorePass.toCharArray()); + } + trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(trustStore); + } catch (final Exception e) { + throw new IllegalStateException("Failed to load Truststore", e); + } + } else { + trustManagerFactory = null; + } + + if (keyManagerFactory != null || trustManagerFactory != null) { + try { + // initialize the ssl context + KeyManager[] keyManagers = keyManagerFactory != null ? keyManagerFactory.getKeyManagers() : null; + TrustManager[] trustManagers = trustManagerFactory != null ? trustManagerFactory.getTrustManagers() : null; + final SSLContext sslContext = SSLContext.getInstance(getProtocol()); + sslContext.init(keyManagers, trustManagers, new SecureRandom()); + sslContext.getDefaultSSLParameters().setNeedClientAuth(true); + + return sslContext; + } catch (final Exception e) { + throw new IllegalStateException("Created keystore and truststore but failed to initialize SSLContext", e); + } + } else { + return null; + } + } + + public String getKeystoreFilename() { + return keystoreFilename; + } + + public String getKeystorePass() { + return keystorePass; + } + + public String getKeyPass() { + return keyPass; + } + + public KeystoreType getKeystoreType() { + return keystoreType; + } + + public String getTruststoreFilename() { + return truststoreFilename; + } + + public String getTruststorePass() { + return truststorePass; + } + + public KeystoreType getTruststoreType() { + return truststoreType; + } + + public String getProtocol() { + return protocol; + } + + public HostnameVerifier getHostnameVerifier() { + return hostnameVerifier; + } + + public Integer getReadTimeout() { + return readTimeout; + } + + public Integer getConnectTimeout() { + return connectTimeout; + } + + /** + * Builder for client configuration. + */ + public static class Builder { + + private String baseUrl; + private SSLContext sslContext; + private String keystoreFilename; + private String keystorePass; + private String keyPass; + private KeystoreType keystoreType; + private String truststoreFilename; + private String truststorePass; + private KeystoreType truststoreType; + private String protocol; + private HostnameVerifier hostnameVerifier; + private Integer readTimeout; + private Integer connectTimeout; + + public Builder baseUrl(final String baseUrl) { + this.baseUrl = baseUrl; + return this; + } + + public Builder sslContext(final SSLContext sslContext) { + this.sslContext = sslContext; + return this; + } + + public Builder keystoreFilename(final String keystoreFilename) { + this.keystoreFilename = keystoreFilename; + return this; + } + + public Builder keystorePassword(final String keystorePass) { + this.keystorePass = keystorePass; + return this; + } + + public Builder keyPassword(final String keyPass) { + this.keyPass = keyPass; + return this; + } + + public Builder keystoreType(final KeystoreType keystoreType) { + this.keystoreType = keystoreType; + return this; + } + + public Builder truststoreFilename(final String truststoreFilename) { + this.truststoreFilename = truststoreFilename; + return this; + } + + public Builder truststorePassword(final String truststorePass) { + this.truststorePass = truststorePass; + return this; + } + + public Builder truststoreType(final KeystoreType truststoreType) { + this.truststoreType = truststoreType; + return this; + } + + public Builder protocol(final String protocol) { + this.protocol = protocol; + return this; + } + + public Builder hostnameVerifier(final HostnameVerifier hostnameVerifier) { + this.hostnameVerifier = hostnameVerifier; + return this; + } + + public Builder readTimeout(final Integer readTimeout) { + this.readTimeout = readTimeout; + return this; + } + + public Builder connectTimeout(final Integer connectTimeout) { + this.connectTimeout = connectTimeout; + return this; + } + + public NiFiRegistryClientConfig build() { + return new NiFiRegistryClientConfig(this); + } + + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryException.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryException.java new file mode 100644 index 0000000000..273a032678 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryException.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client; + +/** + * Indicates an error interacting with the NiFi registry for a reason other than IOException. + */ +public class NiFiRegistryException extends Exception { + + public NiFiRegistryException(final String message) { + super(message); + } + + public NiFiRegistryException(final String message, final Throwable cause) { + super(message, cause); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/PoliciesClient.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/PoliciesClient.java new file mode 100644 index 0000000000..00a46e2c88 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/PoliciesClient.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client; + +import org.apache.nifi.registry.authorization.AccessPolicy; + +import java.io.IOException; + +public interface PoliciesClient { + + /** + * Returns a given access policy. + * + * @param resource The action allowed by the access policy. + * @param action The resource managed by the access policy. + * + * @return The access policy. + * + * @throws NiFiRegistryException Thrown in case of unsuccessful execution. + * @throws IOException Thrown when there is an issue while communicating with NiFi Registry. + */ + AccessPolicy getAccessPolicy(String action, String resource) throws NiFiRegistryException, IOException; + + /** + * Creates a new access policy. + * + * @param policy The access policy to be created. Note: identifier will be ignored and assigned by NiFi Registry. + * + * @return The created access with an assigned identifier. + * + * @throws NiFiRegistryException Thrown in case of unsuccessful execution. + * @throws IOException Thrown when there is an issue while communicating with NiFi Registry. + */ + AccessPolicy createAccessPolicy(AccessPolicy policy) throws NiFiRegistryException, IOException; + + /** + * Updates an existing access policy. + * + * @param policy The access policy with new attributes. + * + * @return The updated access policy. + * + * @throws NiFiRegistryException Thrown in case of unsuccessful execution. + * @throws IOException Thrown when there is an issue while communicating with NiFi Registry. + */ + AccessPolicy updateAccessPolicy(AccessPolicy policy) throws NiFiRegistryException, IOException; + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/RequestConfig.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/RequestConfig.java new file mode 100644 index 0000000000..fcb83e9460 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/RequestConfig.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client; + +import java.util.Map; + +/** + * Configuration applied to each client request. + */ +public interface RequestConfig { + + /** + * @return the headers to apply to each request + */ + Map getHeaders(); + + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/TenantsClient.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/TenantsClient.java new file mode 100644 index 0000000000..adb117ca61 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/TenantsClient.java @@ -0,0 +1,166 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client; + +import org.apache.nifi.registry.authorization.User; +import org.apache.nifi.registry.authorization.UserGroup; +import org.apache.nifi.registry.revision.entity.RevisionInfo; + +import java.io.IOException; +import java.util.List; + +public interface TenantsClient { + + /** + * Returns all users. + * + * @return The list of users. + * + * @throws NiFiRegistryException Thrown in case of unsuccessful execution. + * @throws IOException Thrown when there is an issue while communicating with NiFi Registry. + */ + List getUsers() throws NiFiRegistryException, IOException; + + /** + * Returns a user with a given identifier. + * + * @param id Identifier of the user. + * + * @return The user. + * + * @throws NiFiRegistryException Thrown in case of unsuccessful execution. + * @throws IOException Thrown when there is an issue while communicating with NiFi Registry. + */ + User getUser(String id) throws NiFiRegistryException, IOException; + + /** + * Creates a new user in NiFi Registry. + * + * @param user The new user. Note: identifier will be ignored and assigned be NiFi Registry. + * + * @return The created user with an assigned identifier. + * + * @throws NiFiRegistryException Thrown in case of unsuccessful execution. + * @throws IOException Thrown when there is an issue while communicating with NiFi Registry. + */ + User createUser(User user) throws NiFiRegistryException, IOException; + + /** + * Updates an existing user. + * + * @param user The user with the new attributes. + * + * @return The updated user. + * + * @throws NiFiRegistryException Thrown in case of unsuccessful execution. + * @throws IOException Thrown when there is an issue while communicating with NiFi Registry. + */ + User updateUser(User user) throws NiFiRegistryException, IOException; + + /** + * Deletes an existing user. + * + * @param id identifier of the user + * @return the deleted user + * + * @throws NiFiRegistryException Thrown in case of unsuccessful execution. + * @throws IOException Thrown when there is an issue while communicating with NiFi Registry. + */ + User deleteUser(String id) throws NiFiRegistryException, IOException; + + /** + * Deletes an existing user. + * + * @param id identifier of the user + * @param revisionInfo the revision info for the user to delete + * @return the deleted user + * + * @throws NiFiRegistryException Thrown in case of unsuccessful execution. + * @throws IOException Thrown when there is an issue while communicating with NiFi Registry. + */ + User deleteUser(String id, RevisionInfo revisionInfo) throws NiFiRegistryException, IOException; + + /** + * Returns all user groups. + * + * @return The list of user groups. + * + * @throws NiFiRegistryException Thrown in case of unsuccessful execution. + * @throws IOException Thrown when there is an issue while communicating with NiFi Registry. + */ + List getUserGroups() throws NiFiRegistryException, IOException; + + /** + * Returns a user group with a given identifier. + * + * @param id Identifier of the user group. + * + * @return The user group. + * + * @throws NiFiRegistryException Thrown in case of unsuccessful execution. + * @throws IOException Thrown when there is an issue while communicating with NiFi Registry. + */ + UserGroup getUserGroup(String id) throws NiFiRegistryException, IOException; + + /** + * Creates a new user group. + * + * @param group The user group to be created. Note: identifier will be ignored and assigned by NiFi Registry. + * + * @return The created user group with an assigned identifier. + * + * @throws NiFiRegistryException Thrown in case of unsuccessful execution. + * @throws IOException Thrown when there is an issue while communicating with NiFi Registry. + */ + UserGroup createUserGroup(UserGroup group) throws NiFiRegistryException, IOException; + + /** + * Updates an existing user group. + * + * @param group The user group with new attributes. + * + * @return The user group after store. + * + * @throws NiFiRegistryException Thrown in case of unsuccessful execution. + * @throws IOException Thrown when there is an issue while communicating with NiFi Registry. + */ + UserGroup updateUserGroup(UserGroup group) throws NiFiRegistryException, IOException; + + /** + * Deletes an existing group. + * + * @param id identifier of the group + * @return the deleted group + * + * @throws NiFiRegistryException Thrown in case of unsuccessful execution. + * @throws IOException Thrown when there is an issue while communicating with NiFi Registry. + */ + UserGroup deleteUserGroup(String id) throws NiFiRegistryException, IOException; + + /** + * Deletes an existing group. + * + * @param id identifier of the group + * @param revisionInfo the revision info for the group to delete + * @return the deleted group + * + * @throws NiFiRegistryException Thrown in case of unsuccessful execution. + * @throws IOException Thrown when there is an issue while communicating with NiFi Registry. + */ + UserGroup deleteUserGroup(String id, RevisionInfo revisionInfo) throws NiFiRegistryException, IOException; + +} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/UserClient.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/UserClient.java new file mode 100644 index 0000000000..181f7af9b9 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/UserClient.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client; + +import org.apache.nifi.registry.authorization.CurrentUser; + +import java.io.IOException; + +public interface UserClient { + + /** + * Obtains the access status of the current user. + * + * If the UserClient was obtained with proxied entities, then the access status should represent the status + * of the last identity in the chain. + * + * If the UserClient was obtained without proxied entities, then it would represent the identity of the certificate + * in the keystore used by the client. + * + * If the registry is not in secure mode, the anonymous identity is expected to be returned along with a flag indicating + * the user is anonymous. + * + * @return the access status of the current user + * @throws NiFiRegistryException if the proxying user is not a valid proxy or identity claim is otherwise invalid + */ + CurrentUser getAccessStatus() throws NiFiRegistryException, IOException; + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/AbstractCRUDJerseyClient.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/AbstractCRUDJerseyClient.java new file mode 100644 index 0000000000..41005c1699 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/AbstractCRUDJerseyClient.java @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client.impl; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.client.NiFiRegistryException; +import org.apache.nifi.registry.client.RequestConfig; +import org.apache.nifi.registry.revision.entity.RevisionInfo; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MediaType; +import java.io.IOException; + +public class AbstractCRUDJerseyClient extends AbstractJerseyClient { + + protected final WebTarget baseTarget; + + public AbstractCRUDJerseyClient(final WebTarget baseTarget, final RequestConfig requestConfig) { + super(requestConfig); + this.baseTarget = baseTarget; + } + + protected T get( + String id, + Class entityType, + String entityTypeName, + String entityPath + ) throws NiFiRegistryException, IOException { + if (StringUtils.isBlank(id)) { + throw new IllegalArgumentException(entityTypeName + " id cannot be blank"); + } + + return executeAction("Error retrieving " + entityTypeName.toLowerCase(), () -> { + final WebTarget target = baseTarget.path(entityPath).path(id); + return getRequestBuilder(target).get(entityType); + }); + } + + protected T create( + T entity, + Class entityType, + String entityTypeName, + String entityPath + ) throws NiFiRegistryException, IOException { + if (entity == null) { + throw new IllegalArgumentException(entityTypeName + " cannot be null"); + } + + return executeAction("Error creating " + entityTypeName.toLowerCase(), () -> { + final WebTarget target = baseTarget.path(entityPath); + + return getRequestBuilder(target).post( + Entity.entity(entity, MediaType.APPLICATION_JSON_TYPE), entityType + ); + }); + } + + protected T update( + T entity, + String id, + Class entityType, + String entityTypeName, + String entityPath + ) throws NiFiRegistryException, IOException { + if (entity == null) { + throw new IllegalArgumentException(entityTypeName + " cannot be null"); + } + + return executeAction("Error updating " + entityTypeName.toLowerCase(), () -> { + final WebTarget target = baseTarget.path(entityPath).path(id); + + return getRequestBuilder(target).put( + Entity.entity(entity, MediaType.APPLICATION_JSON_TYPE), entityType + ); + }); + } + + protected T delete( + String id, + RevisionInfo revisionInfo, + Class entityType, + String entityTypeName, + String entityPath + ) throws NiFiRegistryException, IOException { + if (StringUtils.isBlank(id)) { + throw new IllegalArgumentException(entityTypeName + " id cannot be blank"); + } + + return executeAction("Error deleting " + entityTypeName.toLowerCase(), () -> { + WebTarget target = baseTarget.path(entityPath).path(id); + target = addRevisionQueryParams(target, revisionInfo); + + return getRequestBuilder(target).delete(entityType); + }); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/AbstractJerseyClient.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/AbstractJerseyClient.java new file mode 100644 index 0000000000..ad5ea41db4 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/AbstractJerseyClient.java @@ -0,0 +1,152 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client.impl; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.client.NiFiRegistryException; +import org.apache.nifi.registry.client.RequestConfig; +import org.apache.nifi.registry.revision.entity.RevisionInfo; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.util.Collections; +import java.util.Map; + +/** + * Base class for the client operations to share exception handling. + * + * Sub-classes should always execute a request from getRequestBuilder(target) to ensure proper headers are sent. + */ +public class AbstractJerseyClient { + + private static final RequestConfig EMPTY_REQUEST_CONFIG = () -> Collections.emptyMap(); + + private final RequestConfig requestConfig; + + public AbstractJerseyClient(final RequestConfig requestConfig) { + this.requestConfig = (requestConfig == null ? EMPTY_REQUEST_CONFIG : requestConfig); + } + + protected RequestConfig getRequestConfig() { + return this.requestConfig; + } + /** + * Adds query parameters for the given RevisionInfo if populated. + * + * @param target the WebTarget + * @param revision the RevisionInfo + * @return the target with query params added + */ + protected WebTarget addRevisionQueryParams(WebTarget target, RevisionInfo revision) { + if (revision == null) { + return target; + } + + WebTarget localTarget = target; + + final Long version = revision.getVersion(); + if (version != null) { + localTarget = localTarget.queryParam("version", version.longValue()); + } + + final String clientId = revision.getClientId(); + if (!StringUtils.isBlank(clientId)) { + localTarget = localTarget.queryParam("clientId", clientId); + } + return localTarget; + } + + /** + * Creates a new Invocation.Builder for the given WebTarget with the headers added to the builder. + * + * @param webTarget the target for the request + * @return the builder for the target with the headers added + */ + protected Invocation.Builder getRequestBuilder(final WebTarget webTarget) { + final Invocation.Builder requestBuilder = webTarget.request(); + + final Map headers = requestConfig.getHeaders(); + headers.entrySet().stream().forEach(e -> requestBuilder.header(e.getKey(), e.getValue())); + + return requestBuilder; + } + + /** + * Executes the given action and returns the result. + * + * @param action the action to execute + * @param errorMessage the message to use if a NiFiRegistryException is thrown + * @param the return type of the action + * @return the result of the action + * @throws NiFiRegistryException if any exception other than IOException is encountered + * @throws IOException if an I/O error occurs communicating with the registry + */ + protected T executeAction(final String errorMessage, final NiFiRegistryAction action) throws NiFiRegistryException, IOException { + try { + return action.execute(); + } catch (final Exception e) { + final Throwable ioeCause = getIOExceptionCause(e); + + if (ioeCause == null) { + final StringBuilder errorMessageBuilder = new StringBuilder(errorMessage); + + // see if we have a WebApplicationException, and if so add the response body to the error message + if (e instanceof WebApplicationException) { + final Response response = ((WebApplicationException) e).getResponse(); + final String responseBody = response.readEntity(String.class); + errorMessageBuilder.append(": ").append(responseBody); + } + + throw new NiFiRegistryException(errorMessageBuilder.toString(), e); + } else { + throw (IOException) ioeCause; + } + } + } + + + /** + * An action to execute with the given return type. + * + * @param the return type of the action + */ + protected interface NiFiRegistryAction { + + T execute(); + + } + + /** + * @param e an exception that was encountered interacting with the registry + * @return the IOException that caused this exception, or null if the an IOException did not cause this exception + */ + protected Throwable getIOExceptionCause(final Throwable e) { + if (e == null) { + return null; + } + + if (e instanceof IOException) { + return e; + } + + return getIOExceptionCause(e.getCause()); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/BucketItemDeserializer.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/BucketItemDeserializer.java new file mode 100644 index 0000000000..01e1975116 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/BucketItemDeserializer.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client.impl; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.bucket.BucketItem; +import org.apache.nifi.registry.bucket.BucketItemType; +import org.apache.nifi.registry.extension.bundle.Bundle; +import org.apache.nifi.registry.flow.VersionedFlow; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class BucketItemDeserializer extends StdDeserializer { + + public BucketItemDeserializer() { + super(BucketItem[].class); + } + + @Override + public BucketItem[] deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { + final JsonNode arrayNode = jsonParser.getCodec().readTree(jsonParser); + + final List bucketItems = new ArrayList<>(); + + final Iterator nodeIter = arrayNode.elements(); + while (nodeIter.hasNext()) { + final JsonNode node = nodeIter.next(); + + final String type = node.get("type").asText(); + if (StringUtils.isBlank(type)) { + throw new IllegalStateException("BucketItem type cannot be null or blank"); + } + + final BucketItemType bucketItemType; + try { + bucketItemType = BucketItemType.valueOf(type); + } catch (Exception e) { + throw new IllegalStateException("Unknown type for BucketItem: " + type, e); + } + + + switch (bucketItemType) { + case Flow: + final VersionedFlow versionedFlow = jsonParser.getCodec().treeToValue(node, VersionedFlow.class); + bucketItems.add(versionedFlow); + break; + case Bundle: + final Bundle bundle = jsonParser.getCodec().treeToValue(node, Bundle.class); + bucketItems.add(bundle); + break; + default: + throw new IllegalStateException("Unknown type for BucketItem: " + bucketItemType); + } + } + + return bucketItems.toArray(new BucketItem[bucketItems.size()]); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/ClientUtils.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/ClientUtils.java new file mode 100644 index 0000000000..98614fb824 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/ClientUtils.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client.impl; + +import org.apache.commons.lang3.StringUtils; + +import javax.ws.rs.core.Response; +import java.io.File; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; + +public class ClientUtils { + + public static File getExtensionBundleVersionContent(final Response response, final File outputDirectory) { + final String contentDispositionHeader = response.getHeaderString("Content-Disposition"); + if (StringUtils.isBlank(contentDispositionHeader)) { + throw new IllegalStateException("Content-Disposition header was blank or missing"); + } + + final int equalsIndex = contentDispositionHeader.lastIndexOf("="); + final String filename = contentDispositionHeader.substring(equalsIndex + 1).trim(); + final File bundleFile = new File(outputDirectory, filename); + + try (final InputStream responseInputStream = response.readEntity(InputStream.class)) { + Files.copy(responseInputStream, bundleFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + return bundleFile; + } catch (Exception e) { + throw new IllegalStateException("Unable to write bundle content due to: " + e.getMessage(), e); + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyAccessClient.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyAccessClient.java new file mode 100644 index 0000000000..4913f15743 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyAccessClient.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client.impl; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.client.AccessClient; +import org.apache.nifi.registry.client.NiFiRegistryException; +import org.apache.nifi.registry.client.RequestConfig; +import org.apache.nifi.registry.client.impl.request.BasicAuthRequestConfig; +import org.apache.nifi.registry.client.impl.request.BearerTokenRequestConfig; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.client.WebTarget; +import java.io.IOException; +import java.util.Map; + +/** + * Jersey implementation of AccessClient. + */ +public class JerseyAccessClient extends AbstractJerseyClient implements AccessClient { + + private final WebTarget accessTarget; + + public JerseyAccessClient(final WebTarget baseTarget) { + super(null); + this.accessTarget = baseTarget.path("/access"); + } + + @Override + public String getToken(final String username, final String password) throws NiFiRegistryException, IOException { + if (StringUtils.isBlank(username)) { + throw new IllegalArgumentException("Username is required"); + } + + if (StringUtils.isBlank(password)) { + throw new IllegalArgumentException("Password is required"); + } + + return executeAction("Error performing login", () -> { + final WebTarget target = accessTarget.path("token/login"); + final Invocation.Builder requestBuilder = getRequestBuilder(target); + + final RequestConfig basicCredsConfig = new BasicAuthRequestConfig(username, password); + final Map basicAuthHeaders = basicCredsConfig.getHeaders(); + basicAuthHeaders.entrySet().stream().forEach(e -> requestBuilder.header(e.getKey(), e.getValue())); + + return requestBuilder.post(Entity.json(null), String.class); + }); + } + + @Override + public String getTokenFromKerberosTicket() throws NiFiRegistryException, IOException { + return executeAction("Error performing kerberos login", () -> { + final WebTarget target = accessTarget.path("token/kerberos"); + return getRequestBuilder(target).post(Entity.json(null), String.class); + }); + } + + @Override + public void logout(final String token) throws IOException, NiFiRegistryException { + if (StringUtils.isBlank(token)) { + throw new IllegalArgumentException("Token is required"); + } + + executeAction("Error performing logout", () -> { + final WebTarget target = accessTarget.path("logout"); + final Invocation.Builder requestBuilder = getRequestBuilder(target); + + final RequestConfig tokenConfig = new BearerTokenRequestConfig(token); + final Map bearerHeaders = tokenConfig.getHeaders(); + bearerHeaders.entrySet().stream().forEach(e -> requestBuilder.header(e.getKey(), e.getValue())); + + requestBuilder.delete(); + return null; + }); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBucketClient.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBucketClient.java new file mode 100644 index 0000000000..6d35998f79 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBucketClient.java @@ -0,0 +1,148 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client.impl; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.client.BucketClient; +import org.apache.nifi.registry.client.NiFiRegistryException; +import org.apache.nifi.registry.client.RequestConfig; +import org.apache.nifi.registry.field.Fields; +import org.apache.nifi.registry.revision.entity.RevisionInfo; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MediaType; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Jersey implementation of BucketClient. + */ +public class JerseyBucketClient extends AbstractJerseyClient implements BucketClient { + + private final WebTarget bucketsTarget; + + + public JerseyBucketClient(final WebTarget baseTarget) { + this(baseTarget, null); + } + + public JerseyBucketClient(final WebTarget baseTarget, final RequestConfig requestConfig) { + super(requestConfig); + this.bucketsTarget = baseTarget.path("/buckets"); + } + + @Override + public Bucket create(final Bucket bucket) throws NiFiRegistryException, IOException { + if (bucket == null) { + throw new IllegalArgumentException("Bucket cannot be null"); + } + + return executeAction("Error creating bucket", () -> { + return getRequestBuilder(bucketsTarget) + .post( + Entity.entity(bucket, MediaType.APPLICATION_JSON), + Bucket.class + ); + }); + + } + + @Override + public Bucket get(final String bucketId) throws NiFiRegistryException, IOException { + if (StringUtils.isBlank(bucketId)) { + throw new IllegalArgumentException("Bucket ID cannot be blank"); + } + + return executeAction("Error retrieving bucket", () -> { + final WebTarget target = bucketsTarget + .path("/{bucketId}") + .resolveTemplate("bucketId", bucketId); + + return getRequestBuilder(target).get(Bucket.class); + }); + + } + + @Override + public Bucket update(final Bucket bucket) throws NiFiRegistryException, IOException { + if (bucket == null) { + throw new IllegalArgumentException("Bucket cannot be null"); + } + + if (StringUtils.isBlank(bucket.getIdentifier())) { + throw new IllegalArgumentException("Bucket Identifier must be provided"); + } + + return executeAction("Error updating bucket", () -> { + final WebTarget target = bucketsTarget + .path("/{bucketId}") + .resolveTemplate("bucketId", bucket.getIdentifier()); + + return getRequestBuilder(target) + .put( + Entity.entity(bucket, MediaType.APPLICATION_JSON), + Bucket.class + ); + + }); + } + + @Override + public Bucket delete(final String bucketId) throws NiFiRegistryException, IOException { + return delete(bucketId, null); + } + + @Override + public Bucket delete(final String bucketId, final RevisionInfo revision) throws NiFiRegistryException, IOException { + if (StringUtils.isBlank(bucketId)) { + throw new IllegalArgumentException("Bucket ID cannot be blank"); + } + + return executeAction("Error deleting bucket", () -> { + WebTarget target = bucketsTarget + .path("/{bucketId}") + .resolveTemplate("bucketId", bucketId); + + target = addRevisionQueryParams(target, revision); + + return getRequestBuilder(target).delete(Bucket.class); + }); + } + + @Override + public Fields getFields() throws NiFiRegistryException, IOException { + return executeAction("Error retrieving bucket field info", () -> { + final WebTarget target = bucketsTarget + .path("/fields"); + + return getRequestBuilder(target).get(Fields.class); + }); + } + + @Override + public List getAll() throws NiFiRegistryException, IOException { + return executeAction("Error retrieving all buckets", () -> { + final Bucket[] buckets = getRequestBuilder(bucketsTarget).get(Bucket[].class); + return buckets == null ? Collections.emptyList() : Arrays.asList(buckets); + }); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBundleClient.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBundleClient.java new file mode 100644 index 0000000000..a3ce5aa96f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBundleClient.java @@ -0,0 +1,121 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client.impl; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.client.BundleClient; +import org.apache.nifi.registry.client.NiFiRegistryException; +import org.apache.nifi.registry.client.RequestConfig; +import org.apache.nifi.registry.extension.bundle.Bundle; +import org.apache.nifi.registry.extension.bundle.BundleFilterParams; + +import javax.ws.rs.client.WebTarget; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Jersey implementation of BundleClient. + */ +public class JerseyBundleClient extends AbstractJerseyClient implements BundleClient { + + private final WebTarget bucketExtensionBundlesTarget; + private final WebTarget extensionBundlesTarget; + + public JerseyBundleClient(final WebTarget baseTarget) { + this(baseTarget, null); + } + + public JerseyBundleClient(final WebTarget baseTarget, final RequestConfig requestConfig) { + super(requestConfig); + this.bucketExtensionBundlesTarget = baseTarget.path("buckets/{bucketId}/bundles"); + this.extensionBundlesTarget = baseTarget.path("bundles"); + } + + @Override + public List getAll() throws IOException, NiFiRegistryException { + return getAll(null); + } + + @Override + public List getAll(final BundleFilterParams filterParams) throws IOException, NiFiRegistryException { + return executeAction("Error getting extension bundles", () -> { + WebTarget target = extensionBundlesTarget; + + if (filterParams != null) { + if (!StringUtils.isBlank(filterParams.getBucketName())) { + target = target.queryParam("bucketName", filterParams.getBucketName()); + } + if (!StringUtils.isBlank(filterParams.getGroupId())) { + target = target.queryParam("groupId", filterParams.getGroupId()); + } + if (!StringUtils.isBlank(filterParams.getArtifactId())) { + target = target.queryParam("artifactId", filterParams.getArtifactId()); + } + } + + final Bundle[] bundles = getRequestBuilder(target).get(Bundle[].class); + return bundles == null ? Collections.emptyList() : Arrays.asList(bundles); + }); + } + + @Override + public List getByBucket(final String bucketId) throws IOException, NiFiRegistryException { + if (StringUtils.isBlank(bucketId)) { + throw new IllegalArgumentException("Bucket id cannot be null or blank"); + } + + return executeAction("Error getting extension bundles for bucket", () -> { + WebTarget target = bucketExtensionBundlesTarget.resolveTemplate("bucketId", bucketId); + + final Bundle[] bundles = getRequestBuilder(target).get(Bundle[].class); + return bundles == null ? Collections.emptyList() : Arrays.asList(bundles); + }); + } + + @Override + public Bundle get(final String bundleId) throws IOException, NiFiRegistryException { + if (StringUtils.isBlank(bundleId)) { + throw new IllegalArgumentException("Bundle id cannot be null or blank"); + } + + return executeAction("Error getting extension bundle", () -> { + WebTarget target = extensionBundlesTarget + .path("{bundleId}") + .resolveTemplate("bundleId", bundleId); + + return getRequestBuilder(target).get(Bundle.class); + }); + } + + @Override + public Bundle delete(final String bundleId) throws IOException, NiFiRegistryException { + if (StringUtils.isBlank(bundleId)) { + throw new IllegalArgumentException("Bundle id cannot be null or blank"); + } + + return executeAction("Error deleting extension bundle", () -> { + WebTarget target = extensionBundlesTarget + .path("{bundleId}") + .resolveTemplate("bundleId", bundleId); + + return getRequestBuilder(target).delete(Bundle.class); + }); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBundleVersionClient.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBundleVersionClient.java new file mode 100644 index 0000000000..27775d0aa6 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBundleVersionClient.java @@ -0,0 +1,370 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client.impl; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.client.BundleVersionClient; +import org.apache.nifi.registry.client.NiFiRegistryException; +import org.apache.nifi.registry.client.RequestConfig; +import org.apache.nifi.registry.extension.bundle.BundleType; +import org.apache.nifi.registry.extension.bundle.BundleVersion; +import org.apache.nifi.registry.extension.bundle.BundleVersionFilterParams; +import org.apache.nifi.registry.extension.bundle.BundleVersionMetadata; +import org.apache.nifi.registry.extension.component.ExtensionMetadata; +import org.apache.nifi.registry.extension.component.manifest.Extension; +import org.glassfish.jersey.media.multipart.FormDataMultiPart; +import org.glassfish.jersey.media.multipart.file.FileDataBodyPart; +import org.glassfish.jersey.media.multipart.file.StreamDataBodyPart; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Jersey implementation of BundleVersionClient. + */ +public class JerseyBundleVersionClient extends AbstractJerseyClient implements BundleVersionClient { + + private final WebTarget bucketExtensionBundlesTarget; + private final WebTarget extensionBundlesTarget; + + public JerseyBundleVersionClient(final WebTarget baseTarget) { + this(baseTarget, null); + } + + public JerseyBundleVersionClient(final WebTarget baseTarget, final RequestConfig requestConfig) { + super(requestConfig); + this.bucketExtensionBundlesTarget = baseTarget.path("buckets/{bucketId}/bundles"); + this.extensionBundlesTarget = baseTarget.path("bundles"); + } + + @Override + public BundleVersion create(final String bucketId, final BundleType bundleType, final InputStream bundleContentStream) + throws IOException, NiFiRegistryException { + return create(bucketId, bundleType, bundleContentStream, null); + } + + @Override + public BundleVersion create(final String bucketId, final BundleType bundleType, final InputStream bundleContentStream, final String sha256) + throws IOException, NiFiRegistryException { + + if (StringUtils.isBlank(bucketId)) { + throw new IllegalArgumentException("Bucket id cannot be null or blank"); + } + + if (bundleType == null) { + throw new IllegalArgumentException("Bundle type cannot be null"); + } + + if (bundleContentStream == null) { + throw new IllegalArgumentException("Bundle content cannot be null"); + } + + return executeAction("Error creating extension bundle version", () -> { + final WebTarget target = bucketExtensionBundlesTarget + .path("{bundleType}") + .resolveTemplate("bucketId", bucketId) + .resolveTemplate("bundleType", bundleType.toString()); + + final StreamDataBodyPart streamBodyPart = new StreamDataBodyPart("file", bundleContentStream); + + final FormDataMultiPart multipart = new FormDataMultiPart(); + multipart.bodyPart(streamBodyPart); + + if (!StringUtils.isBlank(sha256)) { + multipart.field("sha256", sha256); + } + + return getRequestBuilder(target) + .post( + Entity.entity(multipart, multipart.getMediaType()), + BundleVersion.class + ); + }); + } + + @Override + public BundleVersion create(final String bucketId, final BundleType bundleType, final File bundleFile) + throws IOException, NiFiRegistryException { + return create(bucketId, bundleType, bundleFile, null); + } + + @Override + public BundleVersion create(final String bucketId, final BundleType bundleType, final File bundleFile, final String sha256) + throws IOException, NiFiRegistryException { + + if (StringUtils.isBlank(bucketId)) { + throw new IllegalArgumentException("Bucket id cannot be null or blank"); + } + + if (bundleType == null) { + throw new IllegalArgumentException("Bundle type cannot be null"); + } + + if (bundleFile == null) { + throw new IllegalArgumentException("Bundle file cannot be null"); + } + + return executeAction("Error creating extension bundle version", () -> { + final WebTarget target = bucketExtensionBundlesTarget + .path("{bundleType}") + .resolveTemplate("bucketId", bucketId) + .resolveTemplate("bundleType", bundleType.toString()); + + final FileDataBodyPart fileBodyPart = new FileDataBodyPart("file", bundleFile, MediaType.APPLICATION_OCTET_STREAM_TYPE); + + final FormDataMultiPart multipart = new FormDataMultiPart(); + multipart.bodyPart(fileBodyPart); + + if (!StringUtils.isBlank(sha256)) { + multipart.field("sha256", sha256); + } + + return getRequestBuilder(target) + .post( + Entity.entity(multipart, multipart.getMediaType()), + BundleVersion.class + ); + }); + } + + @Override + public List getBundleVersions(final BundleVersionFilterParams filterParams) + throws IOException, NiFiRegistryException { + + return executeAction("Error getting extension bundle versions", () -> { + WebTarget target = extensionBundlesTarget.path("/versions"); + + if (filterParams != null) { + if (!StringUtils.isBlank(filterParams.getGroupId())) { + target = target.queryParam("groupId", filterParams.getGroupId()); + } + + if (!StringUtils.isBlank(filterParams.getArtifactId())) { + target = target.queryParam("artifactId", filterParams.getArtifactId()); + } + + if (!StringUtils.isBlank(filterParams.getVersion())) { + target = target.queryParam("version", filterParams.getVersion()); + } + } + + final BundleVersionMetadata[] bundleVersions = getRequestBuilder(target).get(BundleVersionMetadata[].class); + return bundleVersions == null ? Collections.emptyList() : Arrays.asList(bundleVersions); + }); + } + + @Override + public List getBundleVersions(final String bundleId) + throws IOException, NiFiRegistryException { + + if (StringUtils.isBlank(bundleId)) { + throw new IllegalArgumentException("Bundle id cannot be null or blank"); + } + + return executeAction("Error getting extension bundle versions", () -> { + final WebTarget target = extensionBundlesTarget + .path("{bundleId}/versions") + .resolveTemplate("bundleId", bundleId); + + final BundleVersionMetadata[] bundleVersions = getRequestBuilder(target).get(BundleVersionMetadata[].class); + return bundleVersions == null ? Collections.emptyList() : Arrays.asList(bundleVersions); + }); + } + + @Override + public BundleVersion getBundleVersion(final String bundleId, final String version) + throws IOException, NiFiRegistryException { + + if (StringUtils.isBlank(bundleId)) { + throw new IllegalArgumentException("Bundle id cannot be null or blank"); + } + + if (StringUtils.isBlank(version)) { + throw new IllegalArgumentException("Version cannot be null or blank"); + } + + return executeAction("Error getting extension bundle version", () -> { + final WebTarget target = extensionBundlesTarget + .path("{bundleId}/versions/{version}") + .resolveTemplate("bundleId", bundleId) + .resolveTemplate("version", version); + + return getRequestBuilder(target).get(BundleVersion.class); + }); + } + + @Override + public List getExtensions(final String bundleId, final String version) + throws IOException, NiFiRegistryException { + + if (StringUtils.isBlank(bundleId)) { + throw new IllegalArgumentException("Bundle id cannot be null or blank"); + } + + if (StringUtils.isBlank(version)) { + throw new IllegalArgumentException("Version cannot be null or blank"); + } + + return executeAction("Error getting extension bundle metadata", () -> { + final WebTarget target = extensionBundlesTarget + .path("{bundleId}/versions/{version}/extensions") + .resolveTemplate("bundleId", bundleId) + .resolveTemplate("version", version); + + final ExtensionMetadata[] extensions = getRequestBuilder(target).get(ExtensionMetadata[].class); + return extensions == null ? Collections.emptyList() : Arrays.asList(extensions); + }); + } + + @Override + public Extension getExtension(final String bundleId, final String version, final String name) throws IOException, NiFiRegistryException { + if (StringUtils.isBlank(bundleId)) { + throw new IllegalArgumentException("Bundle id cannot be null or blank"); + } + + if (StringUtils.isBlank(version)) { + throw new IllegalArgumentException("Version cannot be null or blank"); + } + + if (StringUtils.isBlank(name)) { + throw new IllegalArgumentException("Extension name cannot be null or blank"); + } + + return executeAction("Error getting extension", () -> { + final WebTarget target = extensionBundlesTarget + .path("{bundleId}/versions/{version}/extensions/{name}") + .resolveTemplate("bundleId", bundleId) + .resolveTemplate("version", version) + .resolveTemplate("name", name); + + final Extension extension = getRequestBuilder(target).get(Extension.class); + return extension; + }); + } + + @Override + public InputStream getExtensionDocs(final String bundleId, final String version, final String name) throws IOException, NiFiRegistryException { + if (StringUtils.isBlank(bundleId)) { + throw new IllegalArgumentException("Bundle id cannot be null or blank"); + } + + if (StringUtils.isBlank(version)) { + throw new IllegalArgumentException("Version cannot be null or blank"); + } + + if (StringUtils.isBlank(name)) { + throw new IllegalArgumentException("Extension name cannot be null or blank"); + } + + return executeAction("Error getting extension", () -> { + final WebTarget target = extensionBundlesTarget + .path("{bundleId}/versions/{version}/extensions/{name}/docs") + .resolveTemplate("bundleId", bundleId) + .resolveTemplate("version", version) + .resolveTemplate("name", name); + + return getRequestBuilder(target) + .accept(MediaType.TEXT_HTML) + .get() + .readEntity(InputStream.class); + }); + } + + @Override + public InputStream getBundleVersionContent(final String bundleId, final String version) + throws IOException, NiFiRegistryException { + + if (StringUtils.isBlank(bundleId)) { + throw new IllegalArgumentException("Bundle id cannot be null or blank"); + } + + if (StringUtils.isBlank(version)) { + throw new IllegalArgumentException("Version cannot be null or blank"); + } + + return executeAction("Error getting extension bundle version", () -> { + final WebTarget target = extensionBundlesTarget + .path("{bundleId}/versions/{version}/content") + .resolveTemplate("bundleId", bundleId) + .resolveTemplate("version", version); + + return getRequestBuilder(target) + .accept(MediaType.APPLICATION_OCTET_STREAM_TYPE) + .get() + .readEntity(InputStream.class); + }); + } + + @Override + public File writeBundleVersionContent(final String bundleId, final String version, final File directory) + throws IOException, NiFiRegistryException { + + if (StringUtils.isBlank(bundleId)) { + throw new IllegalArgumentException("Bundle id cannot be null or blank"); + } + + if (StringUtils.isBlank(version)) { + throw new IllegalArgumentException("Version cannot be null or blank"); + } + + if (directory == null || !directory.exists() || !directory.isDirectory()) { + throw new IllegalArgumentException("Directory must exist and be a valid directory"); + } + + return executeAction("Error getting extension bundle version", () -> { + final WebTarget target = extensionBundlesTarget + .path("{bundleId}/versions/{version}/content") + .resolveTemplate("bundleId", bundleId) + .resolveTemplate("version", version); + + final Response response = getRequestBuilder(target) + .accept(MediaType.APPLICATION_OCTET_STREAM_TYPE) + .get(); + + return ClientUtils.getExtensionBundleVersionContent(response, directory); + }); + } + + @Override + public BundleVersion delete(final String bundleId, final String version) throws IOException, NiFiRegistryException { + if (StringUtils.isBlank(bundleId)) { + throw new IllegalArgumentException("Bundle id cannot be null or blank"); + } + + if (StringUtils.isBlank(version)) { + throw new IllegalArgumentException("Version cannot be null or blank"); + } + + return executeAction("Error deleting extension bundle version", () -> { + final WebTarget target = extensionBundlesTarget + .path("{bundleId}/versions/{version}") + .resolveTemplate("bundleId", bundleId) + .resolveTemplate("version", version); + + return getRequestBuilder(target).delete(BundleVersion.class); + }); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionClient.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionClient.java new file mode 100644 index 0000000000..bbd440e2bd --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionClient.java @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client.impl; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.client.ExtensionClient; +import org.apache.nifi.registry.client.NiFiRegistryException; +import org.apache.nifi.registry.client.RequestConfig; +import org.apache.nifi.registry.extension.bundle.BundleType; +import org.apache.nifi.registry.extension.component.ExtensionFilterParams; +import org.apache.nifi.registry.extension.component.ExtensionMetadataContainer; +import org.apache.nifi.registry.extension.component.TagCount; +import org.apache.nifi.registry.extension.component.manifest.ExtensionType; +import org.apache.nifi.registry.extension.component.manifest.ProvidedServiceAPI; + +import javax.ws.rs.client.WebTarget; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +public class JerseyExtensionClient extends AbstractJerseyClient implements ExtensionClient { + + private final WebTarget extensionsTarget; + + public JerseyExtensionClient(final WebTarget baseTarget) { + this(baseTarget, null); + } + + public JerseyExtensionClient(final WebTarget baseTarget, final RequestConfig requestConfig) { + super(requestConfig); + this.extensionsTarget = baseTarget.path("extensions"); + } + + @Override + public ExtensionMetadataContainer findExtensions(final ExtensionFilterParams filterParams) + throws IOException, NiFiRegistryException { + + return executeAction("Error retrieving extensions", () -> { + WebTarget target = extensionsTarget; + + if (filterParams != null) { + final BundleType bundleType = filterParams.getBundleType(); + if (bundleType != null) { + target = target.queryParam("bundleType", bundleType.toString()); + } + + final ExtensionType extensionType = filterParams.getExtensionType(); + if (extensionType != null) { + target = target.queryParam("extensionType", extensionType.toString()); + } + + final Set tags = filterParams.getTags(); + if (tags != null) { + for (final String tag : tags) { + target = target.queryParam("tag", tag); + } + } + } + + return getRequestBuilder(target).get(ExtensionMetadataContainer.class); + }); + } + + @Override + public ExtensionMetadataContainer findExtensions(final ProvidedServiceAPI serviceAPI) throws IOException, NiFiRegistryException { + if (serviceAPI == null + || StringUtils.isBlank(serviceAPI.getClassName()) + || StringUtils.isBlank(serviceAPI.getGroupId()) + || StringUtils.isBlank(serviceAPI.getArtifactId()) + || StringUtils.isBlank(serviceAPI.getVersion())) { + throw new IllegalArgumentException("Provided service API must be specified with a class, group, artifact, and version"); + } + + return executeAction("Error retrieving extensions", () -> { + WebTarget target = extensionsTarget.path("provided-service-api"); + target = target.queryParam("className", serviceAPI.getClassName()); + target = target.queryParam("groupId", serviceAPI.getGroupId()); + target = target.queryParam("artifactId", serviceAPI.getArtifactId()); + target = target.queryParam("version", serviceAPI.getVersion()); + + return getRequestBuilder(target).get(ExtensionMetadataContainer.class); + }); + } + + @Override + public List getTagCounts() throws IOException, NiFiRegistryException { + return executeAction("Error retrieving tag counts", () -> { + final WebTarget target = extensionsTarget.path("tags"); + + final TagCount[] tagCounts = getRequestBuilder(target).get(TagCount[].class); + return tagCounts == null ? Collections.emptyList() : Arrays.asList(tagCounts); + }); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionRepoClient.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionRepoClient.java new file mode 100644 index 0000000000..3a0daf5101 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionRepoClient.java @@ -0,0 +1,335 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client.impl; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.client.ExtensionRepoClient; +import org.apache.nifi.registry.client.NiFiRegistryException; +import org.apache.nifi.registry.client.RequestConfig; +import org.apache.nifi.registry.extension.component.manifest.Extension; +import org.apache.nifi.registry.extension.repo.ExtensionRepoArtifact; +import org.apache.nifi.registry.extension.repo.ExtensionRepoBucket; +import org.apache.nifi.registry.extension.repo.ExtensionRepoExtensionMetadata; +import org.apache.nifi.registry.extension.repo.ExtensionRepoGroup; +import org.apache.nifi.registry.extension.repo.ExtensionRepoVersion; +import org.apache.nifi.registry.extension.repo.ExtensionRepoVersionSummary; + +import javax.ws.rs.NotFoundException; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +public class JerseyExtensionRepoClient extends AbstractJerseyClient implements ExtensionRepoClient { + + private WebTarget extensionRepoTarget; + + public JerseyExtensionRepoClient(final WebTarget baseTarget) { + this(baseTarget, null); + } + + public JerseyExtensionRepoClient(final WebTarget baseTarget, final RequestConfig requestConfig) { + super(requestConfig); + this.extensionRepoTarget = baseTarget.path("extension-repository"); + } + + @Override + public List getBuckets() throws IOException, NiFiRegistryException { + return executeAction("Error retrieving buckets for extension repo", () -> { + final ExtensionRepoBucket[] repoBuckets = getRequestBuilder(extensionRepoTarget).get(ExtensionRepoBucket[].class); + return repoBuckets == null ? Collections.emptyList() : Arrays.asList(repoBuckets); + }); + } + + @Override + public List getGroups(final String bucketName) throws IOException, NiFiRegistryException { + if (StringUtils.isBlank(bucketName)) { + throw new IllegalArgumentException("Bucket name cannot be null or blank"); + } + + return executeAction("Error retrieving groups for extension repo", () -> { + final WebTarget target = extensionRepoTarget + .path("{bucketName}") + .resolveTemplate("bucketName", bucketName); + + final ExtensionRepoGroup[] repoGroups = getRequestBuilder(target).get(ExtensionRepoGroup[].class); + return repoGroups == null ? Collections.emptyList() : Arrays.asList(repoGroups); + }); + } + + @Override + public List getArtifacts(final String bucketName, final String groupId) + throws IOException, NiFiRegistryException { + + if (StringUtils.isBlank(bucketName)) { + throw new IllegalArgumentException("Bucket name cannot be null or blank"); + } + + if (StringUtils.isBlank(groupId)) { + throw new IllegalArgumentException("Group id cannot be null or blank"); + } + + return executeAction("Error retrieving artifacts for extension repo", () -> { + final WebTarget target = extensionRepoTarget + .path("{bucketName}/{groupId}") + .resolveTemplate("bucketName", bucketName) + .resolveTemplate("groupId", groupId); + + final ExtensionRepoArtifact[] repoArtifacts = getRequestBuilder(target).get(ExtensionRepoArtifact[].class); + return repoArtifacts == null ? Collections.emptyList() : Arrays.asList(repoArtifacts); + }); + } + + @Override + public List getVersions(final String bucketName, final String groupId, final String artifactId) + throws IOException, NiFiRegistryException { + + if (StringUtils.isBlank(bucketName)) { + throw new IllegalArgumentException("Bucket name cannot be null or blank"); + } + + if (StringUtils.isBlank(groupId)) { + throw new IllegalArgumentException("Group id cannot be null or blank"); + } + + if (StringUtils.isBlank(artifactId)) { + throw new IllegalArgumentException("Artifact id cannot be null or blank"); + } + + return executeAction("Error retrieving versions for extension repo", () -> { + final WebTarget target = extensionRepoTarget + .path("{bucketName}/{groupId}/{artifactId}") + .resolveTemplate("bucketName", bucketName) + .resolveTemplate("groupId", groupId) + .resolveTemplate("artifactId", artifactId); + + final ExtensionRepoVersionSummary[] repoVersions = getRequestBuilder(target).get(ExtensionRepoVersionSummary[].class); + return repoVersions == null ? Collections.emptyList() : Arrays.asList(repoVersions); + }); + } + + @Override + public ExtensionRepoVersion getVersion(final String bucketName, final String groupId, final String artifactId, final String version) + throws IOException, NiFiRegistryException { + + validate(bucketName, groupId, artifactId, version); + + return executeAction("Error retrieving versions for extension repo", () -> { + final WebTarget target = extensionRepoTarget + .path("{bucketName}/{groupId}/{artifactId}/{version}") + .resolveTemplate("bucketName", bucketName) + .resolveTemplate("groupId", groupId) + .resolveTemplate("artifactId", artifactId) + .resolveTemplate("version", version); + + return getRequestBuilder(target).get(ExtensionRepoVersion.class); + }); + } + + @Override + public List getVersionExtensions(final String bucketName, final String groupId, final String artifactId, final String version) + throws IOException, NiFiRegistryException { + + validate(bucketName, groupId, artifactId, version); + + return executeAction("Error retrieving versions for extension repo", () -> { + final WebTarget target = extensionRepoTarget + .path("{bucketName}/{groupId}/{artifactId}/{version}/extensions") + .resolveTemplate("bucketName", bucketName) + .resolveTemplate("groupId", groupId) + .resolveTemplate("artifactId", artifactId) + .resolveTemplate("version", version); + + final ExtensionRepoExtensionMetadata[] extensions = getRequestBuilder(target).get(ExtensionRepoExtensionMetadata[].class); + return extensions == null ? Collections.emptyList() : Arrays.asList(extensions); + }); + } + + @Override + public Extension getVersionExtension(final String bucketName, final String groupId, final String artifactId, + final String version, final String extensionName) + throws IOException, NiFiRegistryException { + + validate(bucketName, groupId, artifactId, version); + + if (StringUtils.isBlank(extensionName)) { + throw new IllegalArgumentException("Extension name is required"); + } + + return executeAction("Error retrieving versions for extension repo", () -> { + final WebTarget target = extensionRepoTarget + .path("{bucketName}/{groupId}/{artifactId}/{version}/extensions/{extensionName}") + .resolveTemplate("bucketName", bucketName) + .resolveTemplate("groupId", groupId) + .resolveTemplate("artifactId", artifactId) + .resolveTemplate("version", version) + .resolveTemplate("extensionName", extensionName); + + final Extension extension = getRequestBuilder(target).get(Extension.class); + return extension; + }); + } + + @Override + public InputStream getVersionExtensionDocs(final String bucketName, final String groupId, final String artifactId, + final String version, final String extensionName) + throws IOException, NiFiRegistryException { + + validate(bucketName, groupId, artifactId, version); + + if (StringUtils.isBlank(extensionName)) { + throw new IllegalArgumentException("Extension name is required"); + } + + return executeAction("Error retrieving versions for extension repo", () -> { + final WebTarget target = extensionRepoTarget + .path("{bucketName}/{groupId}/{artifactId}/{version}/extensions/{extensionName}/docs") + .resolveTemplate("bucketName", bucketName) + .resolveTemplate("groupId", groupId) + .resolveTemplate("artifactId", artifactId) + .resolveTemplate("version", version) + .resolveTemplate("extensionName", extensionName); + + return getRequestBuilder(target) + .accept(MediaType.TEXT_HTML) + .get() + .readEntity(InputStream.class); + }); + } + + @Override + public InputStream getVersionContent(final String bucketName, final String groupId, final String artifactId, final String version) + throws IOException, NiFiRegistryException { + + validate(bucketName, groupId, artifactId, version); + + return executeAction("Error retrieving version content for extension repo", () -> { + final WebTarget target = extensionRepoTarget + .path("{bucketName}/{groupId}/{artifactId}/{version}/content") + .resolveTemplate("bucketName", bucketName) + .resolveTemplate("groupId", groupId) + .resolveTemplate("artifactId", artifactId) + .resolveTemplate("version", version); + + return getRequestBuilder(target) + .accept(MediaType.APPLICATION_OCTET_STREAM_TYPE) + .get() + .readEntity(InputStream.class); + }); + } + + @Override + public File writeBundleVersionContent(final String bucketName, final String groupId, final String artifactId, final String version, final File directory) + throws IOException, NiFiRegistryException { + + validate(bucketName, groupId, artifactId, version); + + if (directory == null) { + throw new IllegalArgumentException("Directory cannot be null"); + } + + return executeAction("Error retrieving version content for extension repo", () -> { + final WebTarget target = extensionRepoTarget + .path("{bucketName}/{groupId}/{artifactId}/{version}/content") + .resolveTemplate("bucketName", bucketName) + .resolveTemplate("groupId", groupId) + .resolveTemplate("artifactId", artifactId) + .resolveTemplate("version", version); + + final Response response = getRequestBuilder(target) + .accept(MediaType.APPLICATION_OCTET_STREAM_TYPE) + .get(); + + return ClientUtils.getExtensionBundleVersionContent(response, directory); + }); + + } + + @Override + public String getVersionSha256(final String bucketName, final String groupId, final String artifactId, final String version) + throws IOException, NiFiRegistryException { + + validate(bucketName, groupId, artifactId, version); + + return executeAction("Error retrieving version content for extension repo", () -> { + final WebTarget target = extensionRepoTarget + .path("{bucketName}/{groupId}/{artifactId}/{version}/sha256") + .resolveTemplate("bucketName", bucketName) + .resolveTemplate("groupId", groupId) + .resolveTemplate("artifactId", artifactId) + .resolveTemplate("version", version); + + return getRequestBuilder(target).accept(MediaType.TEXT_PLAIN_TYPE).get(String.class); + }); + } + + @Override + public Optional getVersionSha256(final String groupId, final String artifactId, final String version) + throws IOException, NiFiRegistryException { + + if (StringUtils.isBlank(groupId)) { + throw new IllegalArgumentException("Group id cannot be null or blank"); + } + + if (StringUtils.isBlank(artifactId)) { + throw new IllegalArgumentException("Artifact id cannot be null or blank"); + } + + if (StringUtils.isBlank(version)) { + throw new IllegalArgumentException("Version cannot be null or blank"); + } + + return executeAction("Error retrieving version content for extension repo", () -> { + final WebTarget target = extensionRepoTarget + .path("{groupId}/{artifactId}/{version}/sha256") + .resolveTemplate("groupId", groupId) + .resolveTemplate("artifactId", artifactId) + .resolveTemplate("version", version); + + try { + final String sha256 = getRequestBuilder(target).accept(MediaType.TEXT_PLAIN_TYPE).get(String.class); + return Optional.of(sha256); + } catch (NotFoundException nfe) { + return Optional.empty(); + } + }); + } + + private void validate(String bucketName, String groupId, String artifactId, String version) { + if (StringUtils.isBlank(bucketName)) { + throw new IllegalArgumentException("Bucket name cannot be null or blank"); + } + + if (StringUtils.isBlank(groupId)) { + throw new IllegalArgumentException("Group id cannot be null or blank"); + } + + if (StringUtils.isBlank(artifactId)) { + throw new IllegalArgumentException("Artifact id cannot be null or blank"); + } + + if (StringUtils.isBlank(version)) { + throw new IllegalArgumentException("Version cannot be null or blank"); + } + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowClient.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowClient.java new file mode 100644 index 0000000000..4a61a30b01 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowClient.java @@ -0,0 +1,214 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client.impl; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.client.FlowClient; +import org.apache.nifi.registry.client.NiFiRegistryException; +import org.apache.nifi.registry.client.RequestConfig; +import org.apache.nifi.registry.diff.VersionedFlowDifference; +import org.apache.nifi.registry.field.Fields; +import org.apache.nifi.registry.flow.VersionedFlow; +import org.apache.nifi.registry.revision.entity.RevisionInfo; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MediaType; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Jersey implementation of FlowClient. + */ +public class JerseyFlowClient extends AbstractJerseyClient implements FlowClient { + + private final WebTarget flowsTarget; + private final WebTarget bucketFlowsTarget; + + public JerseyFlowClient(final WebTarget baseTarget) { + this(baseTarget, null); + } + + public JerseyFlowClient(final WebTarget baseTarget, final RequestConfig requestConfig) { + super(requestConfig); + this.flowsTarget = baseTarget.path("/flows"); + this.bucketFlowsTarget = baseTarget.path("/buckets/{bucketId}/flows"); + } + + @Override + public VersionedFlow create(final VersionedFlow flow) throws NiFiRegistryException, IOException { + if (flow == null) { + throw new IllegalArgumentException("VersionedFlow cannot be null"); + } + + final String bucketId = flow.getBucketIdentifier(); + if (StringUtils.isBlank(bucketId)) { + throw new IllegalArgumentException("Bucket Identifier cannot be blank"); + } + + return executeAction("Error creating flow", () -> { + final WebTarget target = bucketFlowsTarget + .resolveTemplate("bucketId", bucketId); + + return getRequestBuilder(target) + .post( + Entity.entity(flow, MediaType.APPLICATION_JSON), + VersionedFlow.class + ); + }); + } + + @Override + public VersionedFlow get(final String bucketId, final String flowId) throws NiFiRegistryException, IOException { + if (StringUtils.isBlank(bucketId)) { + throw new IllegalArgumentException("Bucket Identifier cannot be blank"); + } + + if (StringUtils.isBlank(flowId)) { + throw new IllegalArgumentException("Flow Identifier cannot be blank"); + } + + return executeAction("Error retrieving flow", () -> { + final WebTarget target = bucketFlowsTarget + .path("/{flowId}") + .resolveTemplate("bucketId", bucketId) + .resolveTemplate("flowId", flowId); + + return getRequestBuilder(target).get(VersionedFlow.class); + }); + } + + @Override + public VersionedFlow get(final String flowId) throws NiFiRegistryException, IOException { + if (StringUtils.isBlank(flowId)) { + throw new IllegalArgumentException("Flow Identifier cannot be blank"); + } + + // this uses the flowsTarget because its calling /flows/{flowId} without knowing a bucketId + return executeAction("Error retrieving flow", () -> { + final WebTarget target = flowsTarget + .path("/{flowId}") + .resolveTemplate("flowId", flowId); + + return getRequestBuilder(target).get(VersionedFlow.class); + }); + } + + @Override + public VersionedFlow update(final String bucketId, final VersionedFlow flow) throws NiFiRegistryException, IOException { + if (StringUtils.isBlank(bucketId)) { + throw new IllegalArgumentException("Bucket Identifier cannot be blank"); + } + + if (flow == null) { + throw new IllegalArgumentException("VersionedFlow cannot be null"); + } + + if (StringUtils.isBlank(flow.getIdentifier())) { + throw new IllegalArgumentException("VersionedFlow identifier must be provided"); + } + + return executeAction("Error updating flow", () -> { + final WebTarget target = bucketFlowsTarget + .path("/{flowId}") + .resolveTemplate("bucketId", bucketId) + .resolveTemplate("flowId", flow.getIdentifier()); + + return getRequestBuilder(target) + .put( + Entity.entity(flow, MediaType.APPLICATION_JSON), + VersionedFlow.class + ); + }); + } + + @Override + public VersionedFlow delete(final String bucketId, final String flowId) throws NiFiRegistryException, IOException { + return delete(bucketId, flowId, null); + } + + @Override + public VersionedFlow delete(final String bucketId, final String flowId, final RevisionInfo revision) + throws NiFiRegistryException, IOException { + if (StringUtils.isBlank(bucketId)) { + throw new IllegalArgumentException("Bucket Identifier cannot be blank"); + } + + if (StringUtils.isBlank(flowId)) { + throw new IllegalArgumentException("Flow Identifier cannot be blank"); + } + + return executeAction("Error deleting flow", () -> { + WebTarget target = bucketFlowsTarget + .path("/{flowId}") + .resolveTemplate("bucketId", bucketId) + .resolveTemplate("flowId", flowId); + + target = addRevisionQueryParams(target, revision); + + return getRequestBuilder(target).delete(VersionedFlow.class); + }); + } + + @Override + public Fields getFields() throws NiFiRegistryException, IOException { + return executeAction("Error retrieving fields info for flows", () -> { + final WebTarget target = flowsTarget.path("/fields"); + return getRequestBuilder(target).get(Fields.class); + }); + } + + @Override + public List getByBucket(final String bucketId) throws NiFiRegistryException, IOException { + if (StringUtils.isBlank(bucketId)) { + throw new IllegalArgumentException("Bucket Identifier cannot be blank"); + } + + return executeAction("Error getting flows for bucket", () -> { + WebTarget target = bucketFlowsTarget; + target = target.resolveTemplate("bucketId", bucketId); + + final VersionedFlow[] versionedFlows = getRequestBuilder(target).get(VersionedFlow[].class); + return versionedFlows == null ? Collections.emptyList() : Arrays.asList(versionedFlows); + }); + } + + @Override + public VersionedFlowDifference diff(final String bucketId, final String flowId, + final Integer versionA, final Integer versionB) throws NiFiRegistryException, IOException { + if (StringUtils.isBlank(bucketId)) { + throw new IllegalArgumentException("Bucket Identifier cannot be blank"); + } + + if (StringUtils.isBlank(flowId)) { + throw new IllegalArgumentException("Flow Identifier cannot be blank"); + } + + return executeAction("Error retrieving flow", () -> { + final WebTarget target = bucketFlowsTarget + .path("/{flowId}/diff/{versionA}/{versionB}") + .resolveTemplate("bucketId", bucketId) + .resolveTemplate("flowId", flowId) + .resolveTemplate("versionA", versionA) + .resolveTemplate("versionB", versionB); + + return getRequestBuilder(target).get(VersionedFlowDifference.class); + }); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowSnapshotClient.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowSnapshotClient.java new file mode 100644 index 0000000000..19890ca2f5 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowSnapshotClient.java @@ -0,0 +1,246 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client.impl; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.client.FlowSnapshotClient; +import org.apache.nifi.registry.client.NiFiRegistryException; +import org.apache.nifi.registry.client.RequestConfig; +import org.apache.nifi.registry.flow.VersionedFlowSnapshot; +import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MediaType; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Jersey implementation of FlowSnapshotClient. + */ +public class JerseyFlowSnapshotClient extends AbstractJerseyClient implements FlowSnapshotClient { + + final WebTarget bucketFlowSnapshotTarget; + final WebTarget flowsFlowSnapshotTarget; + + public JerseyFlowSnapshotClient(final WebTarget baseTarget) { + this(baseTarget, null); + } + + public JerseyFlowSnapshotClient(final WebTarget baseTarget, final RequestConfig requestConfig) { + super(requestConfig); + this.bucketFlowSnapshotTarget = baseTarget.path("/buckets/{bucketId}/flows/{flowId}/versions"); + this.flowsFlowSnapshotTarget = baseTarget.path("/flows/{flowId}/versions"); + } + + @Override + public VersionedFlowSnapshot create(final VersionedFlowSnapshot snapshot) + throws NiFiRegistryException, IOException { + if (snapshot.getSnapshotMetadata() == null) { + throw new IllegalArgumentException("Snapshot Metadata cannot be null"); + } + + final String bucketId = snapshot.getSnapshotMetadata().getBucketIdentifier(); + if (StringUtils.isBlank(bucketId)) { + throw new IllegalArgumentException("Bucket Identifier cannot be blank"); + } + + final String flowId = snapshot.getSnapshotMetadata().getFlowIdentifier(); + if (StringUtils.isBlank(flowId)) { + throw new IllegalArgumentException("Flow Identifier cannot be blank"); + } + + return executeAction("Error creating snapshot", () -> { + final WebTarget target = bucketFlowSnapshotTarget + .resolveTemplate("bucketId", bucketId) + .resolveTemplate("flowId", flowId); + + return getRequestBuilder(target) + .post( + Entity.entity(snapshot, MediaType.APPLICATION_JSON), + VersionedFlowSnapshot.class + ); + }); + } + + @Override + public VersionedFlowSnapshot get(final String bucketId, final String flowId, final int version) + throws NiFiRegistryException, IOException { + if (StringUtils.isBlank(bucketId)) { + throw new IllegalArgumentException("Bucket Identifier cannot be blank"); + } + + if (StringUtils.isBlank(flowId)) { + throw new IllegalArgumentException("Flow Identifier cannot be blank"); + } + + if (version < 1) { + throw new IllegalArgumentException("Version must be greater than 1"); + } + + return executeAction("Error retrieving flow snapshot", () -> { + final WebTarget target = bucketFlowSnapshotTarget + .path("/{version}") + .resolveTemplate("bucketId", bucketId) + .resolveTemplate("flowId", flowId) + .resolveTemplate("version", version); + + return getRequestBuilder(target).get(VersionedFlowSnapshot.class); + }); + } + + @Override + public VersionedFlowSnapshot get(final String flowId, final int version) + throws NiFiRegistryException, IOException { + + if (StringUtils.isBlank(flowId)) { + throw new IllegalArgumentException("Flow Identifier cannot be blank"); + } + + if (version < 1) { + throw new IllegalArgumentException("Version must be greater than 1"); + } + + return executeAction("Error retrieving flow snapshot", () -> { + final WebTarget target = flowsFlowSnapshotTarget + .path("/{version}") + .resolveTemplate("flowId", flowId) + .resolveTemplate("version", version); + + return getRequestBuilder(target).get(VersionedFlowSnapshot.class); + }); + } + + @Override + public VersionedFlowSnapshot getLatest(final String bucketId, final String flowId) + throws NiFiRegistryException, IOException { + if (StringUtils.isBlank(bucketId)) { + throw new IllegalArgumentException("Bucket Identifier cannot be blank"); + } + + if (StringUtils.isBlank(flowId)) { + throw new IllegalArgumentException("Flow Identifier cannot be blank"); + } + + return executeAction("Error retrieving latest snapshot", () -> { + final WebTarget target = bucketFlowSnapshotTarget + .path("/latest") + .resolveTemplate("bucketId", bucketId) + .resolveTemplate("flowId", flowId); + + return getRequestBuilder(target).get(VersionedFlowSnapshot.class); + }); + } + + @Override + public VersionedFlowSnapshot getLatest(final String flowId) + throws NiFiRegistryException, IOException { + if (StringUtils.isBlank(flowId)) { + throw new IllegalArgumentException("Flow Identifier cannot be blank"); + } + + return executeAction("Error retrieving latest snapshot", () -> { + final WebTarget target = flowsFlowSnapshotTarget + .path("/latest") + .resolveTemplate("flowId", flowId); + + return getRequestBuilder(target).get(VersionedFlowSnapshot.class); + }); + } + + @Override + public VersionedFlowSnapshotMetadata getLatestMetadata(final String bucketId, final String flowId) throws NiFiRegistryException, IOException { + if (StringUtils.isBlank(bucketId)) { + throw new IllegalArgumentException("Bucket Identifier cannot be blank"); + } + + if (StringUtils.isBlank(flowId)) { + throw new IllegalArgumentException("Flow Identifier cannot be blank"); + } + + return executeAction("Error retrieving latest snapshot metadata", () -> { + final WebTarget target = bucketFlowSnapshotTarget + .path("/latest/metadata") + .resolveTemplate("bucketId", bucketId) + .resolveTemplate("flowId", flowId); + + return getRequestBuilder(target).get(VersionedFlowSnapshotMetadata.class); + }); + } + + @Override + public VersionedFlowSnapshotMetadata getLatestMetadata(final String flowId) throws NiFiRegistryException, IOException { + if (StringUtils.isBlank(flowId)) { + throw new IllegalArgumentException("Flow Identifier cannot be blank"); + } + + return executeAction("Error retrieving latest snapshot metadata", () -> { + final WebTarget target = flowsFlowSnapshotTarget + .path("/latest/metadata") + .resolveTemplate("flowId", flowId); + + return getRequestBuilder(target).get(VersionedFlowSnapshotMetadata.class); + }); + } + + @Override + @SuppressWarnings("unchecked") + public List getSnapshotMetadata(final String bucketId, final String flowId) + throws NiFiRegistryException, IOException { + if (StringUtils.isBlank(bucketId)) { + throw new IllegalArgumentException("Bucket Identifier cannot be blank"); + } + + if (StringUtils.isBlank(flowId)) { + throw new IllegalArgumentException("Flow Identifier cannot be blank"); + } + + return executeAction("Error retrieving snapshot metadata", () -> { + final WebTarget target = bucketFlowSnapshotTarget + .resolveTemplate("bucketId", bucketId) + .resolveTemplate("flowId", flowId); + + final VersionedFlowSnapshotMetadata[] snapshots = getRequestBuilder(target) + .get(VersionedFlowSnapshotMetadata[].class); + + return snapshots == null ? Collections.emptyList() : Arrays.asList(snapshots); + }); + } + + @Override + @SuppressWarnings("unchecked") + public List getSnapshotMetadata(final String flowId) + throws NiFiRegistryException, IOException { + + if (StringUtils.isBlank(flowId)) { + throw new IllegalArgumentException("Flow Identifier cannot be blank"); + } + + return executeAction("Error retrieving snapshot metadata", () -> { + final WebTarget target = flowsFlowSnapshotTarget + .resolveTemplate("flowId", flowId); + + final VersionedFlowSnapshotMetadata[] snapshots = getRequestBuilder(target) + .get(VersionedFlowSnapshotMetadata[].class); + + return snapshots == null ? Collections.emptyList() : Arrays.asList(snapshots); + }); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyItemsClient.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyItemsClient.java new file mode 100644 index 0000000000..85c965fce2 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyItemsClient.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client.impl; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.bucket.BucketItem; +import org.apache.nifi.registry.client.ItemsClient; +import org.apache.nifi.registry.client.NiFiRegistryException; +import org.apache.nifi.registry.client.RequestConfig; +import org.apache.nifi.registry.field.Fields; + +import javax.ws.rs.client.WebTarget; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Jersey implementation of ItemsClient. + */ +public class JerseyItemsClient extends AbstractJerseyClient implements ItemsClient { + + private final WebTarget itemsTarget; + + public JerseyItemsClient(final WebTarget baseTarget) { + this(baseTarget, null); + } + + public JerseyItemsClient(final WebTarget baseTarget, final RequestConfig requestConfig) { + super(requestConfig); + this.itemsTarget = baseTarget.path("/items"); + } + + + + @Override + public List getAll() throws NiFiRegistryException, IOException { + return executeAction("", () -> { + WebTarget target = itemsTarget; + final BucketItem[] bucketItems = getRequestBuilder(target).get(BucketItem[].class); + return bucketItems == null ? Collections.emptyList() : Arrays.asList(bucketItems); + }); + } + + @Override + public List getByBucket(final String bucketId) + throws NiFiRegistryException, IOException { + if (StringUtils.isBlank(bucketId)) { + throw new IllegalArgumentException("Bucket Identifier cannot be blank"); + } + + return executeAction("", () -> { + WebTarget target = itemsTarget + .path("/{bucketId}") + .resolveTemplate("bucketId", bucketId); + + final BucketItem[] bucketItems = getRequestBuilder(target).get(BucketItem[].class); + return bucketItems == null ? Collections.emptyList() : Arrays.asList(bucketItems); + }); + } + + @Override + public Fields getFields() throws NiFiRegistryException, IOException { + return executeAction("", () -> { + final WebTarget target = itemsTarget.path("/fields"); + return getRequestBuilder(target).get(Fields.class); + + }); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyNiFiRegistryClient.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyNiFiRegistryClient.java new file mode 100644 index 0000000000..3e64badfb3 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyNiFiRegistryClient.java @@ -0,0 +1,363 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client.impl; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector; +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.bucket.BucketItem; +import org.apache.nifi.registry.client.AccessClient; +import org.apache.nifi.registry.client.BucketClient; +import org.apache.nifi.registry.client.BundleClient; +import org.apache.nifi.registry.client.BundleVersionClient; +import org.apache.nifi.registry.client.ExtensionClient; +import org.apache.nifi.registry.client.ExtensionRepoClient; +import org.apache.nifi.registry.client.FlowClient; +import org.apache.nifi.registry.client.FlowSnapshotClient; +import org.apache.nifi.registry.client.ItemsClient; +import org.apache.nifi.registry.client.NiFiRegistryClient; +import org.apache.nifi.registry.client.NiFiRegistryClientConfig; +import org.apache.nifi.registry.client.PoliciesClient; +import org.apache.nifi.registry.client.RequestConfig; +import org.apache.nifi.registry.client.TenantsClient; +import org.apache.nifi.registry.client.UserClient; +import org.apache.nifi.registry.client.impl.request.ProxiedEntityRequestConfig; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.RequestEntityProcessing; +import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJaxbJsonProvider; +import org.glassfish.jersey.media.multipart.MultiPartFeature; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import java.io.IOException; +import java.net.URI; + +/** + * A NiFiRegistryClient that uses Jersey Client. + */ +public class JerseyNiFiRegistryClient implements NiFiRegistryClient { + + static final String NIFI_REGISTRY_CONTEXT = "nifi-registry-api"; + static final int DEFAULT_CONNECT_TIMEOUT = 10000; + static final int DEFAULT_READ_TIMEOUT = 10000; + + private final Client client; + private final WebTarget baseTarget; + + private final BucketClient bucketClient; + private final FlowClient flowClient; + private final FlowSnapshotClient flowSnapshotClient; + private final ItemsClient itemsClient; + + private JerseyNiFiRegistryClient(final NiFiRegistryClient.Builder builder) { + final NiFiRegistryClientConfig registryClientConfig = builder.getConfig(); + if (registryClientConfig == null) { + throw new IllegalArgumentException("NiFiRegistryClientConfig cannot be null"); + } + + String baseUrl = registryClientConfig.getBaseUrl(); + if (StringUtils.isBlank(baseUrl)) { + throw new IllegalArgumentException("Base URL cannot be blank"); + } + + if (baseUrl.endsWith("/")) { + baseUrl = baseUrl.substring(0, baseUrl.length() - 1); + } + + if (!baseUrl.endsWith(NIFI_REGISTRY_CONTEXT)) { + baseUrl = baseUrl + "/" + NIFI_REGISTRY_CONTEXT; + } + + try { + new URI(baseUrl); + } catch (final Exception e) { + throw new IllegalArgumentException("Invalid base URL: " + e.getMessage(), e); + } + + final SSLContext sslContext = registryClientConfig.getSslContext(); + final HostnameVerifier hostnameVerifier = registryClientConfig.getHostnameVerifier(); + + final ClientBuilder clientBuilder = ClientBuilder.newBuilder(); + if (sslContext != null) { + clientBuilder.sslContext(sslContext); + } + if (hostnameVerifier != null) { + clientBuilder.hostnameVerifier(hostnameVerifier); + } + + final int connectTimeout = registryClientConfig.getConnectTimeout() == null ? DEFAULT_CONNECT_TIMEOUT : registryClientConfig.getConnectTimeout(); + final int readTimeout = registryClientConfig.getReadTimeout() == null ? DEFAULT_READ_TIMEOUT : registryClientConfig.getReadTimeout(); + + final ClientConfig clientConfig = new ClientConfig(); + clientConfig.property(ClientProperties.CONNECT_TIMEOUT, connectTimeout); + clientConfig.property(ClientProperties.READ_TIMEOUT, readTimeout); + clientConfig.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.CHUNKED); + clientConfig.register(jacksonJaxbJsonProvider()); + clientBuilder.withConfig(clientConfig); + + this.client = clientBuilder + .register(MultiPartFeature.class) + .build(); + + this.baseTarget = client.target(baseUrl); + this.bucketClient = new JerseyBucketClient(baseTarget); + this.flowClient = new JerseyFlowClient(baseTarget); + this.flowSnapshotClient = new JerseyFlowSnapshotClient(baseTarget); + this.itemsClient = new JerseyItemsClient(baseTarget); + } + + @Override + public BucketClient getBucketClient() { + return this.bucketClient; + } + + @Override + public BucketClient getBucketClient(String... proxiedEntity) { + final RequestConfig requestConfig = new ProxiedEntityRequestConfig(proxiedEntity); + return new JerseyBucketClient(baseTarget, requestConfig); + } + + @Override + public BucketClient getBucketClient(RequestConfig requestConfig) { + return new JerseyBucketClient(baseTarget, requestConfig); + } + + @Override + public FlowClient getFlowClient() { + return this.flowClient; + } + + @Override + public FlowClient getFlowClient(String... proxiedEntity) { + final RequestConfig requestConfig = new ProxiedEntityRequestConfig(proxiedEntity); + return new JerseyFlowClient(baseTarget, requestConfig); + } + + @Override + public FlowClient getFlowClient(RequestConfig requestConfig) { + return new JerseyFlowClient(baseTarget, requestConfig); + } + + @Override + public FlowSnapshotClient getFlowSnapshotClient() { + return this.flowSnapshotClient; + } + + @Override + public FlowSnapshotClient getFlowSnapshotClient(String... proxiedEntity) { + final RequestConfig requestConfig = new ProxiedEntityRequestConfig(proxiedEntity); + return new JerseyFlowSnapshotClient(baseTarget, requestConfig); + } + + @Override + public FlowSnapshotClient getFlowSnapshotClient(RequestConfig requestConfig) { + return new JerseyFlowSnapshotClient(baseTarget, requestConfig); + } + + @Override + public ItemsClient getItemsClient() { + return this.itemsClient; + } + + @Override + public ItemsClient getItemsClient(String... proxiedEntity) { + final RequestConfig requestConfig = new ProxiedEntityRequestConfig(proxiedEntity); + return new JerseyItemsClient(baseTarget, requestConfig); + } + + @Override + public ItemsClient getItemsClient(RequestConfig requestConfig) { + return new JerseyItemsClient(baseTarget, requestConfig); + } + + @Override + public UserClient getUserClient() { + return new JerseyUserClient(baseTarget); + } + + @Override + public UserClient getUserClient(String... proxiedEntity) { + final RequestConfig requestConfig = new ProxiedEntityRequestConfig(proxiedEntity); + return new JerseyUserClient(baseTarget, requestConfig); + } + + @Override + public UserClient getUserClient(RequestConfig requestConfig) { + return new JerseyUserClient(baseTarget, requestConfig); + } + + @Override + public BundleClient getBundleClient() { + return new JerseyBundleClient(baseTarget); + } + + @Override + public BundleClient getBundleClient(String... proxiedEntity) { + final RequestConfig requestConfig = new ProxiedEntityRequestConfig(proxiedEntity); + return new JerseyBundleClient(baseTarget, requestConfig); + } + + @Override + public BundleClient getBundleClient(RequestConfig requestConfig) { + return new JerseyBundleClient(baseTarget, requestConfig); + } + + @Override + public BundleVersionClient getBundleVersionClient() { + return new JerseyBundleVersionClient(baseTarget); + } + + @Override + public BundleVersionClient getBundleVersionClient(String... proxiedEntity) { + final RequestConfig requestConfig = new ProxiedEntityRequestConfig(proxiedEntity); + return new JerseyBundleVersionClient(baseTarget, requestConfig); + } + + @Override + public BundleVersionClient getBundleVersionClient(RequestConfig requestConfig) { + return new JerseyBundleVersionClient(baseTarget, requestConfig); + } + + @Override + public ExtensionRepoClient getExtensionRepoClient() { + return new JerseyExtensionRepoClient(baseTarget); + } + + @Override + public ExtensionRepoClient getExtensionRepoClient(String... proxiedEntity) { + final RequestConfig requestConfig = new ProxiedEntityRequestConfig(proxiedEntity); + return new JerseyExtensionRepoClient(baseTarget, requestConfig); + } + + @Override + public ExtensionRepoClient getExtensionRepoClient(RequestConfig requestConfig) { + return new JerseyExtensionRepoClient(baseTarget, requestConfig); + } + + @Override + public ExtensionClient getExtensionClient() { + return new JerseyExtensionClient(baseTarget); + } + + @Override + public ExtensionClient getExtensionClient(String... proxiedEntity) { + final RequestConfig requestConfig = new ProxiedEntityRequestConfig(proxiedEntity); + return new JerseyExtensionClient(baseTarget, requestConfig); + } + + @Override + public ExtensionClient getExtensionClient(RequestConfig requestConfig) { + return new JerseyExtensionClient(baseTarget, requestConfig); + } + + @Override + public TenantsClient getTenantsClient() { + return new JerseyTenantsClient(baseTarget); + } + + @Override + public TenantsClient getTenantsClient(String... proxiedEntity) { + final RequestConfig requestConfig = new ProxiedEntityRequestConfig(proxiedEntity); + return new JerseyTenantsClient(baseTarget, requestConfig); + } + + @Override + public TenantsClient getTenantsClient(RequestConfig requestConfig) { + return new JerseyTenantsClient(baseTarget, requestConfig); + } + + @Override + public PoliciesClient getPoliciesClient() { + return new JerseyPoliciesClient(baseTarget); + } + + @Override + public PoliciesClient getPoliciesClient(String... proxiedEntity) { + final RequestConfig requestConfig = new ProxiedEntityRequestConfig(proxiedEntity); + return new JerseyPoliciesClient(baseTarget, requestConfig); + } + + @Override + public PoliciesClient getPoliciesClient(RequestConfig requestConfig) { + return new JerseyPoliciesClient(baseTarget, requestConfig); + } + + @Override + public AccessClient getAccessClient() { + return new JerseyAccessClient(baseTarget); + } + + @Override + public void close() throws IOException { + if (this.client != null) { + try { + this.client.close(); + } catch (Exception e) { + + } + } + } + + /** + * Builder for creating a JerseyNiFiRegistryClient. + */ + public static class Builder implements NiFiRegistryClient.Builder { + + private NiFiRegistryClientConfig clientConfig; + + @Override + public Builder config(final NiFiRegistryClientConfig clientConfig) { + this.clientConfig = clientConfig; + return this; + } + + @Override + public NiFiRegistryClientConfig getConfig() { + return clientConfig; + } + + @Override + public NiFiRegistryClient build() { + return new JerseyNiFiRegistryClient(this); + } + + } + + private static JacksonJaxbJsonProvider jacksonJaxbJsonProvider() { + JacksonJaxbJsonProvider jacksonJaxbJsonProvider = new JacksonJaxbJsonProvider(); + + ObjectMapper mapper = new ObjectMapper(); + mapper.setPropertyInclusion(JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, JsonInclude.Include.NON_NULL)); + mapper.setAnnotationIntrospector(new JaxbAnnotationIntrospector(mapper.getTypeFactory())); + // Ignore unknown properties so that deployed client remain compatible with future versions of NiFi Registry that add new fields + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + SimpleModule module = new SimpleModule(); + module.addDeserializer(BucketItem[].class, new BucketItemDeserializer()); + mapper.registerModule(module); + + jacksonJaxbJsonProvider.setMapper(mapper); + return jacksonJaxbJsonProvider; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyPoliciesClient.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyPoliciesClient.java new file mode 100644 index 0000000000..ae9e8dddba --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyPoliciesClient.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client.impl; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.authorization.AccessPolicy; +import org.apache.nifi.registry.client.NiFiRegistryException; +import org.apache.nifi.registry.client.PoliciesClient; +import org.apache.nifi.registry.client.RequestConfig; + +import javax.ws.rs.client.WebTarget; +import java.io.IOException; + +public class JerseyPoliciesClient extends AbstractCRUDJerseyClient implements PoliciesClient { + + public static final String ACCESS_POLICY = "Access policy"; + public static final String POLICIES_PATH = "policies"; + + public JerseyPoliciesClient(final WebTarget baseTarget) { + this(baseTarget, null); + } + + public JerseyPoliciesClient(final WebTarget baseTarget, final RequestConfig requestConfig) { + super(baseTarget, requestConfig); + } + + @Override + public AccessPolicy getAccessPolicy(String action, String resource) throws NiFiRegistryException, IOException { + if (StringUtils.isBlank(resource) || StringUtils.isBlank(action)) { + throw new IllegalArgumentException("Resource and action cannot be null"); + } + + return executeAction("Error retrieving access policy", () -> { + final WebTarget target = baseTarget.path(POLICIES_PATH).path(action).path(resource); + return getRequestBuilder(target).get(AccessPolicy.class); + }); + } + + @Override + public AccessPolicy createAccessPolicy(final AccessPolicy policy) throws NiFiRegistryException, IOException { + return create(policy, AccessPolicy.class, ACCESS_POLICY, POLICIES_PATH); + } + + @Override + public AccessPolicy updateAccessPolicy(final AccessPolicy policy) throws NiFiRegistryException, IOException { + return update(policy, policy.getIdentifier(), AccessPolicy.class, ACCESS_POLICY, POLICIES_PATH); + } + +} + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyTenantsClient.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyTenantsClient.java new file mode 100644 index 0000000000..3f95357e13 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyTenantsClient.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client.impl; + +import org.apache.nifi.registry.authorization.User; +import org.apache.nifi.registry.authorization.UserGroup; +import org.apache.nifi.registry.client.NiFiRegistryException; +import org.apache.nifi.registry.client.RequestConfig; +import org.apache.nifi.registry.client.TenantsClient; +import org.apache.nifi.registry.revision.entity.RevisionInfo; + +import javax.ws.rs.client.WebTarget; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +public class JerseyTenantsClient extends AbstractCRUDJerseyClient implements TenantsClient { + public static final String USER = "User"; + public static final String USERS_PATH = "users"; + + public static final String USER_GROUP = "User group"; + public static final String USER_GROUPS_PATH = "user-groups"; + + public JerseyTenantsClient(final WebTarget baseTarget) { + this(baseTarget, null); + } + + public JerseyTenantsClient(final WebTarget baseTarget, final RequestConfig requestConfig) { + super(baseTarget.path("/tenants"), requestConfig); + } + + @Override + public List getUsers() throws NiFiRegistryException, IOException { + return executeAction("Error retrieving users", () -> { + final WebTarget target = baseTarget.path(USERS_PATH); + return Arrays.asList(getRequestBuilder(target).get(User[].class)); + }); + } + + @Override + public User getUser(final String id) throws NiFiRegistryException, IOException { + return get(id, User.class, USER, USERS_PATH); + } + + @Override + public User createUser(final User user) throws NiFiRegistryException, IOException { + return create(user, User.class, USER, USERS_PATH); + } + + @Override + public User updateUser(final User user) throws NiFiRegistryException, IOException { + return update(user, user.getIdentifier(), User.class, USER, USERS_PATH); + } + + @Override + public User deleteUser(final String id) throws NiFiRegistryException, IOException { + return delete(id, null, User.class, USER, USERS_PATH); + } + + @Override + public User deleteUser(final String id, final RevisionInfo revisionInfo) throws NiFiRegistryException, IOException { + return delete(id, revisionInfo, User.class, USER, USERS_PATH); + } + + @Override + public List getUserGroups() throws NiFiRegistryException, IOException { + return executeAction("Error retrieving users", () -> { + final WebTarget target = baseTarget.path(USER_GROUPS_PATH); + return Arrays.asList(getRequestBuilder(target).get(UserGroup[].class)); + }); + } + + @Override + public UserGroup getUserGroup(final String id) throws NiFiRegistryException, IOException { + return get(id, UserGroup.class, USER_GROUP, USER_GROUPS_PATH); + } + + @Override + public UserGroup createUserGroup(final UserGroup group) throws NiFiRegistryException, IOException { + return create(group, UserGroup.class, USER_GROUP, USER_GROUPS_PATH); + } + + @Override + public UserGroup updateUserGroup(final UserGroup group) throws NiFiRegistryException, IOException { + return update(group, group.getIdentifier(), UserGroup.class, USER_GROUP, USER_GROUPS_PATH); + } + + @Override + public UserGroup deleteUserGroup(final String id) throws NiFiRegistryException, IOException { + return delete(id, null, UserGroup.class, USER_GROUP, USER_GROUPS_PATH); + } + + @Override + public UserGroup deleteUserGroup(final String id, final RevisionInfo revisionInfo) throws NiFiRegistryException, IOException { + return delete(id, revisionInfo, UserGroup.class, USER_GROUP, USER_GROUPS_PATH); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyUserClient.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyUserClient.java new file mode 100644 index 0000000000..484c5cbc9c --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyUserClient.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client.impl; + +import org.apache.nifi.registry.authorization.CurrentUser; +import org.apache.nifi.registry.client.NiFiRegistryException; +import org.apache.nifi.registry.client.RequestConfig; +import org.apache.nifi.registry.client.UserClient; + +import javax.ws.rs.client.WebTarget; +import java.io.IOException; + +public class JerseyUserClient extends AbstractJerseyClient implements UserClient { + + private final WebTarget accessTarget; + + public JerseyUserClient(final WebTarget baseTarget) { + this(baseTarget, null); + } + + public JerseyUserClient(final WebTarget baseTarget, final RequestConfig requestConfig) { + super(requestConfig); + this.accessTarget = baseTarget.path("/access"); + } + + @Override + public CurrentUser getAccessStatus() throws NiFiRegistryException, IOException { + return executeAction("Error retrieving access status for the current user", () -> { + return getRequestBuilder(accessTarget).get(CurrentUser.class); + }); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/request/BasicAuthRequestConfig.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/request/BasicAuthRequestConfig.java new file mode 100644 index 0000000000..4105b4f44b --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/request/BasicAuthRequestConfig.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client.impl.request; + +import org.apache.commons.lang3.Validate; +import org.apache.nifi.registry.client.RequestConfig; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +/** + * Implementation of RequestConfig for a request with basic auth. + */ +public class BasicAuthRequestConfig implements RequestConfig { + + public static final String AUTHORIZATION_HEADER = "Authorization"; + public static final String BASIC = "Basic"; + + private final String username; + private final String password; + + public BasicAuthRequestConfig(final String username, final String password) { + this.username = Validate.notBlank(username); + this.password = Validate.notBlank(password); + } + + @Override + public Map getHeaders() { + final String basicCreds = username + ":" + password; + final byte[] basicCredsBytes = basicCreds.getBytes(StandardCharsets.UTF_8); + + final Base64.Encoder encoder = Base64.getEncoder(); + final String encodedBasicCreds = encoder.encodeToString(basicCredsBytes); + + final Map headers = new HashMap<>(); + headers.put(AUTHORIZATION_HEADER, BASIC + " " + encodedBasicCreds); + return headers; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/request/BearerTokenRequestConfig.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/request/BearerTokenRequestConfig.java new file mode 100644 index 0000000000..9daf77b102 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/request/BearerTokenRequestConfig.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client.impl.request; + +import org.apache.commons.lang3.Validate; +import org.apache.nifi.registry.client.RequestConfig; + +import java.util.HashMap; +import java.util.Map; + +/** + * Implementation of RequestConfig for a request with a bearer token. + */ +public class BearerTokenRequestConfig implements RequestConfig { + + public static final String AUTHORIZATION_HEADER = "Authorization"; + public static final String BEARER = "Bearer"; + + private final String token; + + public BearerTokenRequestConfig(final String token) { + this.token = Validate.notBlank(token); + } + + @Override + public Map getHeaders() { + final Map headers = new HashMap<>(); + headers.put(AUTHORIZATION_HEADER, BEARER + " " + token); + return headers; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/request/ProxiedEntityRequestConfig.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/request/ProxiedEntityRequestConfig.java new file mode 100644 index 0000000000..08da6b4acc --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/request/ProxiedEntityRequestConfig.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client.impl.request; + +import org.apache.commons.lang3.Validate; +import org.apache.nifi.registry.client.RequestConfig; +import org.apache.nifi.registry.security.util.ProxiedEntitiesUtils; + +import java.util.HashMap; +import java.util.Map; + +/** + * Implementation of RequestConfig that produces headers for a request with proxied-entities. + */ +public class ProxiedEntityRequestConfig implements RequestConfig { + + private final String proxiedEntitiesChain; + + public ProxiedEntityRequestConfig(final String... proxiedEntities) { + Validate.notNull(proxiedEntities); + this.proxiedEntitiesChain = ProxiedEntitiesUtils.getProxiedEntitiesChain(proxiedEntities); + } + + @Override + public Map getHeaders() { + final Map headers = new HashMap<>(); + if (proxiedEntitiesChain != null) { + headers.put(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxiedEntitiesChain); + } + return headers; + } + +} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/test/java/org/apache/nifi/registry/client/impl/request/TestBasicAuthRequestConfig.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/test/java/org/apache/nifi/registry/client/impl/request/TestBasicAuthRequestConfig.java new file mode 100644 index 0000000000..91c22cf4bb --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/test/java/org/apache/nifi/registry/client/impl/request/TestBasicAuthRequestConfig.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client.impl.request; + +import org.apache.nifi.registry.client.RequestConfig; +import org.junit.Test; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Map; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertEquals; + +public class TestBasicAuthRequestConfig { + + @Test + public void testBasicAuthRequestConfig() { + final String username = "user1"; + final String password = "password"; + final String basicCreds = username + ":" + password; + + final String expectedHeaderValue = "Basic " + Base64.getEncoder().encodeToString(basicCreds.getBytes(StandardCharsets.UTF_8)); + + final RequestConfig requestConfig = new BasicAuthRequestConfig(username, password); + + final Map headers = requestConfig.getHeaders(); + assertNotNull(headers); + assertEquals(1, headers.size()); + + final String authorizationHeaderValue = headers.get("Authorization"); + assertEquals(expectedHeaderValue, authorizationHeaderValue); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/test/java/org/apache/nifi/registry/client/impl/request/TestBearerTokenRequestConfig.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/test/java/org/apache/nifi/registry/client/impl/request/TestBearerTokenRequestConfig.java new file mode 100644 index 0000000000..eeaacd3299 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/test/java/org/apache/nifi/registry/client/impl/request/TestBearerTokenRequestConfig.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client.impl.request; + +import org.apache.nifi.registry.client.RequestConfig; +import org.junit.Test; + +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class TestBearerTokenRequestConfig { + + @Test + public void testBearerTokenRequestConfig() { + final String token = "some-token"; + final String expectedHeaderValue = "Bearer " + token; + + final RequestConfig requestConfig = new BearerTokenRequestConfig(token); + + final Map headers = requestConfig.getHeaders(); + assertNotNull(headers); + assertEquals(1, headers.size()); + + final String authorizationHeaderValue = headers.get("Authorization"); + assertEquals(expectedHeaderValue, authorizationHeaderValue); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-client/src/test/java/org/apache/nifi/registry/client/impl/request/TestProxiedEntityRequestConfig.java b/nifi-registry/nifi-registry-core/nifi-registry-client/src/test/java/org/apache/nifi/registry/client/impl/request/TestProxiedEntityRequestConfig.java new file mode 100644 index 0000000000..e5427ac764 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-client/src/test/java/org/apache/nifi/registry/client/impl/request/TestProxiedEntityRequestConfig.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.client.impl.request; + +import org.apache.nifi.registry.client.RequestConfig; +import org.apache.nifi.registry.security.util.ProxiedEntitiesUtils; +import org.junit.Test; + +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class TestProxiedEntityRequestConfig { + + @Test + public void testSingleProxiedEntity() { + final String proxiedEntity = "user1"; + final String expectedProxiedEntitiesChain = ""; + + final RequestConfig requestConfig = new ProxiedEntityRequestConfig(proxiedEntity); + + final Map headers = requestConfig.getHeaders(); + assertNotNull(headers); + assertEquals(1, headers.size()); + + final String proxiedEntitiesChainHeaderValue = headers.get(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN); + assertEquals(expectedProxiedEntitiesChain, proxiedEntitiesChainHeaderValue); + } + + @Test + public void testMultipleProxiedEntity() { + final String proxiedEntity1 = "user1"; + final String proxiedEntity2 = "user2"; + final String proxiedEntity3 = "user3"; + final String expectedProxiedEntitiesChain = ""; + + final RequestConfig requestConfig = new ProxiedEntityRequestConfig( + proxiedEntity1, proxiedEntity2, proxiedEntity3); + + final Map headers = requestConfig.getHeaders(); + assertNotNull(headers); + assertEquals(1, headers.size()); + + final String proxiedEntitiesChainHeaderValue = headers.get(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN); + assertEquals(expectedProxiedEntitiesChain, proxiedEntitiesChainHeaderValue); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/pom.xml b/nifi-registry/nifi-registry-core/nifi-registry-data-model/pom.xml new file mode 100644 index 0000000000..8af5d80643 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/pom.xml @@ -0,0 +1,58 @@ + + + + 4.0.0 + + org.apache.nifi.registry + nifi-registry-core + 1.14.0-SNAPSHOT + + + nifi-registry-data-model + jar + + + + io.swagger + swagger-annotations + + + javax.validation + validation-api + + + javax.ws.rs + javax.ws.rs-api + + + org.apache.nifi.registry + nifi-registry-revision-entity-model + 1.14.0-SNAPSHOT + + + + + + + jigsaw + + (1.8,) + + + + jakarta.xml.bind + jakarta.xml.bind-api + + + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/RegistryConfiguration.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/RegistryConfiguration.java new file mode 100644 index 0000000000..4c50477591 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/RegistryConfiguration.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement +@ApiModel +public class RegistryConfiguration { + + private Boolean supportsManagedAuthorizer; + private Boolean supportsConfigurableAuthorizer; + private Boolean supportsConfigurableUsersAndGroups; + + /** + * @return whether this NiFi Registry supports a managed authorizer. Managed authorizers can visualize users, groups, + * and policies in the UI. This value is read only + */ + @ApiModelProperty( + value = "Whether this NiFi Registry supports a managed authorizer. Managed authorizers can visualize users, groups, and policies in the UI.", + readOnly = true + ) + public Boolean getSupportsManagedAuthorizer() { + return supportsManagedAuthorizer; + } + + public void setSupportsManagedAuthorizer(Boolean supportsManagedAuthorizer) { + this.supportsManagedAuthorizer = supportsManagedAuthorizer; + } + + /** + * @return whether this NiFi Registry supports configurable users and groups. This value is read only + */ + @ApiModelProperty( + value = "Whether this NiFi Registry supports configurable users and groups.", + readOnly = true + ) + public Boolean getSupportsConfigurableUsersAndGroups() { + return supportsConfigurableUsersAndGroups; + } + + public void setSupportsConfigurableUsersAndGroups(Boolean supportsConfigurableUsersAndGroups) { + this.supportsConfigurableUsersAndGroups = supportsConfigurableUsersAndGroups; + } + + /** + * @return whether this NiFi Registry supports a configurable authorizer. This value is read only + */ + @ApiModelProperty( + value = "Whether this NiFi Registry supports a configurable authorizer.", + readOnly = true + ) + public Boolean getSupportsConfigurableAuthorizer() { + return supportsConfigurableAuthorizer; + } + + public void setSupportsConfigurableAuthorizer(Boolean supportsConfigurableAuthorizer) { + this.supportsConfigurableAuthorizer = supportsConfigurableAuthorizer; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/AccessPolicy.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/AccessPolicy.java new file mode 100644 index 0000000000..7b911ecde1 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/AccessPolicy.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.authorization; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +/** + * Access policy details, including the users and user groups to which the policy applies. + */ +@ApiModel +public class AccessPolicy extends AccessPolicySummary { + + private Set users; + private Set userGroups; + + @ApiModelProperty(value = "The set of user IDs associated with this access policy.") + public Set getUsers() { + return users; + } + + public void setUsers(Set users) { + this.users = users; + } + + public void addUsers(Collection users) { + if (users != null) { + if (this.users == null) { + this.users = new HashSet<>(); + } + this.users.addAll(users); + } + } + + @ApiModelProperty(value = "The set of user group IDs associated with this access policy.") + public Set getUserGroups() { + return userGroups; + } + + public void setUserGroups(Set userGroups) { + this.userGroups = userGroups; + } + + public void addUserGroups(Collection userGroups) { + if (userGroups != null) { + if (this.userGroups == null) { + this.userGroups = new HashSet<>(); + } + this.userGroups.addAll(userGroups); + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/AccessPolicySummary.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/AccessPolicySummary.java new file mode 100644 index 0000000000..e432dc495e --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/AccessPolicySummary.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.authorization; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import org.apache.nifi.registry.revision.entity.RevisableEntity; +import org.apache.nifi.registry.revision.entity.RevisionInfo; + +/** + * Access policy summary of which actions ("read', "write", "delete") are allowable for a specified web resource. + */ +@ApiModel +public class AccessPolicySummary implements RevisableEntity { + + private String identifier; + private String resource; + private String action; + private Boolean configurable; + private RevisionInfo revision; + + @ApiModelProperty(value = "The id of the policy. Set by server at creation time.", readOnly = true) + @Override + public String getIdentifier() { + return identifier; + } + + @Override + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + + @ApiModelProperty(value = "The resource for this access policy.", required = true + ) + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + @ApiModelProperty( + value = "The action associated with this access policy.", + allowableValues = "read, write, delete", + required = true + ) + public String getAction() { + return action; + } + + public void setAction(String action) { + this.action = action; + } + + @ApiModelProperty(value = "Indicates if this access policy is configurable, based on which Authorizer has been configured to manage it.", readOnly = true) + public Boolean getConfigurable() { + return configurable; + } + + public void setConfigurable(Boolean configurable) { + this.configurable = configurable; + } + + @ApiModelProperty( + value = "The revision of this entity used for optimistic-locking during updates.", + readOnly = true + ) + @Override + public RevisionInfo getRevision() { + return revision; + } + + @Override + public void setRevision(RevisionInfo revision) { + this.revision = revision; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/CurrentUser.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/CurrentUser.java new file mode 100644 index 0000000000..3bbf8e59cc --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/CurrentUser.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.authorization; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +@ApiModel +public class CurrentUser { + + private String identity; + private boolean anonymous; + private boolean loginSupported; + private boolean oidcLoginSupported; + private ResourcePermissions resourcePermissions; + + @ApiModelProperty(value = "The identity of the current user", readOnly = true) + public String getIdentity() { + return identity; + } + + public void setIdentity(String identity) { + this.identity = identity; + } + + @ApiModelProperty(value = "Indicates if the current user is anonymous", readOnly = true) + public boolean isAnonymous() { + return anonymous; + } + + public void setAnonymous(boolean anonymous) { + this.anonymous = anonymous; + } + + @ApiModelProperty(value = "Indicates if the NiFi Registry instance supports logging in") + public boolean isLoginSupported() { + return loginSupported; + } + + @ApiModelProperty(value = "Indicates if the NiFi Registry instance supports logging in with an OIDC provider") + public boolean isOIDCLoginSupported() { + return oidcLoginSupported; + } + + public void setLoginSupported(boolean loginSupported) { + this.loginSupported = loginSupported; + } + + public void setOIDCLoginSupported(boolean oidcLoginSupported) { + this.oidcLoginSupported = oidcLoginSupported; + } + + @ApiModelProperty(value = "The access that the current user has to top level resources", readOnly = true) + public ResourcePermissions getResourcePermissions() { + return resourcePermissions; + } + + public void setResourcePermissions(ResourcePermissions resourcePermissions) { + this.resourcePermissions = resourcePermissions; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/Permissions.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/Permissions.java new file mode 100644 index 0000000000..ca856b9332 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/Permissions.java @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.authorization; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +@ApiModel +public class Permissions { + + private boolean canRead = false; + private boolean canWrite = false; + private boolean canDelete = false; + + public Permissions() { + } + + public Permissions(Permissions permissions) { + if (permissions == null) { + throw new IllegalArgumentException("Cannot call copy constructor with null argument"); + } + + this.canRead = permissions.getCanRead(); + this.canWrite = permissions.getCanWrite(); + this.canDelete = permissions.getCanDelete(); + } + + /** + * @return Indicates whether the user can read a given resource. + */ + @ApiModelProperty( + value = "Indicates whether the user can read a given resource.", + readOnly = true + ) + public boolean getCanRead() { + return canRead; + } + + public void setCanRead(boolean canRead) { + this.canRead = canRead; + } + + public Permissions withCanRead(boolean canRead) { + setCanRead(canRead); + return this; + } + + /** + * @return Indicates whether the user can write a given resource. + */ + @ApiModelProperty( + value = "Indicates whether the user can write a given resource.", + readOnly = true + ) + public boolean getCanWrite() { + return canWrite; + } + + public void setCanWrite(boolean canWrite) { + this.canWrite = canWrite; + } + + public Permissions withCanWrite(boolean canWrite) { + setCanWrite(canWrite); + return this; + } + + /** + * @return Indicates whether the user can delete a given resource. + */ + @ApiModelProperty( + value = "Indicates whether the user can delete a given resource.", + readOnly = true + ) + public boolean getCanDelete() { + return canDelete; + } + + public void setCanDelete(boolean canDelete) { + this.canDelete = canDelete; + } + + public Permissions withCanDelete(boolean canDelete) { + setCanDelete(canDelete); + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Permissions that = (Permissions) o; + + if (canRead != that.canRead) return false; + if (canWrite != that.canWrite) return false; + return canDelete == that.canDelete; + } + + @Override + public int hashCode() { + int result = (canRead ? 1 : 0); + result = 31 * result + (canWrite ? 1 : 0); + result = 31 * result + (canDelete ? 1 : 0); + return result; + } + + @Override + public String toString() { + return "Permissions{" + + "canRead=" + canRead + + ", canWrite=" + canWrite + + ", canDelete=" + canDelete + + '}'; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/Resource.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/Resource.java new file mode 100644 index 0000000000..d409bf9913 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/Resource.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.authorization; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +@ApiModel +public class Resource { + + private String identifier; + private String name; + + /** + * The name of the resource. + * + * @return The name of the resource + */ + @ApiModelProperty(value = "The name of the resource.", readOnly = true) + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + /** + * The identifier of the resource. + * + * @return The identifier of the resource + */ + @ApiModelProperty(value = "The identifier of the resource.", readOnly = true) + public String getIdentifier() { + return identifier; + } + + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/ResourcePermissions.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/ResourcePermissions.java new file mode 100644 index 0000000000..de55e26112 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/ResourcePermissions.java @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.authorization; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +@ApiModel +public class ResourcePermissions { + + private Permissions buckets = new Permissions(); + private Permissions tenants = new Permissions(); + private Permissions policies = new Permissions(); + private Permissions proxy = new Permissions(); + + @ApiModelProperty( + value = "The access that the current user has to any top level resources (a logical 'OR' of all other values)", + readOnly = true) + public Permissions getAnyTopLevelResource() { + return new Permissions() + .withCanRead(buckets.getCanRead() + || tenants.getCanRead() + || policies.getCanRead() + || proxy.getCanRead()) + .withCanWrite(buckets.getCanWrite() + || tenants.getCanWrite() + || policies.getCanWrite() + || proxy.getCanWrite()) + .withCanDelete(buckets.getCanDelete() + || tenants.getCanDelete() + || policies.getCanDelete() + || proxy.getCanDelete()); + } + + @ApiModelProperty( + value = "The access that the current user has to the top level /buckets resource of this NiFi Registry (i.e., access to all buckets)", + readOnly = true) + public Permissions getBuckets() { + return buckets; + } + + public void setBuckets(Permissions buckets) { + this.buckets = buckets; + } + + @ApiModelProperty( + value = "The access that the current user has to the top level /tenants resource of this NiFi Registry", + readOnly = true) + public Permissions getTenants() { + return tenants; + } + + public void setTenants(Permissions tenants) { + this.tenants = tenants; + } + + @ApiModelProperty( + value = "The access that the current user has to the top level /policies resource of this NiFi Registry", + readOnly = true) + public Permissions getPolicies() { + return policies; + } + + public void setPolicies(Permissions policies) { + this.policies = policies; + } + + @ApiModelProperty( + value = "The access that the current user has to the top level /proxy resource of this NiFi Registry", + readOnly = true) + public Permissions getProxy() { + return proxy; + } + + public void setProxy(Permissions proxy) { + this.proxy = proxy; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ResourcePermissions that = (ResourcePermissions) o; + + if (buckets != null ? !buckets.equals(that.buckets) : that.buckets != null) + return false; + if (tenants != null ? !tenants.equals(that.tenants) : that.tenants != null) + return false; + if (policies != null ? !policies.equals(that.policies) : that.policies != null) + return false; + return proxy != null ? proxy.equals(that.proxy) : that.proxy == null; + } + + @Override + public int hashCode() { + int result = buckets != null ? buckets.hashCode() : 0; + result = 31 * result + (tenants != null ? tenants.hashCode() : 0); + result = 31 * result + (policies != null ? policies.hashCode() : 0); + result = 31 * result + (proxy != null ? proxy.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "ResourcePermissions{" + + "buckets=" + buckets + + ", tenants=" + tenants + + ", policies=" + policies + + ", proxy=" + proxy + + '}'; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/Tenant.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/Tenant.java new file mode 100644 index 0000000000..7d416ae49d --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/Tenant.java @@ -0,0 +1,135 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.authorization; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import org.apache.nifi.registry.revision.entity.RevisableEntity; +import org.apache.nifi.registry.revision.entity.RevisionInfo; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +/** + * A tenant of this NiFi Registry + */ +@ApiModel +public class Tenant implements RevisableEntity { + + private String identifier; + private String identity; + private Boolean configurable; + private ResourcePermissions resourcePermissions; + private Set accessPolicies; + private RevisionInfo revision; + + public Tenant() {} + + public Tenant(String identifier, String identity) { + this.identifier = identifier; + this.identity = identity; + } + + /** + * @return tenant's unique identifier + */ + @ApiModelProperty( + value = "The computer-generated identifier of the tenant.", + readOnly = true) + @Override + public String getIdentifier() { + return identifier; + } + + @Override + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + + /** + * @return tenant's identity + */ + @ApiModelProperty( + value = "The human-facing identity of the tenant. This can only be changed if the tenant is configurable.", + required = true) + public String getIdentity() { + return identity; + } + + public void setIdentity(String identity) { + this.identity = identity; + } + + @ApiModelProperty( + value = "Indicates if this tenant is configurable, based on which UserGroupProvider has been configured to manage it.", + readOnly = true) + public Boolean getConfigurable() { + return configurable; + } + + public void setConfigurable(Boolean configurable) { + this.configurable = configurable; + } + + @ApiModelProperty( + value = "A summary top-level resource access policies granted to this tenant.", + readOnly = true + ) + public ResourcePermissions getResourcePermissions() { + return resourcePermissions; + } + + public void setResourcePermissions(ResourcePermissions resourcePermissions) { + this.resourcePermissions = resourcePermissions; + } + + @ApiModelProperty( + value = "The access policies granted to this tenant.", + readOnly = true + ) + public Set getAccessPolicies() { + return accessPolicies; + } + + public void setAccessPolicies(Set accessPolicies) { + this.accessPolicies = accessPolicies; + } + + public void addAccessPolicies(Collection accessPolicies) { + if (accessPolicies != null) { + if (this.accessPolicies == null) { + this.accessPolicies = new HashSet<>(); + } + this.accessPolicies.addAll(accessPolicies); + } + } + + @ApiModelProperty( + value = "The revision of this entity used for optimistic-locking during updates.", + readOnly = true + ) + @Override + public RevisionInfo getRevision() { + return revision; + } + + @Override + public void setRevision(RevisionInfo revision) { + this.revision = revision; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/User.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/User.java new file mode 100644 index 0000000000..0a91f71e1e --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/User.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.authorization; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +@ApiModel +public class User extends Tenant { + + private Set userGroups; + + public User() {} + + public User(String identifier, String identity) { + super(identifier, identity); + } + + @ApiModelProperty( + value = "The groups to which the user belongs.", + readOnly = true + ) + public Set getUserGroups() { + return userGroups; + } + + public void setUserGroups(Set userGroups) { + this.userGroups = userGroups; + } + + public void addUserGroups(Collection userGroups) { + if (userGroups != null) { + if (this.userGroups == null) { + this.userGroups = new HashSet<>(); + } + this.userGroups.addAll(userGroups); + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/UserGroup.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/UserGroup.java new file mode 100644 index 0000000000..39fca3610e --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/UserGroup.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.authorization; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +/** + * A user group, used to apply a single set of authorization policies to a group of users. + */ +@ApiModel +public class UserGroup extends Tenant { + + private Set users; + + public UserGroup() {} + + public UserGroup(String identifier, String identity) { + super(identifier, identity); + } + + /** + * @return The users that belong to this user group. + */ + @ApiModelProperty(value = "The users that belong to this user group. This can only be changed if this group is configurable.") + public Set getUsers() { + return users; + } + + public void setUsers(Set users) { + this.users = users; + } + + public void addUsers(Collection users) { + if (users != null) { + if (this.users == null) { + this.users = new HashSet<>(); + } + this.users.addAll(users); + } + } + + public void addUser(Tenant user) { + if (user != null) { + if (this.users == null) { + this.users = new HashSet<>(); + } + this.users.add(user); + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/Bucket.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/Bucket.java new file mode 100644 index 0000000000..e94c38e2a5 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/Bucket.java @@ -0,0 +1,149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.bucket; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import org.apache.nifi.registry.authorization.Permissions; +import org.apache.nifi.registry.link.LinkableEntity; +import org.apache.nifi.registry.revision.entity.RevisableEntity; +import org.apache.nifi.registry.revision.entity.RevisionInfo; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Objects; + +@XmlRootElement +@ApiModel +public class Bucket extends LinkableEntity implements RevisableEntity { + + @NotBlank + private String identifier; + + @NotBlank + private String name; + + @Min(1) + private long createdTimestamp; + + private String description; + + private Boolean allowBundleRedeploy; + + private Boolean allowPublicRead; + + private Permissions permissions; + + private RevisionInfo revision; + + @ApiModelProperty(value = "An ID to uniquely identify this object.", readOnly = true) + public String getIdentifier() { + return identifier; + } + + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + + @ApiModelProperty(value = "The name of the bucket.", required = true) + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @ApiModelProperty(value = "The timestamp of when the bucket was first created. This is set by the server at creation time.", readOnly = true) + public long getCreatedTimestamp() { + return createdTimestamp; + } + + public void setCreatedTimestamp(long createdTimestamp) { + this.createdTimestamp = createdTimestamp; + } + + @ApiModelProperty("A description of the bucket.") + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @ApiModelProperty("Indicates if this bucket allows the same version of an extension bundle to be redeployed and thus overwrite the existing artifact. By default this is false.") + public Boolean isAllowBundleRedeploy() { + return allowBundleRedeploy; + } + + public void setAllowBundleRedeploy(final Boolean allowBundleRedeploy) { + this.allowBundleRedeploy = allowBundleRedeploy; + } + + @ApiModelProperty("Indicates if this bucket allows read access to unauthenticated anonymous users") + public Boolean isAllowPublicRead() { + return allowPublicRead; + } + + public void setAllowPublicRead(final Boolean allowPublicRead) { + this.allowPublicRead = allowPublicRead; + } + + @ApiModelProperty(value = "The access that the current user has to this bucket.", readOnly = true) + public Permissions getPermissions() { + return permissions; + } + + public void setPermissions(Permissions permissions) { + this.permissions = permissions; + } + + @ApiModelProperty( + value = "The revision of this entity used for optimistic-locking during updates.", + readOnly = true + ) + @Override + public RevisionInfo getRevision() { + return revision; + } + + @Override + public void setRevision(RevisionInfo revision) { + this.revision = revision; + } + + @Override + public int hashCode() { + return Objects.hashCode(this.getIdentifier()); + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + + final Bucket other = (Bucket) obj; + return Objects.equals(this.getIdentifier(), other.getIdentifier()); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/BucketItem.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/BucketItem.java new file mode 100644 index 0000000000..09fe35b248 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/BucketItem.java @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.bucket; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import org.apache.nifi.registry.authorization.Permissions; +import org.apache.nifi.registry.link.LinkableEntity; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.util.Objects; + +@ApiModel +public abstract class BucketItem extends LinkableEntity { + + @NotBlank + private String identifier; + + @NotBlank + private String name; + + private String description; + + @NotBlank + private String bucketIdentifier; + + // read-only + private String bucketName; + + @Min(1) + private long createdTimestamp; + + @Min(1) + private long modifiedTimestamp; + + @NotNull + private final BucketItemType type; + + private Permissions permissions; + + + public BucketItem(final BucketItemType type) { + this.type = type; + } + + @ApiModelProperty(value = "An ID to uniquely identify this object.", readOnly = true) + public String getIdentifier() { + return identifier; + } + + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + + @ApiModelProperty(value = "The name of the item.", required = true) + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @ApiModelProperty("A description of the item.") + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @ApiModelProperty(value = "The identifier of the bucket this items belongs to. This cannot be changed after the item is created.", required = true) + public String getBucketIdentifier() { + return bucketIdentifier; + } + + public void setBucketIdentifier(String bucketIdentifier) { + this.bucketIdentifier = bucketIdentifier; + } + + @ApiModelProperty(value = "The name of the bucket this items belongs to.", readOnly = true) + public String getBucketName() { + return bucketName; + } + + public void setBucketName(String bucketName) { + this.bucketName = bucketName; + } + + @ApiModelProperty(value = "The timestamp of when the item was created, as milliseconds since epoch.", readOnly = true) + public long getCreatedTimestamp() { + return createdTimestamp; + } + + public void setCreatedTimestamp(long createdTimestamp) { + this.createdTimestamp = createdTimestamp; + } + + @ApiModelProperty(value = "The timestamp of when the item was last modified, as milliseconds since epoch.", readOnly = true) + public long getModifiedTimestamp() { + return modifiedTimestamp; + } + + public void setModifiedTimestamp(long modifiedTimestamp) { + this.modifiedTimestamp = modifiedTimestamp; + } + + @ApiModelProperty(value = "The type of item.", required = true) + public BucketItemType getType() { + return type; + } + + @ApiModelProperty(value = "The access that the current user has to the bucket containing this item.", readOnly = true) + public Permissions getPermissions() { + return permissions; + } + + public void setPermissions(Permissions permissions) { + this.permissions = permissions; + } + + @Override + public int hashCode() { + return Objects.hashCode(this.getIdentifier()); + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + + final BucketItem other = (BucketItem) obj; + return Objects.equals(this.getIdentifier(), other.getIdentifier()); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/BucketItemType.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/BucketItemType.java new file mode 100644 index 0000000000..4b1ed69325 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/BucketItemType.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.bucket; + +/** + * Type of item in a bucket. + */ +public enum BucketItemType { + + // The case of these enum names matches what we want to return in + // the BucketItem.type field when serialized in an API response. + + Flow, + + Bundle; +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/ComponentDifference.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/ComponentDifference.java new file mode 100644 index 0000000000..d4fb5d01f1 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/ComponentDifference.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.diff; + +import io.swagger.annotations.ApiModelProperty; + +/** + * Represents a specific, individual difference that has changed between 2 versions. + * The change data and textual descriptions of the change are included for client consumption. + */ +public class ComponentDifference { + private String valueA; + private String valueB; + private String changeDescription; + private String differenceType; + private String differenceTypeDescription; + + @ApiModelProperty("The earlier value from the difference.") + public String getValueA() { + return valueA; + } + + public void setValueA(String valueA) { + this.valueA = valueA; + } + + @ApiModelProperty("The newer value from the difference.") + public String getValueB() { + return valueB; + } + + public void setValueB(String valueB) { + this.valueB = valueB; + } + + @ApiModelProperty("The description of the change.") + public String getChangeDescription() { + return changeDescription; + } + + public void setChangeDescription(String changeDescription) { + this.changeDescription = changeDescription; + } + + @ApiModelProperty("The key to the difference.") + public String getDifferenceType() { + return differenceType; + } + + public void setDifferenceType(String differenceType) { + this.differenceType = differenceType; + } + + @ApiModelProperty("The description of the change type.") + public String getDifferenceTypeDescription() { + return differenceTypeDescription; + } + + public void setDifferenceTypeDescription(String differenceTypeDescription) { + this.differenceTypeDescription = differenceTypeDescription; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/ComponentDifferenceGroup.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/ComponentDifferenceGroup.java new file mode 100644 index 0000000000..a7b1d5822a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/ComponentDifferenceGroup.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.diff; + +import io.swagger.annotations.ApiModelProperty; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** + * Represents a group of differences related to a specific component in a flow. + */ +public class ComponentDifferenceGroup { + private String componentId; + private String componentName; + private String componentType; + private String processGroupId; + private Set differences = new HashSet<>(); + + @ApiModelProperty("The id of the component whose changes are grouped together.") + public String getComponentId() { + return componentId; + } + + public void setComponentId(String componentId) { + this.componentId = componentId; + } + + @ApiModelProperty("The name of the component whose changes are grouped together.") + public String getComponentName() { + return componentName; + } + + public void setComponentName(String componentName) { + this.componentName = componentName; + } + + @ApiModelProperty("The type of component these changes relate to.") + public String getComponentType() { + return componentType; + } + + public void setComponentType(String componentType) { + this.componentType = componentType; + } + + @ApiModelProperty("The process group id for this component.") + public String getProcessGroupId() { + return processGroupId; + } + + public void setProcessGroupId(String processGroupId) { + this.processGroupId = processGroupId; + } + + @ApiModelProperty("The list of changes related to this component between the 2 versions.") + public Set getDifferences() { + return differences; + } + + public void setDifferences(Set differences) { + this.differences = differences; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ComponentDifferenceGroup that = (ComponentDifferenceGroup) o; + return Objects.equals(componentId, that.componentId) + && Objects.equals(componentName, that.componentName) + && Objects.equals(componentType, that.componentType) + && Objects.equals(processGroupId, that.processGroupId); + } + + @Override + public int hashCode() { + return Objects.hash(componentId, componentName, componentType, processGroupId); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/VersionedFlowDifference.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/VersionedFlowDifference.java new file mode 100644 index 0000000000..ccc6a0519a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/VersionedFlowDifference.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.diff; + +import io.swagger.annotations.ApiModelProperty; + +import java.util.Set; + +/** + * Represents the result of a diff between 2 versions of the same flow. + * A subset of the model classes in registry.flow.diff for exposing on the API + * The differences are grouped by component + */ +public class VersionedFlowDifference { + private String bucketId; + private String flowId; + private int versionA; + private int versionB; + private Set componentDifferenceGroups; + + public Set getComponentDifferenceGroups() { + return componentDifferenceGroups; + } + + public void setComponentDifferenceGroups(Set componentDifferenceGroups) { + this.componentDifferenceGroups = componentDifferenceGroups; + } + + @ApiModelProperty("The id of the bucket that the flow is stored in.") + public String getBucketId() { + return bucketId; + } + + public void setBucketId(String bucketId) { + this.bucketId = bucketId; + } + + @ApiModelProperty("The id of the flow that is being examined.") + public String getFlowId() { + return flowId; + } + + public void setFlowId(String flowId) { + this.flowId = flowId; + } + + @ApiModelProperty("The earlier version from the diff operation.") + public int getVersionA() { + return versionA; + } + + public void setVersionA(int versionA) { + this.versionA = versionA; + } + + @ApiModelProperty("The latter version from the diff operation.") + public int getVersionB() { + return versionB; + } + + public void setVersionB(int versionB) { + this.versionB = versionB; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/BuildInfo.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/BuildInfo.java new file mode 100644 index 0000000000..deb5944861 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/BuildInfo.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.bundle; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +@ApiModel +public class BuildInfo { + + private String buildTool; + + private String buildFlags; + + private String buildBranch; + + private String buildTag; + + private String buildRevision; + + private long built; + + private String builtBy; + + @ApiModelProperty(value = "The tool used to build the version of the bundle") + public String getBuildTool() { + return buildTool; + } + + public void setBuildTool(String buildTool) { + this.buildTool = buildTool; + } + + @ApiModelProperty(value = "The flags used to build the version of the bundle") + public String getBuildFlags() { + return buildFlags; + } + + public void setBuildFlags(String buildFlags) { + this.buildFlags = buildFlags; + } + + @ApiModelProperty(value = "The branch used to build the version of the bundle") + public String getBuildBranch() { + return buildBranch; + } + + public void setBuildBranch(String buildBranch) { + this.buildBranch = buildBranch; + } + + @ApiModelProperty(value = "The tag used to build the version of the bundle") + public String getBuildTag() { + return buildTag; + } + + public void setBuildTag(String buildTag) { + this.buildTag = buildTag; + } + + @ApiModelProperty(value = "The revision used to build the version of the bundle") + public String getBuildRevision() { + return buildRevision; + } + + public void setBuildRevision(String buildRevision) { + this.buildRevision = buildRevision; + } + + @ApiModelProperty(value = "The timestamp the version of the bundle was built") + public long getBuilt() { + return built; + } + + public void setBuilt(long built) { + this.built = built; + } + + @ApiModelProperty(value = "The identity of the user that performed the build") + public String getBuiltBy() { + return builtBy; + } + + public void setBuiltBy(String builtBy) { + this.builtBy = builtBy; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/Bundle.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/Bundle.java new file mode 100644 index 0000000000..b9cf3eb018 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/Bundle.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.bundle; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import org.apache.nifi.registry.bucket.BucketItem; +import org.apache.nifi.registry.bucket.BucketItemType; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Represents an extension bundle identified by a group and artifact id with in a bucket. + * + * Each bundle may then have one or more versions associated with it by creating an {@link BundleVersion}. + * + * The {@link BundleVersion} represents the actually binary bundle which may contain one or more extensions. + * + * Note: The @ApiModel annotation needs a value specified because there is another class called Bundle in a different + * package for flows, and the model names must be unique since they won't carry the Java package structure forward. + */ +@ApiModel("ExtensionBundle") +@XmlRootElement +public class Bundle extends BucketItem { + + @NotNull + private BundleType bundleType; + + @NotBlank + private String groupId; + + @NotBlank + private String artifactId; + + @Min(0) + private long versionCount; + + public Bundle() { + super(BucketItemType.Bundle); + } + + @ApiModelProperty(value = "The type of the extension bundle") + public BundleType getBundleType() { + return bundleType; + } + + public void setBundleType(BundleType bundleType) { + this.bundleType = bundleType; + } + + @ApiModelProperty(value = "The group id of the extension bundle") + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + @ApiModelProperty(value = "The artifact id of the extension bundle") + public String getArtifactId() { + return artifactId; + } + + public void setArtifactId(String artifactId) { + this.artifactId = artifactId; + } + + @ApiModelProperty(value = "The number of versions of this extension bundle.", readOnly = true) + public long getVersionCount() { + return versionCount; + } + + public void setVersionCount(long versionCount) { + this.versionCount = versionCount; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/BundleFilterParams.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/BundleFilterParams.java new file mode 100644 index 0000000000..68c60018f6 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/BundleFilterParams.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.bundle; + +/** + * Filter parameters for retrieving extension bundles. + * + * Any combination of fields may be provided to filter on the provided values. + * + * Note: This class is currently not part of the REST API so it doesn't not have the Swagger annotations, but it is used + * in the service layer and client to pass around params. + * + */ +public class BundleFilterParams { + + private static final BundleFilterParams EMPTY_PARAMS = new Builder().build(); + + private final String bucketName; + private final String groupId; + private final String artifactId; + + private BundleFilterParams(final Builder builder) { + this.bucketName = builder.bucketName; + this.groupId = builder.groupId; + this.artifactId = builder.artifactId; + } + + public String getBucketName() { + return bucketName; + } + + public String getGroupId() { + return groupId; + } + + public String getArtifactId() { + return artifactId; + } + + public static BundleFilterParams of(final String bucketName, final String groupId, final String artifactId) { + return new Builder().bucket(bucketName).group(groupId).artifact(artifactId).build(); + } + + public static BundleFilterParams of(final String groupId, final String artifactId) { + return new Builder().group(groupId).artifact(artifactId).build(); + } + + public static BundleFilterParams empty() { + return EMPTY_PARAMS; + } + + public static class Builder { + + private String bucketName; + private String groupId; + private String artifactId; + + public Builder bucket(final String bucketName) { + this.bucketName = bucketName; + return this; + } + + public Builder group(final String groupId) { + this.groupId = groupId; + return this; + } + + public Builder artifact(final String artifactId) { + this.artifactId = artifactId; + return this; + } + + public BundleFilterParams build() { + return new BundleFilterParams(this); + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/BundleInfo.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/BundleInfo.java new file mode 100644 index 0000000000..417796829f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/BundleInfo.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.bundle; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +@ApiModel +public class BundleInfo { + + private String bucketId; + private String bucketName; + + private String bundleId; + private BundleType bundleType; + + private String groupId; + private String artifactId; + private String version; + + private String systemApiVersion; + + @ApiModelProperty(value = "The id of the bucket where the bundle is located") + public String getBucketId() { + return bucketId; + } + + public void setBucketId(String bucketId) { + this.bucketId = bucketId; + } + + @ApiModelProperty(value = "The name of the bucket where the bundle is located") + public String getBucketName() { + return bucketName; + } + + public void setBucketName(String bucketName) { + this.bucketName = bucketName; + } + + @ApiModelProperty(value = "The id of the bundle") + public String getBundleId() { + return bundleId; + } + + public void setBundleId(String bundleId) { + this.bundleId = bundleId; + } + + @ApiModelProperty("The type of bundle (i.e. a NiFi NAR vs MiNiFi CPP)") + public BundleType getBundleType() { + return bundleType; + } + + public void setBundleType(BundleType bundleType) { + this.bundleType = bundleType; + } + + @ApiModelProperty(value = "The group id of the bundle") + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + @ApiModelProperty(value = "The artifact id of the bundle") + public String getArtifactId() { + return artifactId; + } + + public void setArtifactId(String artifactId) { + this.artifactId = artifactId; + } + + @ApiModelProperty(value = "The version of the bundle") + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + @ApiModelProperty(value = "The version of the system API the bundle was built against") + public String getSystemApiVersion() { + return systemApiVersion; + } + + public void setSystemApiVersion(String systemApiVersion) { + this.systemApiVersion = systemApiVersion; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/BundleType.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/BundleType.java new file mode 100644 index 0000000000..92c54a64ed --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/BundleType.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.bundle; + +import io.swagger.annotations.ApiModel; + +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; + +/** + * The possible types of extension bundles. + */ +@ApiModel +@XmlJavaTypeAdapter(BundleTypeAdapter.class) +public enum BundleType { + + NIFI_NAR(BundleTypeValues.NIFI_NAR_VALUE), + + MINIFI_CPP(BundleTypeValues.MINIFI_CPP_VALUE); + + private final String displayName; + + BundleType(String displayName) { + this.displayName = displayName; + } + + // Note: This method must be name fromString for JAX-RS/Jersey to use it on query and path params + public static BundleType fromString(String value) { + if (value == null) { + throw new IllegalArgumentException("Value cannot be null"); + } + + for (final BundleType type : values()) { + if (type.toString().equals(value)) { + return type; + } + } + + throw new IllegalArgumentException("Unknown BundleType: " + value); + } + + + @Override + public String toString() { + return displayName; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/BundleTypeAdapter.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/BundleTypeAdapter.java new file mode 100644 index 0000000000..4a54caa26a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/BundleTypeAdapter.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.bundle; + +import javax.xml.bind.annotation.adapters.XmlAdapter; + +public class BundleTypeAdapter extends XmlAdapter { + + @Override + public BundleType unmarshal(String v) throws Exception { + if (v == null) { + return null; + } + + return BundleType.fromString(v); + } + + @Override + public String marshal(final BundleType v) throws Exception { + if (v == null) { + return null; + } + + return v.toString(); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/BundleTypeValues.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/BundleTypeValues.java new file mode 100644 index 0000000000..6933882697 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/BundleTypeValues.java @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.bundle; + +public class BundleTypeValues { + + public static final String NIFI_NAR_VALUE = "nifi-nar"; + public static final String MINIFI_CPP_VALUE = "minifi-cpp"; + + public static final String ALL_VALUES = NIFI_NAR_VALUE + ", " + MINIFI_CPP_VALUE; +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/BundleVersion.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/BundleVersion.java new file mode 100644 index 0000000000..852f841082 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/BundleVersion.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.bundle; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.link.LinkableEntity; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlTransient; +import java.util.Set; + +@ApiModel +@XmlRootElement +public class BundleVersion extends LinkableEntity { + + @Valid + @NotNull + private BundleVersionMetadata versionMetadata; + + // read-only, only populated from retrieval of an individual bundle version + private Set dependencies; + + // read-only, only populated from retrieval of an individual bundle version + private Bundle bundle; + + // read-only, only populated from retrieval of an individual bundle version + private Bucket bucket; + + + @ApiModelProperty(value = "The metadata about this version of the extension bundle") + public BundleVersionMetadata getVersionMetadata() { + return versionMetadata; + } + + public void setVersionMetadata(BundleVersionMetadata versionMetadata) { + this.versionMetadata = versionMetadata; + } + + @ApiModelProperty(value = "The set of other bundle versions that this version is dependent on", readOnly = true) + public Set getDependencies() { + return dependencies; + } + + public void setDependencies(Set dependencies) { + this.dependencies = dependencies; + } + + @ApiModelProperty(value = "The bundle this version is for", readOnly = true) + public Bundle getBundle() { + return bundle; + } + + public void setBundle(Bundle bundle) { + this.bundle = bundle; + } + + @ApiModelProperty(value = "The bucket that the extension bundle belongs to") + public Bucket getBucket() { + return bucket; + } + + public void setBucket(Bucket bucket) { + this.bucket = bucket; + } + + @XmlTransient + public String getFilename() { + final String filename = bundle.getArtifactId() + "-" + versionMetadata.getVersion(); + + switch (bundle.getBundleType()) { + case NIFI_NAR: + return filename + ".nar"; + case MINIFI_CPP: + // TODO should CPP get a special extension + return filename; + default: + throw new IllegalStateException("Unknown bundle type: " + bundle.getBundleType()); + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/BundleVersionDependency.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/BundleVersionDependency.java new file mode 100644 index 0000000000..2eb792ba58 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/BundleVersionDependency.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.bundle; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import javax.validation.constraints.NotBlank; +import java.util.Objects; + +@ApiModel +public class BundleVersionDependency { + + @NotBlank + private String groupId; + + @NotBlank + private String artifactId; + + @NotBlank + private String version; + + @ApiModelProperty(value = "The group id of the bundle dependency") + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + @ApiModelProperty(value = "The artifact id of the bundle dependency") + public String getArtifactId() { + return artifactId; + } + + public void setArtifactId(String artifactId) { + this.artifactId = artifactId; + } + + @ApiModelProperty(value = "The version of the bundle dependency") + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + @Override + public int hashCode() { + return Objects.hash(groupId, artifactId, version); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + + final BundleVersionDependency other = (BundleVersionDependency) obj; + + return Objects.equals(groupId, other.groupId) + && Objects.equals(artifactId, other.artifactId) + && Objects.equals(version, other.version); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/BundleVersionFilterParams.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/BundleVersionFilterParams.java new file mode 100644 index 0000000000..2192b9ad9f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/BundleVersionFilterParams.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.bundle; + +/** + * Filter parameters for extension bundle versions. + * + * Any combination of fields may be populated to filter on the provided values. + * + * Note: This class is currently not part of the REST API so it doesn't not have the Swagger annotations, but it is used + * in the service layer and client to pass around params. + */ +public class BundleVersionFilterParams { + + private static final BundleVersionFilterParams EMPTY_PARAMS = new Builder().build(); + + private final String groupId; + private final String artifactId; + private final String version; + + private BundleVersionFilterParams(final Builder builder) { + this.groupId = builder.groupId; + this.artifactId = builder.artifactId; + this.version = builder.version; + } + + public String getGroupId() { + return groupId; + } + + public String getArtifactId() { + return artifactId; + } + + public String getVersion() { + return version; + } + + public static BundleVersionFilterParams of(final String groupId, final String artifactId, final String version) { + return new Builder().group(groupId).artifact(artifactId).version(version).build(); + } + + public static BundleVersionFilterParams empty() { + return EMPTY_PARAMS; + } + + public static class Builder { + + private String groupId; + private String artifactId; + private String version; + + public Builder group(final String groupId) { + this.groupId = groupId; + return this; + } + + public Builder artifact(final String artifactId) { + this.artifactId = artifactId; + return this; + } + + public Builder version(final String version) { + this.version = version; + return this; + } + + public BundleVersionFilterParams build() { + return new BundleVersionFilterParams(this); + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/BundleVersionMetadata.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/BundleVersionMetadata.java new file mode 100644 index 0000000000..474d1e8980 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/bundle/BundleVersionMetadata.java @@ -0,0 +1,224 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.bundle; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import org.apache.nifi.registry.link.LinkableEntity; + +import javax.validation.Valid; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Objects; + +@ApiModel +@XmlRootElement +public class BundleVersionMetadata extends LinkableEntity implements Comparable { + + @NotBlank + private String id; + + @NotBlank + private String bundleId; + + @NotBlank + private String bucketId; + + // read-only, populated on response + private String groupId; + + // read-only, populated on response + private String artifactId; + + @NotBlank + private String version; + + @Min(1) + private long timestamp; + + @NotBlank + private String author; + + private String description; + + @NotBlank + private String sha256; + + @NotNull + private Boolean sha256Supplied; + + @NotNull + @Min(0) + private long contentSize; + + @NotBlank + private String systemApiVersion; + + @Valid + @NotNull + private BuildInfo buildInfo; + + + @ApiModelProperty(value = "The id of this version of the extension bundle") + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @ApiModelProperty(value = "The id of the extension bundle this version is for") + public String getBundleId() { + return bundleId; + } + + public void setBundleId(String bundleId) { + this.bundleId = bundleId; + } + + @ApiModelProperty(value = "The id of the bucket the extension bundle belongs to", required = true) + public String getBucketId() { + return bucketId; + } + + public void setBucketId(String bucketId) { + this.bucketId = bucketId; + } + + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + public String getArtifactId() { + return artifactId; + } + + public void setArtifactId(String artifactId) { + this.artifactId = artifactId; + } + + @ApiModelProperty(value = "The version of the extension bundle") + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + @ApiModelProperty(value = "The timestamp of the create date of this version") + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + @ApiModelProperty(value = "The identity that created this version") + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + @ApiModelProperty(value = "The description for this version") + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @ApiModelProperty(value = "The hex representation of the SHA-256 digest of the binary content for this version") + public String getSha256() { + return sha256; + } + + public void setSha256(String sha256) { + this.sha256 = sha256; + } + + @ApiModelProperty(value = "Whether or not the client supplied a SHA-256 when uploading the bundle") + public Boolean getSha256Supplied() { + return sha256Supplied; + } + + public void setSha256Supplied(Boolean sha256Supplied) { + this.sha256Supplied = sha256Supplied; + } + + @ApiModelProperty(value = "The size of the binary content for this version in bytes") + public long getContentSize() { + return contentSize; + } + + public void setContentSize(long contentSize) { + this.contentSize = contentSize; + } + + @ApiModelProperty(value = "The version of the system API that this bundle version was built against") + public String getSystemApiVersion() { + return systemApiVersion; + } + + public void setSystemApiVersion(String systemApiVersion) { + this.systemApiVersion = systemApiVersion; + } + + @ApiModelProperty(value = "The build information about this version") + public BuildInfo getBuildInfo() { + return buildInfo; + } + + public void setBuildInfo(BuildInfo buildInfo) { + this.buildInfo = buildInfo; + } + + @Override + public int compareTo(final BundleVersionMetadata o) { + return o == null ? -1 : version.compareTo(o.version); + } + + @Override + public int hashCode() { + return Objects.hashCode(this.id); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + + final BundleVersionMetadata other = (BundleVersionMetadata) obj; + return Objects.equals(this.id, other.id); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/ExtensionFilterParams.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/ExtensionFilterParams.java new file mode 100644 index 0000000000..963b3b718c --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/ExtensionFilterParams.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.component; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import org.apache.nifi.registry.extension.bundle.BundleType; +import org.apache.nifi.registry.extension.component.manifest.ExtensionType; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Parameters for filtering on extensions. All parameters will be AND'd together, but tags will be OR'd. + */ +@ApiModel +public class ExtensionFilterParams { + + private final BundleType bundleType; + private final ExtensionType extensionType; + private final Set tags; + + // Used by Jackson + private ExtensionFilterParams() { + bundleType = null; + extensionType = null; + tags = null; + } + + private ExtensionFilterParams(final Builder builder) { + this.bundleType = builder.bundleType; + this.extensionType = builder.extensionType; + this.tags = Collections.unmodifiableSet(new HashSet<>(builder.tags)); + } + + @ApiModelProperty("The type of bundle") + public BundleType getBundleType() { + return bundleType; + } + + @ApiModelProperty("The type of extension") + public ExtensionType getExtensionType() { + return extensionType; + } + + @ApiModelProperty("The tags") + public Set getTags() { + return tags; + } + + public static class Builder { + + private BundleType bundleType; + private ExtensionType extensionType; + private Set tags = new HashSet<>(); + + public Builder bundleType(final BundleType bundleType) { + this.bundleType = bundleType; + return this; + } + + public Builder extensionType(final ExtensionType extensionType) { + this.extensionType = extensionType; + return this; + } + + public Builder tag(final String tag) { + if (tag != null) { + tags.add(tag); + } + return this; + } + + public Builder addTags(final Collection tags) { + if (tags != null) { + this.tags.addAll(tags); + } + return this; + } + + public ExtensionFilterParams build() { + return new ExtensionFilterParams(this); + } + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/ExtensionMetadata.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/ExtensionMetadata.java new file mode 100644 index 0000000000..a41198b50e --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/ExtensionMetadata.java @@ -0,0 +1,174 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.component; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import org.apache.nifi.registry.extension.bundle.BundleInfo; +import org.apache.nifi.registry.extension.component.manifest.DeprecationNotice; +import org.apache.nifi.registry.extension.component.manifest.ExtensionType; +import org.apache.nifi.registry.extension.component.manifest.ProvidedServiceAPI; +import org.apache.nifi.registry.extension.component.manifest.Restricted; +import org.apache.nifi.registry.link.LinkAdapter; +import org.apache.nifi.registry.link.LinkableDocs; +import org.apache.nifi.registry.link.LinkableEntity; + +import javax.ws.rs.core.Link; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +@ApiModel +public class ExtensionMetadata extends LinkableEntity implements LinkableDocs, Comparable { + + private String name; + private String displayName; + private ExtensionType type; + private String description; + private DeprecationNotice deprecationNotice; + private List tags; + private Restricted restricted; + private List providedServiceAPIs; + private BundleInfo bundleInfo; + private boolean hasAdditionalDetails; + private Link linkDocs; + + @ApiModelProperty(value = "The name of the extension") + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @ApiModelProperty(value = "The display name of the extension") + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + @ApiModelProperty(value = "The type of the extension") + public ExtensionType getType() { + return type; + } + + public void setType(ExtensionType type) { + this.type = type; + } + + @ApiModelProperty(value = "The description of the extension") + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @ApiModelProperty(value = "The deprecation notice of the extension") + public DeprecationNotice getDeprecationNotice() { + return deprecationNotice; + } + + public void setDeprecationNotice(DeprecationNotice deprecationNotice) { + this.deprecationNotice = deprecationNotice; + } + + @ApiModelProperty(value = "The tags of the extension") + public List getTags() { + return tags; + } + + public void setTags(List tags) { + this.tags = tags; + } + + @ApiModelProperty(value = "The restrictions of the extension") + public Restricted getRestricted() { + return restricted; + } + + public void setRestricted(Restricted restricted) { + this.restricted = restricted; + } + + @ApiModelProperty(value = "The service APIs provided by the extension") + public List getProvidedServiceAPIs() { + return providedServiceAPIs; + } + + public void setProvidedServiceAPIs(List providedServiceAPIs) { + this.providedServiceAPIs = providedServiceAPIs; + } + + @ApiModelProperty(value = "The information for the bundle where this extension is located") + public BundleInfo getBundleInfo() { + return bundleInfo; + } + + public void setBundleInfo(BundleInfo bundleInfo) { + this.bundleInfo = bundleInfo; + } + + @ApiModelProperty(value = "Whether or not the extension has additional detail documentation") + public boolean getHasAdditionalDetails() { + return hasAdditionalDetails; + } + + public void setHasAdditionalDetails(boolean hasAdditionalDetails) { + this.hasAdditionalDetails = hasAdditionalDetails; + } + + @Override + @XmlElement + @XmlJavaTypeAdapter(LinkAdapter.class) + @ApiModelProperty(value = "A WebLink to the documentation for this extension.", + dataType = "org.apache.nifi.registry.link.JaxbLink", readOnly = true) + public Link getLinkDocs() { + return linkDocs; + } + + @Override + public void setLinkDocs(Link link) { + this.linkDocs = link; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ExtensionMetadata extension = (ExtensionMetadata) o; + return Objects.equals(name, extension.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + @Override + public int compareTo(final ExtensionMetadata o) { + return Comparator.comparing(ExtensionMetadata::getDisplayName).compare(this, o); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/ExtensionMetadataContainer.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/ExtensionMetadataContainer.java new file mode 100644 index 0000000000..867f8b0653 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/ExtensionMetadataContainer.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.component; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import java.util.SortedSet; + +@ApiModel +public class ExtensionMetadataContainer { + + private int numResults; + private ExtensionFilterParams filterParams; + private SortedSet extensions; + + @ApiModelProperty("The number of extensions in the response") + public int getNumResults() { + return numResults; + } + + public void setNumResults(int numResults) { + this.numResults = numResults; + } + + @ApiModelProperty("The filter parameters submitted for the request") + public ExtensionFilterParams getFilterParams() { + return filterParams; + } + + public void setFilterParams(ExtensionFilterParams filterParams) { + this.filterParams = filterParams; + } + + @ApiModelProperty("The metadata for the extensions") + public SortedSet getExtensions() { + return extensions; + } + + public void setExtensions(SortedSet extensions) { + this.extensions = extensions; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/TagCount.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/TagCount.java new file mode 100644 index 0000000000..d040fce0e3 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/TagCount.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.component; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import java.util.Comparator; +import java.util.Objects; + +@ApiModel +public class TagCount implements Comparable { + + private String tag; + private int count; + + @ApiModelProperty("The tag label") + public String getTag() { + return tag; + } + + public void setTag(String tag) { + this.tag = tag; + } + + @ApiModelProperty("The number of occurrences of the given tag") + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } + + @Override + public int compareTo(TagCount o) { + return Comparator.comparing(TagCount::getTag).compare(this, o); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TagCount tagCount = (TagCount) o; + return count == tagCount.count && Objects.equals(tag, tagCount.tag); + } + + @Override + public int hashCode() { + return Objects.hash(tag, count); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/AllowableValue.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/AllowableValue.java new file mode 100644 index 0000000000..debcc8f471 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/AllowableValue.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.component.manifest; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +@ApiModel +@XmlAccessorType(XmlAccessType.FIELD) +public class AllowableValue { + + private String value; + private String displayName; + private String description; + + @ApiModelProperty(value = "The value of the allowable value") + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @ApiModelProperty(value = "The display name of the allowable value") + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + @ApiModelProperty(value = "The description of the allowable value") + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/Attribute.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/Attribute.java new file mode 100644 index 0000000000..acfcd66d85 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/Attribute.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.component.manifest; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +@ApiModel +@XmlAccessorType(XmlAccessType.FIELD) +public class Attribute { + + private String name; + private String description; + + @ApiModelProperty(value = "The name of the attribute") + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @ApiModelProperty(value = "The description of the attribute") + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/ControllerServiceDefinition.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/ControllerServiceDefinition.java new file mode 100644 index 0000000000..a303bc0813 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/ControllerServiceDefinition.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.component.manifest; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import java.util.Objects; + +@ApiModel +@XmlAccessorType(XmlAccessType.FIELD) +public class ControllerServiceDefinition { + + private String className; + private String groupId; + private String artifactId; + private String version; + + @ApiModelProperty(value = "The class name of the service API") + public String getClassName() { + return className; + } + + public void setClassName(String className) { + this.className = className; + } + + @ApiModelProperty(value = "The group id of the service API") + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + @ApiModelProperty(value = "The artifact id of the service API") + public String getArtifactId() { + return artifactId; + } + + public void setArtifactId(String artifactId) { + this.artifactId = artifactId; + } + + @ApiModelProperty(value = "The version of the service API") + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final ControllerServiceDefinition that = (ControllerServiceDefinition) o; + return Objects.equals(className, that.className) + && Objects.equals(groupId, that.groupId) + && Objects.equals(artifactId, that.artifactId) + && Objects.equals(version, that.version); + } + + @Override + public int hashCode() { + return Objects.hash(className, groupId, artifactId, version); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/DeprecationNotice.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/DeprecationNotice.java new file mode 100644 index 0000000000..32fe548fe9 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/DeprecationNotice.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.component.manifest; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import java.util.List; + +@ApiModel +@XmlAccessorType(XmlAccessType.FIELD) +public class DeprecationNotice { + + private String reason; + + @XmlElementWrapper + @XmlElement(name = "alternative") + private List alternatives; + + @ApiModelProperty(value = "The reason for the deprecation") + public String getReason() { + return reason; + } + + public void setReason(String reason) { + this.reason = reason; + } + + @ApiModelProperty(value = "The alternatives to use") + public List getAlternatives() { + return alternatives; + } + + public void setAlternatives(List alternatives) { + this.alternatives = alternatives; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/DynamicProperty.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/DynamicProperty.java new file mode 100644 index 0000000000..cc1be0d2f4 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/DynamicProperty.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.component.manifest; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +@ApiModel +@XmlAccessorType(XmlAccessType.FIELD) +public class DynamicProperty { + + private String name; + private String value; + private String description; + private ExpressionLanguageScope expressionLanguageScope; + private boolean expressionLanguageSupported; + + @ApiModelProperty(value = "The description of the dynamic property name") + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @ApiModelProperty(value = "The description of the dynamic property value") + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @ApiModelProperty(value = "The description of the dynamic property") + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @ApiModelProperty(value = "Whether or not expression language is supported") + public boolean isExpressionLanguageSupported() { + return expressionLanguageSupported; + } + + public void setExpressionLanguageSupported(boolean expressionLanguageSupported) { + this.expressionLanguageSupported = expressionLanguageSupported; + } + + @ApiModelProperty(value = "The scope of the expression language support") + public ExpressionLanguageScope getExpressionLanguageScope() { + return expressionLanguageScope; + } + + public void setExpressionLanguageScope(ExpressionLanguageScope expressionLanguageScope) { + this.expressionLanguageScope = expressionLanguageScope; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/DynamicRelationship.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/DynamicRelationship.java new file mode 100644 index 0000000000..e14e36380f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/DynamicRelationship.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.component.manifest; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +@ApiModel +@XmlAccessorType(XmlAccessType.FIELD) +public class DynamicRelationship { + + private String name; + private String description; + + @ApiModelProperty(value = "The description of the dynamic relationship name") + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @ApiModelProperty(value = "The description of the dynamic relationship") + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/ExpressionLanguageScope.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/ExpressionLanguageScope.java new file mode 100644 index 0000000000..c9e22e1dd5 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/ExpressionLanguageScope.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.component.manifest; + +import io.swagger.annotations.ApiModel; + +@ApiModel +public enum ExpressionLanguageScope { + + /** + * Expression language is disabled + */ + NONE, + + /** + * Expression language is evaluated against variables in registry + */ + VARIABLE_REGISTRY, + + /** + * Expression language is evaluated per flow file using attributes + */ + FLOWFILE_ATTRIBUTES; + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/Extension.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/Extension.java new file mode 100644 index 0000000000..143507f026 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/Extension.java @@ -0,0 +1,258 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.component.manifest; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import javax.validation.Valid; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import java.util.List; +import java.util.Objects; + +@ApiModel +@XmlAccessorType(XmlAccessType.FIELD) +public class Extension { + + @Valid + @XmlElement(required = true) + private String name; + + @Valid + @XmlElement(required = true) + private ExtensionType type; + + private DeprecationNotice deprecationNotice; + + private String description; + + @XmlElementWrapper + @XmlElement(name = "tag") + private List tags; + + @XmlElementWrapper + @XmlElement(name = "property") + private List properties; + + @XmlElementWrapper + @XmlElement(name = "dynamicProperty") + private List dynamicProperties; + + @XmlElementWrapper + @XmlElement(name = "relationship") + private List relationships; + + private DynamicRelationship dynamicRelationship; + + @XmlElementWrapper + @XmlElement(name = "readsAttribute") + private List readsAttributes; + + @XmlElementWrapper + @XmlElement(name = "writesAttribute") + private List writesAttributes; + + private Stateful stateful; + + @Valid + private Restricted restricted; + + private InputRequirement inputRequirement; + + @XmlElementWrapper + @XmlElement(name = "systemResourceConsideration") + private List systemResourceConsiderations; + + @XmlElementWrapper + @XmlElement(name = "see") + private List seeAlso; + + @Valid + @XmlElementWrapper + @XmlElement(name = "providedServiceAPI") + private List providedServiceAPIs; + + + @ApiModelProperty(value = "The name of the extension") + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @ApiModelProperty(value = "The type of the extension") + public ExtensionType getType() { + return type; + } + + public void setType(ExtensionType type) { + this.type = type; + } + + @ApiModelProperty(value = "The deprecation notice of the extension") + public DeprecationNotice getDeprecationNotice() { + return deprecationNotice; + } + + public void setDeprecationNotice(DeprecationNotice deprecationNotice) { + this.deprecationNotice = deprecationNotice; + } + + @ApiModelProperty(value = "The description of the extension") + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @ApiModelProperty(value = "The tags of the extension") + public List getTags() { + return tags; + } + + public void setTags(List tags) { + this.tags = tags; + } + + @ApiModelProperty(value = "The properties of the extension") + public List getProperties() { + return properties; + } + + public void setProperties(List properties) { + this.properties = properties; + } + + @ApiModelProperty(value = "The dynamic properties of the extension") + public List getDynamicProperties() { + return dynamicProperties; + } + + public void setDynamicProperties(List dynamicProperties) { + this.dynamicProperties = dynamicProperties; + } + + @ApiModelProperty(value = "The relationships of the extension") + public List getRelationships() { + return relationships; + } + + public void setRelationships(List relationships) { + this.relationships = relationships; + } + + @ApiModelProperty(value = "The dynamic relationships of the extension") + public DynamicRelationship getDynamicRelationship() { + return dynamicRelationship; + } + + public void setDynamicRelationship(DynamicRelationship dynamicRelationship) { + this.dynamicRelationship = dynamicRelationship; + } + + @ApiModelProperty(value = "The attributes read from flow files by the extension") + public List getReadsAttributes() { + return readsAttributes; + } + + public void setReadsAttributes(List readsAttributes) { + this.readsAttributes = readsAttributes; + } + + @ApiModelProperty(value = "The attributes written to flow files by the extension") + public List getWritesAttributes() { + return writesAttributes; + } + + public void setWritesAttributes(List writesAttributes) { + this.writesAttributes = writesAttributes; + } + + @ApiModelProperty(value = "The information about how the extension stores state") + public Stateful getStateful() { + return stateful; + } + + public void setStateful(Stateful stateful) { + this.stateful = stateful; + } + + @ApiModelProperty(value = "The restrictions of the extension") + public Restricted getRestricted() { + return restricted; + } + + public void setRestricted(Restricted restricted) { + this.restricted = restricted; + } + + @ApiModelProperty(value = "The input requirement of the extension") + public InputRequirement getInputRequirement() { + return inputRequirement; + } + + public void setInputRequirement(InputRequirement inputRequirement) { + this.inputRequirement = inputRequirement; + } + + @ApiModelProperty(value = "The resource considerations of the extension") + public List getSystemResourceConsiderations() { + return systemResourceConsiderations; + } + + public void setSystemResourceConsiderations(List systemResourceConsiderations) { + this.systemResourceConsiderations = systemResourceConsiderations; + } + + @ApiModelProperty(value = "The names of other extensions to see") + public List getSeeAlso() { + return seeAlso; + } + + public void setSeeAlso(List seeAlso) { + this.seeAlso = seeAlso; + } + + @ApiModelProperty(value = "The service APIs provided by this extension") + public List getProvidedServiceAPIs() { + return providedServiceAPIs; + } + + public void setProvidedServiceAPIs(List providedServiceAPIs) { + this.providedServiceAPIs = providedServiceAPIs; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Extension extension = (Extension) o; + return Objects.equals(name, extension.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/ExtensionManifest.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/ExtensionManifest.java new file mode 100644 index 0000000000..c71d771f83 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/ExtensionManifest.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.component.manifest; + +import io.swagger.annotations.ApiModel; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.List; + +@ApiModel +@XmlRootElement(name = "extensionManifest") +@XmlAccessorType(XmlAccessType.FIELD) +public class ExtensionManifest { + + @XmlElement(required = true) + private String systemApiVersion; + + @XmlElementWrapper + @XmlElement(name = "extension") + private List extensions; + + public ExtensionManifest() { + } + + public ExtensionManifest(String systemApiVersion, List extensions) { + this.systemApiVersion = systemApiVersion; + this.extensions = extensions; + } + + public String getSystemApiVersion() { + return systemApiVersion; + } + + public void setSystemApiVersion(String systemApiVersion) { + this.systemApiVersion = systemApiVersion; + } + + public List getExtensions() { + return extensions; + } + + public void setExtensions(List extensions) { + this.extensions = extensions; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/ExtensionType.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/ExtensionType.java new file mode 100644 index 0000000000..f93c75b8db --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/ExtensionType.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.component.manifest; + +/** + * Possible types of extensions. + */ +public enum ExtensionType { + + PROCESSOR, + + CONTROLLER_SERVICE, + + REPORTING_TASK; + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/InputRequirement.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/InputRequirement.java new file mode 100644 index 0000000000..d1dc9ca9ab --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/InputRequirement.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.component.manifest; + +import io.swagger.annotations.ApiModel; + +@ApiModel +public enum InputRequirement { + + /** + * This value is used to indicate that the Processor requires input from other Processors + * in order to run. As a result, the Processor will not be valid if it does not have any + * incoming connections. + */ + INPUT_REQUIRED, + + /** + * This value is used to indicate that the Processor will consume data from an incoming + * connection but does not require an incoming connection in order to perform its task. + * If the {@link InputRequirement} annotation is not present, this is the default value + * that is used. + */ + INPUT_ALLOWED, + + /** + * This value is used to indicate that the Processor is a "Source Processor" and does + * not accept incoming connections. Because the Processor does not pull FlowFiles from + * an incoming connection, it can be very confusing for users who create incoming connections + * to the Processor. As a result, this value can be used in order to clarify that incoming + * connections will not be used. This prevents the user from even creating such a connection. + */ + INPUT_FORBIDDEN; + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/Property.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/Property.java new file mode 100644 index 0000000000..f3723f3413 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/Property.java @@ -0,0 +1,159 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.component.manifest; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import java.util.List; + +@ApiModel +@XmlAccessorType(XmlAccessType.FIELD) +public class Property { + + private String name; + private String displayName; + private String description; + private String defaultValue; + private ControllerServiceDefinition controllerServiceDefinition; + + @XmlElementWrapper + @XmlElement(name = "allowableValue") + private List allowableValues; + + private boolean required; + private boolean sensitive; + + private boolean expressionLanguageSupported; + private ExpressionLanguageScope expressionLanguageScope; + + private boolean dynamicallyModifiesClasspath; + private boolean dynamic; + + + @ApiModelProperty(value = "The name of the property") + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @ApiModelProperty(value = "The display name") + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + @ApiModelProperty(value = "The description") + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @ApiModelProperty(value = "The default value") + public String getDefaultValue() { + return defaultValue; + } + + public void setDefaultValue(String defaultValue) { + this.defaultValue = defaultValue; + } + + @ApiModelProperty(value = "The controller service required by this property, or null if none is required") + public ControllerServiceDefinition getControllerServiceDefinition() { + return controllerServiceDefinition; + } + + public void setControllerServiceDefinition(ControllerServiceDefinition controllerServiceDefinition) { + this.controllerServiceDefinition = controllerServiceDefinition; + } + + @ApiModelProperty(value = "The allowable values for this property") + public List getAllowableValues() { + return allowableValues; + } + + public void setAllowableValues(List allowableValues) { + this.allowableValues = allowableValues; + } + + @ApiModelProperty(value = "Whether or not the property is required") + public boolean isRequired() { + return required; + } + + public void setRequired(boolean required) { + this.required = required; + } + + @ApiModelProperty(value = "Whether or not the property is sensitive") + public boolean isSensitive() { + return sensitive; + } + + public void setSensitive(boolean sensitive) { + this.sensitive = sensitive; + } + + @ApiModelProperty(value = "Whether or not expression language is supported") + public boolean isExpressionLanguageSupported() { + return expressionLanguageSupported; + } + + public void setExpressionLanguageSupported(boolean expressionLanguageSupported) { + this.expressionLanguageSupported = expressionLanguageSupported; + } + + @ApiModelProperty(value = "The scope of expression language support") + public ExpressionLanguageScope getExpressionLanguageScope() { + return expressionLanguageScope; + } + + public void setExpressionLanguageScope(ExpressionLanguageScope expressionLanguageScope) { + this.expressionLanguageScope = expressionLanguageScope; + } + + @ApiModelProperty(value = "Whether or not the processor dynamically modifies the classpath") + public boolean isDynamicallyModifiesClasspath() { + return dynamicallyModifiesClasspath; + } + + public void setDynamicallyModifiesClasspath(boolean dynamicallyModifiesClasspath) { + this.dynamicallyModifiesClasspath = dynamicallyModifiesClasspath; + } + + @ApiModelProperty(value = "Whether or not the processor is dynamic") + public boolean isDynamic() { + return dynamic; + } + + public void setDynamic(boolean dynamic) { + this.dynamic = dynamic; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/ProvidedServiceAPI.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/ProvidedServiceAPI.java new file mode 100644 index 0000000000..abad607ef8 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/ProvidedServiceAPI.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.component.manifest; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import javax.validation.constraints.NotBlank; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import java.util.Objects; + +@ApiModel +@XmlAccessorType(XmlAccessType.FIELD) +public class ProvidedServiceAPI { + + @NotBlank + private String className; + @NotBlank + private String groupId; + @NotBlank + private String artifactId; + @NotBlank + private String version; + + @ApiModelProperty(value = "The class name of the service API being provided") + public String getClassName() { + return className; + } + + public void setClassName(String className) { + this.className = className; + } + + @ApiModelProperty(value = "The group id of the service API being provided") + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + @ApiModelProperty(value = "The artifact id of the service API being provided") + public String getArtifactId() { + return artifactId; + } + + public void setArtifactId(String artifactId) { + this.artifactId = artifactId; + } + + @ApiModelProperty(value = "The version of the service API being provided") + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final ProvidedServiceAPI that = (ProvidedServiceAPI) o; + return Objects.equals(className, that.className) + && Objects.equals(groupId, that.groupId) + && Objects.equals(artifactId, that.artifactId) + && Objects.equals(version, that.version); + } + + @Override + public int hashCode() { + return Objects.hash(className, groupId, artifactId, version); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/Relationship.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/Relationship.java new file mode 100644 index 0000000000..db64045963 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/Relationship.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.component.manifest; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +@ApiModel +@XmlAccessorType(XmlAccessType.FIELD) +public class Relationship { + + private String name; + private String description; + private boolean autoTerminated; + + @ApiModelProperty(value = "The name of the relationship") + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @ApiModelProperty(value = "The description of the relationship") + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @ApiModelProperty(value = "Whether or not the relationship is auto-terminated by default") + public boolean isAutoTerminated() { + return autoTerminated; + } + + public void setAutoTerminated(boolean autoTerminated) { + this.autoTerminated = autoTerminated; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/Restricted.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/Restricted.java new file mode 100644 index 0000000000..82ab09de15 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/Restricted.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.component.manifest; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import javax.validation.Valid; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import java.util.List; + +@ApiModel +@XmlAccessorType(XmlAccessType.FIELD) +public class Restricted { + + private String generalRestrictionExplanation; + + @Valid + @XmlElementWrapper + @XmlElement(name = "restriction") + private List restrictions; + + @ApiModelProperty(value = "The general restriction for the extension, or null if only specific restrictions exist") + public String getGeneralRestrictionExplanation() { + return generalRestrictionExplanation; + } + + public void setGeneralRestrictionExplanation(String generalRestrictionExplanation) { + this.generalRestrictionExplanation = generalRestrictionExplanation; + } + + @ApiModelProperty(value = "The specific restrictions") + public List getRestrictions() { + return restrictions; + } + + public void setRestrictions(List restrictions) { + this.restrictions = restrictions; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/Restriction.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/Restriction.java new file mode 100644 index 0000000000..cb2501c64e --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/Restriction.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.component.manifest; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import javax.validation.constraints.NotBlank; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import java.util.Objects; + +@ApiModel +@XmlAccessorType(XmlAccessType.FIELD) +public class Restriction { + + @NotBlank + private String requiredPermission; + @NotBlank + private String explanation; + + @ApiModelProperty(value = "The permission required for this restriction") + public String getRequiredPermission() { + return requiredPermission; + } + + public void setRequiredPermission(String requiredPermission) { + this.requiredPermission = requiredPermission; + } + + @ApiModelProperty(value = "The explanation of this restriction") + public String getExplanation() { + return explanation; + } + + public void setExplanation(String explanation) { + this.explanation = explanation; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final Restriction that = (Restriction) o; + return Objects.equals(requiredPermission, that.requiredPermission) + && Objects.equals(explanation, that.explanation); + } + + @Override + public int hashCode() { + return Objects.hash(requiredPermission, explanation); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/Scope.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/Scope.java new file mode 100644 index 0000000000..e07d18368f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/Scope.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.component.manifest; + +import io.swagger.annotations.ApiModel; + +/** + * Possible scopes for storing state. + */ +@ApiModel +public enum Scope { + + CLUSTER, + + LOCAL; +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/Stateful.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/Stateful.java new file mode 100644 index 0000000000..3d200702f0 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/Stateful.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.component.manifest; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import java.util.List; + +@ApiModel +@XmlAccessorType(XmlAccessType.FIELD) +public class Stateful { + + private String description; + + @XmlElementWrapper + @XmlElement(name = "scope") + private List scopes; + + @ApiModelProperty(value = "The description for how the extension stores state") + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @ApiModelProperty(value = "The scopes used to store state") + public List getScopes() { + return scopes; + } + + public void setScopes(List scopes) { + this.scopes = scopes; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/SystemResourceConsideration.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/SystemResourceConsideration.java new file mode 100644 index 0000000000..589eed6482 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/component/manifest/SystemResourceConsideration.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.component.manifest; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +@ApiModel +@XmlAccessorType(XmlAccessType.FIELD) +public class SystemResourceConsideration { + + private String resource; + private String description; + + @ApiModelProperty(value = "The resource to consider") + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + @ApiModelProperty(value = "The description of how the resource is affected") + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoArtifact.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoArtifact.java new file mode 100644 index 0000000000..ed10643723 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoArtifact.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.repo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import org.apache.nifi.registry.link.LinkableEntity; + +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Comparator; +import java.util.Objects; + +@ApiModel +@XmlRootElement +public class ExtensionRepoArtifact extends LinkableEntity implements Comparable { + + private String bucketName; + private String groupId; + private String artifactId; + + @ApiModelProperty(value = "The bucket name") + public String getBucketName() { + return bucketName; + } + + public void setBucketName(String bucketName) { + this.bucketName = bucketName; + } + + @ApiModelProperty("The group id") + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + @ApiModelProperty("The artifact id") + public String getArtifactId() { + return artifactId; + } + + public void setArtifactId(String artifactId) { + this.artifactId = artifactId; + } + + @Override + public int compareTo(final ExtensionRepoArtifact o) { + return Comparator.comparing(ExtensionRepoArtifact::getArtifactId) + .thenComparing(ExtensionRepoArtifact::getGroupId) + .thenComparing(ExtensionRepoArtifact::getBucketName) + .compare(this, o); + } + + @Override + public int hashCode() { + return Objects.hash(this.bucketName, this.groupId, this.artifactId) ; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + + final ExtensionRepoArtifact other = (ExtensionRepoArtifact) obj; + + return Objects.equals(this.getBucketName(), other.getBucketName()) + && Objects.equals(this.getGroupId(), other.getGroupId()) + && Objects.equals(this.getArtifactId(), other.getArtifactId()); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoBucket.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoBucket.java new file mode 100644 index 0000000000..1798df74c8 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoBucket.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.repo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import org.apache.nifi.registry.link.LinkableEntity; + +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Comparator; +import java.util.Objects; + +@ApiModel +@XmlRootElement +public class ExtensionRepoBucket extends LinkableEntity implements Comparable { + + private String bucketName; + + @ApiModelProperty(value = "The name of the bucket") + public String getBucketName() { + return bucketName; + } + + public void setBucketName(String bucketName) { + this.bucketName = bucketName; + } + + @Override + public int compareTo(final ExtensionRepoBucket o) { + return Comparator.comparing(ExtensionRepoBucket::getBucketName).compare(this, o); + } + + @Override + public int hashCode() { + return Objects.hashCode(this.bucketName); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + + final ExtensionRepoBucket other = (ExtensionRepoBucket) obj; + return Objects.equals(this.getBucketName(), other.getBucketName()); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoExtensionMetadata.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoExtensionMetadata.java new file mode 100644 index 0000000000..737b6c0f4d --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoExtensionMetadata.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.repo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import org.apache.nifi.registry.extension.component.ExtensionMetadata; +import org.apache.nifi.registry.link.LinkAdapter; +import org.apache.nifi.registry.link.LinkableDocs; +import org.apache.nifi.registry.link.LinkableEntity; + +import javax.ws.rs.core.Link; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import java.util.Comparator; + +@ApiModel +public class ExtensionRepoExtensionMetadata extends LinkableEntity implements LinkableDocs, Comparable { + + private ExtensionMetadata extensionMetadata; + private Link linkDocs; + + public ExtensionRepoExtensionMetadata() { + } + + public ExtensionRepoExtensionMetadata(final ExtensionMetadata extensionMetadata) { + this.extensionMetadata = extensionMetadata; + } + + @ApiModelProperty(value = "The extension metadata") + public ExtensionMetadata getExtensionMetadata() { + return extensionMetadata; + } + + public void setExtensionMetadata(ExtensionMetadata extensionMetadata) { + this.extensionMetadata = extensionMetadata; + } + + @Override + @XmlElement + @XmlJavaTypeAdapter(LinkAdapter.class) + @ApiModelProperty(value = "A WebLink to the documentation for this extension.", + dataType = "org.apache.nifi.registry.link.JaxbLink", readOnly = true) + public Link getLinkDocs() { + return linkDocs; + } + + @Override + public void setLinkDocs(Link link) { + this.linkDocs = link; + } + + @Override + public int compareTo(ExtensionRepoExtensionMetadata o) { + return Comparator.comparing(ExtensionRepoExtensionMetadata::getExtensionMetadata).compare(this, o); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoGroup.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoGroup.java new file mode 100644 index 0000000000..df28fba505 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoGroup.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.repo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import org.apache.nifi.registry.link.LinkableEntity; + +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Comparator; +import java.util.Objects; + +@ApiModel +@XmlRootElement +public class ExtensionRepoGroup extends LinkableEntity implements Comparable { + + private String bucketName; + private String groupId; + + @ApiModelProperty(value = "The bucket name") + public String getBucketName() { + return bucketName; + } + + public void setBucketName(String bucketName) { + this.bucketName = bucketName; + } + + @ApiModelProperty(value = "The group id") + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + @Override + public int compareTo(final ExtensionRepoGroup o) { + return Comparator.comparing(ExtensionRepoGroup::getGroupId) + .thenComparing(ExtensionRepoGroup::getBucketName) + .compare(this, o); + } + + @Override + public int hashCode() { + return Objects.hash(this.bucketName, this.groupId) ; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + + final ExtensionRepoGroup other = (ExtensionRepoGroup) obj; + + return Objects.equals(this.getBucketName(), other.getBucketName()) + && Objects.equals(this.getGroupId(), other.getGroupId()); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoVersion.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoVersion.java new file mode 100644 index 0000000000..1b29985523 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoVersion.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.repo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import org.apache.nifi.registry.link.LinkAdapter; + +import javax.ws.rs.core.Link; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; + +@ApiModel +@XmlRootElement +public class ExtensionRepoVersion { + + private Link extensionsLink; + private Link downloadLink; + private Link sha256Link; + private Boolean sha256Supplied; + + @XmlElement + @XmlJavaTypeAdapter(LinkAdapter.class) + @ApiModelProperty(value = "The WebLink to view the metadata about the extensions contained in the extension bundle.", + dataType = "org.apache.nifi.registry.link.JaxbLink", readOnly = true) + public Link getExtensionsLink() { + return extensionsLink; + } + + public void setExtensionsLink(Link extensionsLink) { + this.extensionsLink = extensionsLink; + } + + @XmlElement + @XmlJavaTypeAdapter(LinkAdapter.class) + @ApiModelProperty(value = "The WebLink to download this version of the extension bundle.", + dataType = "org.apache.nifi.registry.link.JaxbLink", readOnly = true) + public Link getDownloadLink() { + return downloadLink; + } + + public void setDownloadLink(Link downloadLink) { + this.downloadLink = downloadLink; + } + + @XmlElement + @XmlJavaTypeAdapter(LinkAdapter.class) + @ApiModelProperty(value = "The WebLink to retrieve the SHA-256 digest for this version of the extension bundle.", + dataType = "org.apache.nifi.registry.link.JaxbLink", readOnly = true) + public Link getSha256Link() { + return sha256Link; + } + + public void setSha256Link(Link sha256Link) { + this.sha256Link = sha256Link; + } + + @ApiModelProperty(value = "Indicates if the client supplied a SHA-256 when uploading this version of the extension bundle.", + dataType = "org.apache.nifi.registry.link.JaxbLink", readOnly = true) + public Boolean getSha256Supplied() { + return sha256Supplied; + } + + public void setSha256Supplied(Boolean sha256Supplied) { + this.sha256Supplied = sha256Supplied; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoVersionSummary.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoVersionSummary.java new file mode 100644 index 0000000000..41eed21650 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoVersionSummary.java @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension.repo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import org.apache.nifi.registry.link.LinkableEntity; + +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Comparator; +import java.util.Objects; + +@ApiModel +@XmlRootElement +public class ExtensionRepoVersionSummary extends LinkableEntity implements Comparable { + + private String bucketName; + + private String groupId; + private String artifactId; + private String version; + + private String author; + private Long timestamp; + + @ApiModelProperty(value = "The bucket name") + public String getBucketName() { + return bucketName; + } + + public void setBucketName(String bucketName) { + this.bucketName = bucketName; + } + + @ApiModelProperty("The group id") + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + @ApiModelProperty("The artifact id") + public String getArtifactId() { + return artifactId; + } + + public void setArtifactId(String artifactId) { + this.artifactId = artifactId; + } + + @ApiModelProperty("The version") + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + @ApiModelProperty("The identity of the user that created this version") + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + @ApiModelProperty("The timestamp of when this version was created") + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + @Override + public int compareTo(ExtensionRepoVersionSummary o) { + return Comparator.comparing(ExtensionRepoVersionSummary::getVersion) + .thenComparing(ExtensionRepoVersionSummary::getArtifactId) + .thenComparing(ExtensionRepoVersionSummary::getGroupId) + .thenComparing(ExtensionRepoVersionSummary::getBucketName) + .compare(this, o); + } + + @Override + public int hashCode() { + return Objects.hash(this.bucketName, this.groupId, this.artifactId, this.version, this.author, this.timestamp); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + + final ExtensionRepoVersionSummary other = (ExtensionRepoVersionSummary) obj; + + return Objects.equals(this.getBucketName(), other.getBucketName()) + && Objects.equals(this.getGroupId(), other.getGroupId()) + && Objects.equals(this.getArtifactId(), other.getArtifactId()) + && Objects.equals(this.getVersion(), other.getVersion()) + && Objects.equals(this.getAuthor(), other.getAuthor()) + && Objects.equals(this.getVersion(), other.getVersion()); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/field/Fields.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/field/Fields.java new file mode 100644 index 0000000000..d1aac6d805 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/field/Fields.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.field; + +import java.util.Set; + +public class Fields { + + private Set fields; + + public Fields() { + + } + + public Fields(Set fields) { + this.fields = fields; + } + + public Set getFields() { + return fields; + } + + public void setFields(Set fields) { + this.fields = fields; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/BatchSize.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/BatchSize.java new file mode 100644 index 0000000000..5a51240cd8 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/BatchSize.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +import io.swagger.annotations.ApiModelProperty; +import java.util.Objects; + + +public class BatchSize { + private Integer count; + private String size; + private String duration; + + @ApiModelProperty("Preferred number of flow files to include in a transaction.") + public Integer getCount() { + return count; + } + + public void setCount(Integer count) { + this.count = count; + } + + @ApiModelProperty("Preferred number of bytes to include in a transaction.") + public String getSize() { + return size; + } + + public void setSize(String size) { + this.size = size; + } + + @ApiModelProperty("Preferred amount of time that a transaction should span.") + public String getDuration() { + return duration; + } + + public void setDuration(String duration) { + this.duration = duration; + } + + @Override + public int hashCode() { + return Objects.hash(count, duration, size); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + + final BatchSize other = (BatchSize) obj; + return Objects.equals(count, other.count) && Objects.equals(size, other.size) && Objects.equals(duration, other.duration); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/Bundle.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/Bundle.java new file mode 100644 index 0000000000..1050ac9cae --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/Bundle.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +import java.util.Objects; + +import io.swagger.annotations.ApiModelProperty; + +public class Bundle { + private String group; + private String artifact; + private String version; + + public Bundle() { + } + + public Bundle(final String group, final String artifact, final String version) { + this.group = group; + this.artifact = artifact; + this.version = version; + } + + @ApiModelProperty("The group of the bundle") + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + @ApiModelProperty("The artifact of the bundle") + public String getArtifact() { + return artifact; + } + + public void setArtifact(String artifact) { + this.artifact = artifact; + } + + @ApiModelProperty("The version of the bundle") + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + final Bundle other = (Bundle) obj; + return Objects.equals(group, other.group) && Objects.equals(artifact, other.artifact) && Objects.equals(version, other.version); + } + + @Override + public int hashCode() { + return Objects.hash(group, artifact, version); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ComponentType.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ComponentType.java new file mode 100644 index 0000000000..300c146a56 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ComponentType.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +public enum ComponentType { + + CONNECTION("Connection"), + PROCESSOR("Processor"), + PROCESS_GROUP("Process Group"), + REMOTE_PROCESS_GROUP("Remote Process Group"), + INPUT_PORT("Input Port"), + OUTPUT_PORT("Output Port"), + REMOTE_INPUT_PORT("Remote Input Port"), + REMOTE_OUTPUT_PORT("Remote Output Port"), + FUNNEL("Funnel"), + LABEL("Label"), + CONTROLLER_SERVICE("Controller Service"); + + + private final String typeName; + + private ComponentType(final String typeName) { + this.typeName = typeName; + } + + public String getTypeName() { + return typeName; + } + + @Override + public String toString() { + return typeName; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ConnectableComponent.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ConnectableComponent.java new file mode 100644 index 0000000000..de144f247d --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ConnectableComponent.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +import io.swagger.annotations.ApiModelProperty; +import java.util.Objects; + + +public class ConnectableComponent { + private String id; + private ConnectableComponentType type; + private String groupId; + private String name; + private String comments; + + @ApiModelProperty(value = "The id of the connectable component.", required = true) + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @ApiModelProperty(value = "The type of component the connectable is.", required = true) + public ConnectableComponentType getType() { + return type; + } + + public void setType(ConnectableComponentType type) { + this.type = type; + } + + @ApiModelProperty(value = "The id of the group that the connectable component resides in", required = true) + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + @ApiModelProperty("The name of the connectable component") + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @ApiModelProperty("The comments for the connectable component.") + public String getComments() { + return comments; + } + + public void setComments(String comments) { + this.comments = comments; + } + + @Override + public int hashCode() { + return Objects.hash(id, groupId, name, type, comments); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (obj == this) { + return true; + } + if (!(obj instanceof ConnectableComponent)) { + return false; + } + final ConnectableComponent other = (ConnectableComponent) obj; + return Objects.equals(id, other.id); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ConnectableComponentType.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ConnectableComponentType.java new file mode 100644 index 0000000000..1b73cac2a5 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ConnectableComponentType.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +public enum ConnectableComponentType { + PROCESSOR, + REMOTE_INPUT_PORT, + REMOTE_OUTPUT_PORT, + INPUT_PORT, + OUTPUT_PORT, + FUNNEL; +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ControllerServiceAPI.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ControllerServiceAPI.java new file mode 100644 index 0000000000..b46e87afc6 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ControllerServiceAPI.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +import java.util.Objects; + +import io.swagger.annotations.ApiModelProperty; + +public class ControllerServiceAPI { + private String type; + private Bundle bundle; + + @ApiModelProperty("The fully qualified name of the service interface.") + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + @ApiModelProperty("The details of the artifact that bundled this service interface.") + public Bundle getBundle() { + return bundle; + } + + public void setBundle(Bundle bundle) { + this.bundle = bundle; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + final ControllerServiceAPI other = (ControllerServiceAPI) o; + return Objects.equals(type, other.type) && Objects.equals(bundle, other.bundle); + } + + @Override + public int hashCode() { + return Objects.hash(type, bundle); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ExternalControllerServiceReference.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ExternalControllerServiceReference.java new file mode 100644 index 0000000000..3bc269e7a9 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ExternalControllerServiceReference.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +@ApiModel +public class ExternalControllerServiceReference { + + private String identifier; + private String name; + + @ApiModelProperty("The identifier of the controller service") + public String getIdentifier() { + return identifier; + } + + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + + @ApiModelProperty("The name of the controller service") + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/PortType.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/PortType.java new file mode 100644 index 0000000000..6a32c119d3 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/PortType.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +public enum PortType { + INPUT_PORT, + OUTPUT_PORT; +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/Position.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/Position.java new file mode 100644 index 0000000000..aaba748637 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/Position.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import java.util.Objects; + +@ApiModel(description = "The position of a component on the graph") +public class Position { + private double x; + private double y; + + public Position() { + } + + public Position(double x, double y) { + this.x = x; + this.y = y; + } + + @ApiModelProperty("The x coordinate.") + public double getX() { + return x; + } + + public void setX(double x) { + this.x = x; + } + + @ApiModelProperty("The y coordinate.") + public double getY() { + return y; + } + + public void setY(double y) { + this.y = y; + } + + @Override + public String toString() { + return "[x=" + x + ", y=" + y + "]"; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Position position = (Position) o; + return Double.compare(position.x, x) == 0 && Double.compare(position.y, y) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(x, y); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ScheduledState.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ScheduledState.java new file mode 100644 index 0000000000..e44f73dcdd --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ScheduledState.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +public enum ScheduledState { + ENABLED, + DISABLED; +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/SiteToSiteTransportProtocol.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/SiteToSiteTransportProtocol.java new file mode 100644 index 0000000000..9f94c1a811 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/SiteToSiteTransportProtocol.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +public enum SiteToSiteTransportProtocol { + RAW, + HTTP; +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedComponent.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedComponent.java new file mode 100644 index 0000000000..b3a27aaac3 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedComponent.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +import java.util.Objects; + +import io.swagger.annotations.ApiModelProperty; + + +public abstract class VersionedComponent { + + private String identifier; + private String groupId; + private String name; + private String comments; + private Position position; + + @ApiModelProperty("The component's unique identifier") + public String getIdentifier() { + return identifier; + } + + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + + @ApiModelProperty("The ID of the Process Group that this component belongs to") + public String getGroupIdentifier() { + return groupId; + } + + public void setGroupIdentifier(String groupId) { + this.groupId = groupId; + } + + @ApiModelProperty("The component's name") + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @ApiModelProperty("The component's position on the graph") + public Position getPosition() { + return position; + } + + public void setPosition(Position position) { + this.position = position; + } + + @ApiModelProperty("The user-supplied comments for the component") + public String getComments() { + return comments; + } + + public void setComments(String comments) { + this.comments = comments; + } + + public abstract ComponentType getComponentType(); + + public void setComponentType(ComponentType type) { + // purposely do nothing here, this just to allow unmarshalling + } + + @Override + public int hashCode() { + return Objects.hash(identifier); + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (obj == this) { + return true; + } + if (!(obj instanceof VersionedComponent)) { + return false; + } + final VersionedComponent other = (VersionedComponent) obj; + return Objects.equals(identifier, other.identifier); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedConfigurableComponent.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedConfigurableComponent.java new file mode 100644 index 0000000000..3201c5f195 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedConfigurableComponent.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +import java.util.Map; + +/** + * A component that has property descriptors and can be configured with values for those properties. + */ +public interface VersionedConfigurableComponent { + + Map getPropertyDescriptors(); + + void setPropertyDescriptors(Map propertyDescriptors); + + Map getProperties(); + + void setProperties(Map properties); + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedConnection.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedConnection.java new file mode 100644 index 0000000000..52b7d70d3f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedConnection.java @@ -0,0 +1,177 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +import java.util.List; +import java.util.Set; + +import io.swagger.annotations.ApiModelProperty; + +public class VersionedConnection extends VersionedComponent { + private ConnectableComponent source; + private ConnectableComponent destination; + private Integer labelIndex; + private Long zIndex; + private Set selectedRelationships; + + private Long backPressureObjectThreshold; + private String backPressureDataSizeThreshold; + private String flowFileExpiration; + private List prioritizers; + private List bends; + + private String loadBalanceStrategy; + private String partitioningAttribute; + private String loadBalanceCompression; + + + @ApiModelProperty("The source of the connection.") + public ConnectableComponent getSource() { + return source; + } + + public void setSource(ConnectableComponent source) { + this.source = source; + } + + @ApiModelProperty("The destination of the connection.") + public ConnectableComponent getDestination() { + return destination; + } + + public void setDestination(ConnectableComponent destination) { + this.destination = destination; + } + + @ApiModelProperty("The bend points on the connection.") + public List getBends() { + return bends; + } + + public void setBends(List bends) { + this.bends = bends; + } + + @ApiModelProperty("The index of the bend point where to place the connection label.") + public Integer getLabelIndex() { + return labelIndex; + } + + public void setLabelIndex(Integer labelIndex) { + this.labelIndex = labelIndex; + } + + @ApiModelProperty( + value = "The z index of the connection.", + name = "zIndex") // Jackson maps this method name to JSON key "zIndex", but Swagger does not by default + public Long getzIndex() { + return zIndex; + } + + public void setzIndex(Long zIndex) { + this.zIndex = zIndex; + } + + @ApiModelProperty("The selected relationship that comprise the connection.") + public Set getSelectedRelationships() { + return selectedRelationships; + } + + public void setSelectedRelationships(Set relationships) { + this.selectedRelationships = relationships; + } + + + @ApiModelProperty("The object count threshold for determining when back pressure is applied. Updating this value is a passive change in the sense that it won't impact whether existing files " + + "over the limit are affected but it does help feeder processors to stop pushing too much into this work queue.") + public Long getBackPressureObjectThreshold() { + return backPressureObjectThreshold; + } + + public void setBackPressureObjectThreshold(Long backPressureObjectThreshold) { + this.backPressureObjectThreshold = backPressureObjectThreshold; + } + + + @ApiModelProperty("The object data size threshold for determining when back pressure is applied. Updating this value is a passive change in the sense that it won't impact whether existing " + + "files over the limit are affected but it does help feeder processors to stop pushing too much into this work queue.") + public String getBackPressureDataSizeThreshold() { + return backPressureDataSizeThreshold; + } + + public void setBackPressureDataSizeThreshold(String backPressureDataSizeThreshold) { + this.backPressureDataSizeThreshold = backPressureDataSizeThreshold; + } + + + @ApiModelProperty("The amount of time a flow file may be in the flow before it will be automatically aged out of the flow. Once a flow file reaches this age it will be terminated from " + + "the flow the next time a processor attempts to start work on it.") + public String getFlowFileExpiration() { + return flowFileExpiration; + } + + public void setFlowFileExpiration(String flowFileExpiration) { + this.flowFileExpiration = flowFileExpiration; + } + + + @ApiModelProperty("The comparators used to prioritize the queue.") + public List getPrioritizers() { + return prioritizers; + } + + public void setPrioritizers(List prioritizers) { + this.prioritizers = prioritizers; + } + + @ApiModelProperty(value = "The Strategy to use for load balancing data across the cluster, or null, if no Load Balance Strategy has been specified.", + allowableValues = "DO_NOT_LOAD_BALANCE, PARTITION_BY_ATTRIBUTE, ROUND_ROBIN, SINGLE_NODE") + public String getLoadBalanceStrategy() { + return loadBalanceStrategy; + } + + public void setLoadBalanceStrategy(String loadBalanceStrategy) { + this.loadBalanceStrategy = loadBalanceStrategy; + } + + @ApiModelProperty("The attribute to use for partitioning data as it is load balanced across the cluster. If the Load Balance Strategy is configured to use PARTITION_BY_ATTRIBUTE, the value " + + "returned by this method is the name of the FlowFile Attribute that will be used to determine which node in the cluster should receive a given FlowFile. If the Load Balance Strategy is " + + "unset or is set to any other value, the Partitioning Attribute has no effect.") + public String getPartitioningAttribute() { + return partitioningAttribute; + } + + public void setPartitioningAttribute(final String partitioningAttribute) { + this.partitioningAttribute = partitioningAttribute; + } + + @ApiModelProperty(value = "Whether or not compression should be used when transferring FlowFiles between nodes", + allowableValues = "DO_NOT_COMPRESS, COMPRESS_ATTRIBUTES_ONLY, COMPRESS_ATTRIBUTES_AND_CONTENT") + public String getLoadBalanceCompression() { + return loadBalanceCompression; + } + + public void setLoadBalanceCompression(final String compression) { + this.loadBalanceCompression = compression; + } + + @Override + public ComponentType getComponentType() { + return ComponentType.CONNECTION; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedControllerService.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedControllerService.java new file mode 100644 index 0000000000..7b14ac2c72 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedControllerService.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +import java.util.List; +import java.util.Map; + +import io.swagger.annotations.ApiModelProperty; + +public class VersionedControllerService extends VersionedComponent + implements VersionedConfigurableComponent, VersionedExtensionComponent { + + private String type; + private Bundle bundle; + private List controllerServiceApis; + + private Map properties; + private Map propertyDescriptors; + private String annotationData; + + + @Override + @ApiModelProperty(value = "The type of the controller service.") + public String getType() { + return type; + } + + @Override + public void setType(String type) { + this.type = type; + } + + @Override + @ApiModelProperty(value = "The details of the artifact that bundled this processor type.") + public Bundle getBundle() { + return bundle; + } + + @Override + public void setBundle(Bundle bundle) { + this.bundle = bundle; + } + + @ApiModelProperty(value = "Lists the APIs this Controller Service implements.") + public List getControllerServiceApis() { + return controllerServiceApis; + } + + public void setControllerServiceApis(List controllerServiceApis) { + this.controllerServiceApis = controllerServiceApis; + } + + @Override + @ApiModelProperty(value = "The properties of the controller service.") + public Map getProperties() { + return properties; + } + + @Override + public void setProperties(Map properties) { + this.properties = properties; + } + + @Override + @ApiModelProperty("The property descriptors for the processor.") + public Map getPropertyDescriptors() { + return propertyDescriptors; + } + + @Override + public void setPropertyDescriptors(Map propertyDescriptors) { + this.propertyDescriptors = propertyDescriptors; + } + + @ApiModelProperty(value = "The annotation for the controller service. This is how the custom UI relays configuration to the controller service.") + public String getAnnotationData() { + return annotationData; + } + + public void setAnnotationData(String annotationData) { + this.annotationData = annotationData; + } + + @Override + public ComponentType getComponentType() { + return ComponentType.CONTROLLER_SERVICE; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedExtensionComponent.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedExtensionComponent.java new file mode 100644 index 0000000000..e1d514c2a0 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedExtensionComponent.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +/** + * A component that is an extension and has a type and bundle. + */ +public interface VersionedExtensionComponent { + + Bundle getBundle(); + + void setBundle(Bundle bundle); + + String getType(); + + void setType(String type); + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlow.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlow.java new file mode 100644 index 0000000000..818dddd3b6 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlow.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import org.apache.nifi.registry.bucket.BucketItem; +import org.apache.nifi.registry.bucket.BucketItemType; +import org.apache.nifi.registry.revision.entity.RevisableEntity; +import org.apache.nifi.registry.revision.entity.RevisionInfo; + +import javax.validation.constraints.Min; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Objects; + +/** + *

+ * Represents a versioned flow. A versioned flow is a named flow that is expected to change + * over time. This flow is saved to the registry with information such as its name, a description, + * and each version of the flow. + *

+ * + * @see VersionedFlowSnapshot + */ +@XmlRootElement +@ApiModel +public class VersionedFlow extends BucketItem implements RevisableEntity { + + @Min(0) + private long versionCount; + + private RevisionInfo revision; + + public VersionedFlow() { + super(BucketItemType.Flow); + } + + @ApiModelProperty(value = "The number of versions of this flow.", readOnly = true) + public long getVersionCount() { + return versionCount; + } + + public void setVersionCount(long versionCount) { + this.versionCount = versionCount; + } + + @ApiModelProperty( + value = "The revision of this entity used for optimistic-locking during updates.", + readOnly = true + ) + @Override + public RevisionInfo getRevision() { + return revision; + } + + @Override + public void setRevision(RevisionInfo revision) { + this.revision = revision; + } + + @Override + public int hashCode() { + return Objects.hashCode(this.getIdentifier()); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlowCoordinates.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlowCoordinates.java new file mode 100644 index 0000000000..8e39c5b419 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlowCoordinates.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +import java.util.Objects; + +import io.swagger.annotations.ApiModelProperty; + +public class VersionedFlowCoordinates { + private String registryUrl; + private String bucketId; + private String flowId; + private int version; + private Boolean latest; + + @ApiModelProperty("The URL of the Flow Registry that contains the flow") + public String getRegistryUrl() { + return registryUrl; + } + + public void setRegistryUrl(String registryUrl) { + this.registryUrl = registryUrl; + } + + @ApiModelProperty("The UUID of the bucket that the flow resides in") + public String getBucketId() { + return bucketId; + } + + public void setBucketId(String bucketId) { + this.bucketId = bucketId; + } + + @ApiModelProperty("The UUID of the flow") + public String getFlowId() { + return flowId; + } + + public void setFlowId(String flowId) { + this.flowId = flowId; + } + + @ApiModelProperty("The version of the flow") + public int getVersion() { + return version; + } + + public void setVersion(int version) { + this.version = version; + } + + @ApiModelProperty("Whether or not these coordinates point to the latest version of the flow") + public Boolean getLatest() { + return latest; + } + + public void setLatest(Boolean latest) { + this.latest = latest; + } + + @Override + public int hashCode() { + return Objects.hash(registryUrl, bucketId, flowId, version); + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (obj == this) { + return true; + } + if (!(obj instanceof VersionedFlowCoordinates)) { + return false; + } + + final VersionedFlowCoordinates other = (VersionedFlowCoordinates) obj; + return Objects.equals(registryUrl, other.registryUrl) && Objects.equals(bucketId, other.bucketId) && Objects.equals(flowId, other.flowId) && Objects.equals(version, other.version); + } + + @Override + public String toString() { + return "VersionedFlowCoordinates[bucketId=" + bucketId + ", flowId=" + flowId + ", version=" + version + ", registryUrl=" + registryUrl + "]"; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlowSnapshot.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlowSnapshot.java new file mode 100644 index 0000000000..bf6ad989f3 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlowSnapshot.java @@ -0,0 +1,177 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import org.apache.nifi.registry.bucket.Bucket; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlTransient; +import java.util.Map; +import java.util.Objects; + +/** + *

+ * Represents a snapshot of a versioned flow. A versioned flow may change many times + * over the course of its life. Each of these versions that is saved to the registry + * is saved as a snapshot, representing information such as the name of the flow, the + * version of the flow, the timestamp when it was saved, the contents of the flow, etc. + *

+ *

+ * With the advent of download and upload flow in NiFi, this class is now used outside of + * NiFi Registry to represent a snapshot of an unversioned flow, which is purely the + * flow contents without any versioning or snapshot metadata. + *

+ */ +@ApiModel +@XmlRootElement +public class VersionedFlowSnapshot { + + @Valid + @NotNull + private VersionedFlowSnapshotMetadata snapshotMetadata; + + @Valid + @NotNull + private VersionedProcessGroup flowContents; + + // optional map of external controller service references + private Map externalControllerServices; + + // optional parameter contexts mapped by their name + private Map parameterContexts; + + // optional encoding version that clients may specify to track how the flow contents are encoded + private String flowEncodingVersion; + + // read-only, only populated from retrieval of a snapshot + private VersionedFlow flow; + + // read-only, only populated from retrieval of a snapshot + private Bucket bucket; + + @ApiModelProperty(value = "The metadata for this snapshot", required = true) + public VersionedFlowSnapshotMetadata getSnapshotMetadata() { + return snapshotMetadata; + } + + public void setSnapshotMetadata(VersionedFlowSnapshotMetadata snapshotMetadata) { + this.snapshotMetadata = snapshotMetadata; + } + + @ApiModelProperty(value = "The contents of the versioned flow", required = true) + public VersionedProcessGroup getFlowContents() { + return flowContents; + } + + public void setFlowContents(VersionedProcessGroup flowContents) { + this.flowContents = flowContents; + } + + @ApiModelProperty("The information about controller services that exist outside this versioned flow, but are referenced by components within the versioned flow.") + public Map getExternalControllerServices() { + return externalControllerServices; + } + + public void setExternalControllerServices(Map externalControllerServices) { + this.externalControllerServices = externalControllerServices; + } + + @ApiModelProperty(value = "The flow this snapshot is for", readOnly = true) + public VersionedFlow getFlow() { + return flow; + } + + public void setFlow(VersionedFlow flow) { + this.flow = flow; + } + + @ApiModelProperty(value = "The bucket where the flow is located", readOnly = true) + public Bucket getBucket() { + return bucket; + } + + public void setBucket(Bucket bucket) { + this.bucket = bucket; + } + + @ApiModelProperty(value = "The parameter contexts referenced by process groups in the flow contents. " + + "The mapping is from the name of the context to the context instance, and it is expected that any " + + "context in this map is referenced by at least one process group in this flow.") + public Map getParameterContexts() { + return parameterContexts; + } + + public void setParameterContexts(Map parameterContexts) { + this.parameterContexts = parameterContexts; + } + + @ApiModelProperty(value = "The optional encoding version of the flow contents.") + public String getFlowEncodingVersion() { + return flowEncodingVersion; + } + + public void setFlowEncodingVersion(String flowEncodingVersion) { + this.flowEncodingVersion = flowEncodingVersion; + } + + /** + * This is a convenience method that will return true when flow is populated and when the flow's versionCount + * is equal to the version of this snapshot. + * + * @return true if flow is populated and if this snapshot is the latest version for the flow at the time of retrieval + */ + @XmlTransient + public boolean isLatest() { + return flow != null && snapshotMetadata != null && flow.getVersionCount() == getSnapshotMetadata().getVersion(); + } + + @Override + public int hashCode() { + return Objects.hash(this.snapshotMetadata); + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + + final VersionedFlowSnapshot other = (VersionedFlowSnapshot) obj; + return Objects.equals(this.snapshotMetadata, other.snapshotMetadata); + } + + @Override + public String toString() { + // snapshotMetadata and flow will be null when this is used to represent an unversioned flow + if (snapshotMetadata == null) { + return "VersionedFlowSnapshot[flowContentsId=" + flowContents.getIdentifier() + ", flowContentsName=" + + flowContents.getName() + ", NoMetadataAvailable]"; + } else { + final String flowName = (flow == null ? "null" : flow.getName()); + return "VersionedFlowSnapshot[flowId=" + snapshotMetadata.getFlowIdentifier() + ", flowName=" + flowName + + ", version=" + snapshotMetadata.getVersion() + ", comments=" + snapshotMetadata.getComments() + "]"; + } + } +} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlowSnapshotMetadata.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlowSnapshotMetadata.java new file mode 100644 index 0000000000..dab27e3bc5 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlowSnapshotMetadata.java @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import org.apache.nifi.registry.link.LinkableEntity; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import java.util.Objects; + +/** + * The metadata information about a VersionedFlowSnapshot. This class implements Comparable in order + * to sort based on the snapshot version in ascending order. + */ +@ApiModel +public class VersionedFlowSnapshotMetadata extends LinkableEntity implements Comparable { + + @NotBlank + private String bucketIdentifier; + + @NotBlank + private String flowIdentifier; + + @Min(-1) + private int version; + + @Min(1) + private long timestamp; + + @NotBlank + private String author; + + private String comments; + + + @ApiModelProperty(value = "The identifier of the bucket this snapshot belongs to.", required = true) + public String getBucketIdentifier() { + return bucketIdentifier; + } + + public void setBucketIdentifier(String bucketIdentifier) { + this.bucketIdentifier = bucketIdentifier; + } + + @ApiModelProperty(value = "The identifier of the flow this snapshot belongs to.", required = true) + public String getFlowIdentifier() { + return flowIdentifier; + } + + public void setFlowIdentifier(String flowIdentifier) { + this.flowIdentifier = flowIdentifier; + } + + @ApiModelProperty(value = "The version of this snapshot of the flow.", required = true) + public int getVersion() { + return version; + } + + public void setVersion(int version) { + this.version = version; + } + + @ApiModelProperty(value = "The timestamp when the flow was saved, as milliseconds since epoch.", readOnly = true) + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + @ApiModelProperty(value = "The user that created this snapshot of the flow.", readOnly = true) + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + @ApiModelProperty("The comments provided by the user when creating the snapshot.") + public String getComments() { + return comments; + } + + public void setComments(String comments) { + this.comments = comments; + } + + @Override + public int compareTo(final VersionedFlowSnapshotMetadata o) { + return o == null ? -1 : Integer.compare(version, o.version); + } + + @Override + public int hashCode() { + return Objects.hash(this.flowIdentifier, Integer.valueOf(this.version)); + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + + final VersionedFlowSnapshotMetadata other = (VersionedFlowSnapshotMetadata) obj; + + return Objects.equals(this.flowIdentifier, other.flowIdentifier) + && Objects.equals(this.version, other.version); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFunnel.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFunnel.java new file mode 100644 index 0000000000..871dafc08f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFunnel.java @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +public class VersionedFunnel extends VersionedComponent { + @Override + public ComponentType getComponentType() { + return ComponentType.FUNNEL; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedLabel.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedLabel.java new file mode 100644 index 0000000000..f2f78879c0 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedLabel.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +import java.util.Map; + +import io.swagger.annotations.ApiModelProperty; + +public class VersionedLabel extends VersionedComponent { + private String label; + + private Double width; + private Double height; + + private Map style; + + + @ApiModelProperty("The text that appears in the label.") + public String getLabel() { + return label; + } + + public void setLabel(final String label) { + this.label = label; + } + + @ApiModelProperty("The styles for this label (font-size : 12px, background-color : #eee, etc).") + public Map getStyle() { + return style; + } + + public void setStyle(final Map style) { + this.style = style; + } + + @ApiModelProperty("The height of the label in pixels when at a 1:1 scale.") + public Double getHeight() { + return height; + } + + public void setHeight(Double height) { + this.height = height; + } + + @ApiModelProperty("The width of the label in pixels when at a 1:1 scale.") + public Double getWidth() { + return width; + } + + public void setWidth(Double width) { + this.width = width; + } + + @Override + public ComponentType getComponentType() { + return ComponentType.LABEL; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedParameter.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedParameter.java new file mode 100644 index 0000000000..857dd167da --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedParameter.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +import io.swagger.annotations.ApiModelProperty; + +import java.util.Objects; + +public class VersionedParameter { + + private String name; + private String description; + private boolean sensitive; + private String value; + + @ApiModelProperty("The name of the parameter") + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @ApiModelProperty("The description of the param") + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @ApiModelProperty("Whether or not the parameter value is sensitive") + public boolean isSensitive() { + return sensitive; + } + + public void setSensitive(boolean sensitive) { + this.sensitive = sensitive; + } + + @ApiModelProperty("The value of the parameter") + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + VersionedParameter that = (VersionedParameter) o; + return Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedParameterContext.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedParameterContext.java new file mode 100644 index 0000000000..bb0b8374dc --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedParameterContext.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +import io.swagger.annotations.ApiModelProperty; + +import java.util.Set; + +public class VersionedParameterContext { + + private String name; + private String description; + private Set parameters; + + @ApiModelProperty("The name of the context") + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @ApiModelProperty("The description of the parameter context") + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @ApiModelProperty("The parameters in the context") + public Set getParameters() { + return parameters; + } + + public void setParameters(Set parameters) { + this.parameters = parameters; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedPort.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedPort.java new file mode 100644 index 0000000000..947b52e2d2 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedPort.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +import io.swagger.annotations.ApiModelProperty; + +public class VersionedPort extends VersionedComponent { + private PortType type; + private Integer concurrentlySchedulableTaskCount; + private ScheduledState scheduledState; + private boolean allowRemoteAccess; + + @ApiModelProperty("The number of tasks that should be concurrently scheduled for the port.") + public Integer getConcurrentlySchedulableTaskCount() { + return concurrentlySchedulableTaskCount; + } + + public void setConcurrentlySchedulableTaskCount(Integer concurrentlySchedulableTaskCount) { + this.concurrentlySchedulableTaskCount = concurrentlySchedulableTaskCount; + } + + @ApiModelProperty("The type of port.") + public PortType getType() { + return type; + } + + public void setType(PortType type) { + this.type = type; + } + + @ApiModelProperty("The scheduled state of the component") + public ScheduledState getScheduledState() { + return scheduledState; + } + + public void setScheduledState(ScheduledState scheduledState) { + this.scheduledState = scheduledState; + } + + @ApiModelProperty("Whether or not this port allows remote access for site-to-site") + public boolean isAllowRemoteAccess() { + return allowRemoteAccess; + } + + public void setAllowRemoteAccess(boolean allowRemoteAccess) { + this.allowRemoteAccess = allowRemoteAccess; + } + + @Override + public ComponentType getComponentType() { + if (type == PortType.OUTPUT_PORT) { + return ComponentType.OUTPUT_PORT; + } + + return ComponentType.INPUT_PORT; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedProcessGroup.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedProcessGroup.java new file mode 100644 index 0000000000..93e727b7ab --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedProcessGroup.java @@ -0,0 +1,178 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +import io.swagger.annotations.ApiModelProperty; + +import javax.xml.bind.annotation.XmlRootElement; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +@XmlRootElement +public class VersionedProcessGroup extends VersionedComponent { + + private Set processGroups = new HashSet<>(); + private Set remoteProcessGroups = new HashSet<>(); + private Set processors = new HashSet<>(); + private Set inputPorts = new HashSet<>(); + private Set outputPorts = new HashSet<>(); + private Set connections = new HashSet<>(); + private Set labels = new HashSet<>(); + private Set funnels = new HashSet<>(); + private Set controllerServices = new HashSet<>(); + private VersionedFlowCoordinates versionedFlowCoordinates = null; + + private Map variables = new HashMap<>(); + + private String parameterContextName; + private String flowfileConcurrency; + private String flowfileOutboundPolicy; + + @ApiModelProperty("The child Process Groups") + public Set getProcessGroups() { + return processGroups; + } + + public void setProcessGroups(Set processGroups) { + this.processGroups = new HashSet<>(processGroups); + } + + @ApiModelProperty("The Remote Process Groups") + public Set getRemoteProcessGroups() { + return remoteProcessGroups; + } + + public void setRemoteProcessGroups(Set remoteProcessGroups) { + this.remoteProcessGroups = new HashSet<>(remoteProcessGroups); + } + + @ApiModelProperty("The Processors") + public Set getProcessors() { + return processors; + } + + public void setProcessors(Set processors) { + this.processors = new HashSet<>(processors); + } + + @ApiModelProperty("The Input Ports") + public Set getInputPorts() { + return inputPorts; + } + + public void setInputPorts(Set inputPorts) { + this.inputPorts = new HashSet<>(inputPorts); + } + + @ApiModelProperty("The Output Ports") + public Set getOutputPorts() { + return outputPorts; + } + + public void setOutputPorts(Set outputPorts) { + this.outputPorts = new HashSet<>(outputPorts); + } + + @ApiModelProperty("The Connections") + public Set getConnections() { + return connections; + } + + public void setConnections(Set connections) { + this.connections = new HashSet<>(connections); + } + + @ApiModelProperty("The Labels") + public Set getLabels() { + return labels; + } + + public void setLabels(Set labels) { + this.labels = new HashSet<>(labels); + } + + @ApiModelProperty("The Funnels") + public Set getFunnels() { + return funnels; + } + + public void setFunnels(Set funnels) { + this.funnels = new HashSet<>(funnels); + } + + @ApiModelProperty("The Controller Services") + public Set getControllerServices() { + return controllerServices; + } + + public void setControllerServices(Set controllerServices) { + this.controllerServices = new HashSet<>(controllerServices); + } + + @Override + public ComponentType getComponentType() { + return ComponentType.PROCESS_GROUP; + } + + public void setVariables(Map variables) { + this.variables = variables; + } + + @ApiModelProperty("The Variables in the Variable Registry for this Process Group (not including any ancestor or descendant Process Groups)") + public Map getVariables() { + return variables; + } + + public void setVersionedFlowCoordinates(VersionedFlowCoordinates flowCoordinates) { + this.versionedFlowCoordinates = flowCoordinates; + } + + @ApiModelProperty("The coordinates where the remote flow is stored, or null if the Process Group is not directly under Version Control") + public VersionedFlowCoordinates getVersionedFlowCoordinates() { + return versionedFlowCoordinates; + } + + @ApiModelProperty("The name of the parameter context used by this process group") + public String getParameterContextName() { + return parameterContextName; + } + + public void setParameterContextName(String parameterContextName) { + this.parameterContextName = parameterContextName; + } + + @ApiModelProperty(value = "The configured FlowFile Concurrency for the Process Group") + public String getFlowFileConcurrency() { + return flowfileConcurrency; + } + + public void setFlowFileConcurrency(final String flowfileConcurrency) { + this.flowfileConcurrency = flowfileConcurrency; + } + + @ApiModelProperty(value = "The FlowFile Outbound Policy for the Process Group") + public String getFlowFileOutboundPolicy() { + return flowfileOutboundPolicy; + } + + public void setFlowFileOutboundPolicy(final String outboundPolicy) { + this.flowfileOutboundPolicy = outboundPolicy; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedProcessor.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedProcessor.java new file mode 100644 index 0000000000..416bf4256e --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedProcessor.java @@ -0,0 +1,207 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +import io.swagger.annotations.ApiModelProperty; + +import java.util.Map; +import java.util.Set; + +public class VersionedProcessor extends VersionedComponent + implements VersionedConfigurableComponent, VersionedExtensionComponent { + + private Bundle bundle; + private Map style; + + private String type; + private Map properties; + private Map propertyDescriptors; + private String annotationData; + + private String schedulingPeriod; + private String schedulingStrategy; + private String executionNode; + private String penaltyDuration; + private String yieldDuration; + private String bulletinLevel; + private Long runDurationMillis; + private Integer concurrentlySchedulableTaskCount; + private Set autoTerminatedRelationships; + private ScheduledState scheduledState; + + @ApiModelProperty("The frequency with which to schedule the processor. The format of the value will depend on th value of schedulingStrategy.") + public String getSchedulingPeriod() { + return schedulingPeriod; + } + + public void setSchedulingPeriod(String setSchedulingPeriod) { + this.schedulingPeriod = setSchedulingPeriod; + } + + @ApiModelProperty("Indcates whether the prcessor should be scheduled to run in event or timer driven mode.") + public String getSchedulingStrategy() { + return schedulingStrategy; + } + + public void setSchedulingStrategy(String schedulingStrategy) { + this.schedulingStrategy = schedulingStrategy; + } + + @Override + @ApiModelProperty("The type of Processor") + public String getType() { + return type; + } + + @Override + public void setType(final String type) { + this.type = type; + } + + @ApiModelProperty("Indicates the node where the process will execute.") + public String getExecutionNode() { + return executionNode; + } + + public void setExecutionNode(String executionNode) { + this.executionNode = executionNode; + } + + @ApiModelProperty("The amout of time that is used when the process penalizes a flowfile.") + public String getPenaltyDuration() { + return penaltyDuration; + } + + public void setPenaltyDuration(String penaltyDuration) { + this.penaltyDuration = penaltyDuration; + } + + @ApiModelProperty("The amount of time that must elapse before this processor is scheduled again after yielding.") + public String getYieldDuration() { + return yieldDuration; + } + + public void setYieldDuration(String yieldDuration) { + this.yieldDuration = yieldDuration; + } + + @ApiModelProperty("The level at which the processor will report bulletins.") + public String getBulletinLevel() { + return bulletinLevel; + } + + public void setBulletinLevel(String bulletinLevel) { + this.bulletinLevel = bulletinLevel; + } + + @ApiModelProperty("The number of tasks that should be concurrently schedule for the processor. If the processor doesn't allow parallol processing then any positive input will be ignored.") + public Integer getConcurrentlySchedulableTaskCount() { + return concurrentlySchedulableTaskCount; + } + + public void setConcurrentlySchedulableTaskCount(Integer concurrentlySchedulableTaskCount) { + this.concurrentlySchedulableTaskCount = concurrentlySchedulableTaskCount; + } + + @Override + @ApiModelProperty("The properties for the processor. Properties whose value is not set will only contain the property name.") + public Map getProperties() { + return properties; + } + + @Override + public void setProperties(Map properties) { + this.properties = properties; + } + + @Override + @ApiModelProperty("The property descriptors for the processor.") + public Map getPropertyDescriptors() { + return propertyDescriptors; + } + + @Override + public void setPropertyDescriptors(Map propertyDescriptors) { + this.propertyDescriptors = propertyDescriptors; + } + + @ApiModelProperty("The annotation data for the processor used to relay configuration between a custom UI and the procesosr.") + public String getAnnotationData() { + return annotationData; + } + + public void setAnnotationData(String annotationData) { + this.annotationData = annotationData; + } + + + @ApiModelProperty("The names of all relationships that cause a flow file to be terminated if the relationship is not connected elsewhere. This property differs " + + "from the 'isAutoTerminate' property of the RelationshipDTO in that the RelationshipDTO is meant to depict the current configuration, whereas this " + + "property can be set in a DTO when updating a Processor in order to change which Relationships should be auto-terminated.") + public Set getAutoTerminatedRelationships() { + return autoTerminatedRelationships; + } + + public void setAutoTerminatedRelationships(final Set autoTerminatedRelationships) { + this.autoTerminatedRelationships = autoTerminatedRelationships; + } + + @ApiModelProperty("The run duration for the processor in milliseconds.") + public Long getRunDurationMillis() { + return runDurationMillis; + } + + public void setRunDurationMillis(Long runDurationMillis) { + this.runDurationMillis = runDurationMillis; + } + + @Override + @ApiModelProperty("Information about the bundle from which the component came") + public Bundle getBundle() { + return bundle; + } + + @Override + public void setBundle(Bundle bundle) { + this.bundle = bundle; + } + + @ApiModelProperty("Stylistic data for rendering in a UI") + public Map getStyle() { + return style; + } + + public void setStyle(Map style) { + this.style = style; + } + + @ApiModelProperty("The scheduled state of the component") + public ScheduledState getScheduledState() { + return scheduledState; + } + + public void setScheduledState(ScheduledState scheduledState) { + this.scheduledState = scheduledState; + } + + @Override + public ComponentType getComponentType() { + return ComponentType.PROCESSOR; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedPropertyDescriptor.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedPropertyDescriptor.java new file mode 100644 index 0000000000..2fa94634ef --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedPropertyDescriptor.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +import io.swagger.annotations.ApiModelProperty; + +public class VersionedPropertyDescriptor { + private String name; + private String displayName; + private boolean identifiesControllerService; + private boolean sensitive; + + @ApiModelProperty("The name of the property") + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @ApiModelProperty("The display name of the property") + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + @ApiModelProperty("Whether or not the property provides the identifier of a Controller Service") + public boolean getIdentifiesControllerService() { + return identifiesControllerService; + } + + public void setIdentifiesControllerService(boolean identifiesControllerService) { + this.identifiesControllerService = identifiesControllerService; + } + + @ApiModelProperty("Whether or not the property is considered sensitive") + public boolean isSensitive() { + return sensitive; + } + + public void setSensitive(boolean sensitive) { + this.sensitive = sensitive; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedRemoteGroupPort.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedRemoteGroupPort.java new file mode 100644 index 0000000000..19f76dfe10 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedRemoteGroupPort.java @@ -0,0 +1,119 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +import io.swagger.annotations.ApiModelProperty; + +import java.util.Objects; + +public class VersionedRemoteGroupPort extends VersionedComponent { + private String remoteGroupId; + private Integer concurrentlySchedulableTaskCount; + private Boolean useCompression; + private BatchSize batchSize; + private ComponentType componentType; + private String targetId; + private ScheduledState scheduledState; + + @ApiModelProperty("The number of task that may transmit flowfiles to the target port concurrently.") + public Integer getConcurrentlySchedulableTaskCount() { + return concurrentlySchedulableTaskCount; + } + + public void setConcurrentlySchedulableTaskCount(Integer concurrentlySchedulableTaskCount) { + this.concurrentlySchedulableTaskCount = concurrentlySchedulableTaskCount; + } + + @ApiModelProperty("The id of the remote process group that the port resides in.") + public String getRemoteGroupId() { + return remoteGroupId; + } + + public void setRemoteGroupId(String groupId) { + this.remoteGroupId = groupId; + } + + + @ApiModelProperty("Whether the flowfiles are compressed when sent to the target port.") + public Boolean isUseCompression() { + return useCompression; + } + + public void setUseCompression(Boolean useCompression) { + this.useCompression = useCompression; + } + + @ApiModelProperty("The batch settings for data transmission.") + public BatchSize getBatchSize() { + return batchSize; + } + + public void setBatchSize(BatchSize batchSize) { + this.batchSize = batchSize; + } + + @ApiModelProperty("The ID of the port on the target NiFi instance") + public String getTargetId() { + return targetId; + } + + public void setTargetId(final String targetId) { + this.targetId = targetId; + } + + @ApiModelProperty("The scheduled state of the component") + public ScheduledState getScheduledState() { + return scheduledState; + } + + public void setScheduledState(ScheduledState scheduledState) { + this.scheduledState = scheduledState; + } + + @Override + public int hashCode() { + return 923847 + String.valueOf(getName()).hashCode(); + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (!(obj instanceof VersionedRemoteGroupPort)) { + return false; + } + + final VersionedRemoteGroupPort other = (VersionedRemoteGroupPort) obj; + return Objects.equals(getName(), other.getName()); + } + + @Override + public ComponentType getComponentType() { + return componentType; + } + + @Override + public void setComponentType(final ComponentType componentType) { + if (componentType != ComponentType.REMOTE_INPUT_PORT && componentType != ComponentType.REMOTE_OUTPUT_PORT) { + throw new IllegalArgumentException(); + } + + this.componentType = componentType; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedRemoteProcessGroup.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedRemoteProcessGroup.java new file mode 100644 index 0000000000..ec219b74eb --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedRemoteProcessGroup.java @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +import io.swagger.annotations.ApiModelProperty; + +import java.util.Set; + +public class VersionedRemoteProcessGroup extends VersionedComponent { + private String targetUri; + private String targetUris; + + private String communicationsTimeout; + private String yieldDuration; + private String transportProtocol; + private String localNetworkInterface; + private String proxyHost; + private Integer proxyPort; + private String proxyUser; + + private Set inputPorts; + private Set outputPorts; + + + @Deprecated + @ApiModelProperty( + value = "[DEPRECATED] The target URI of the remote process group." + + " If target uri is not set, but uris are set, then returns the first uri in the uris." + + " If neither target uri nor uris are set, then returns null.", + notes = "This field is deprecated and will be removed in version 1.x of NiFi Registry." + + " Please migrate to using targetUris only.") + public String getTargetUri() { + + if (!isEmpty(targetUri)) { + return targetUri; + } + return !isEmpty(targetUris) ? targetUris.split(",", 2)[0] : null; + + } + + public void setTargetUri(final String targetUri) { + this.targetUri = targetUri; + } + + @ApiModelProperty( + value = "The target URIs of the remote process group." + + " If target uris is not set but target uri is set, then returns the single target uri." + + " If neither target uris nor target uri is set, then returns null.") + public String getTargetUris() { + + if (!isEmpty(targetUris)) { + return targetUris; + } + return !isEmpty(targetUri) ? targetUri : null; + + } + + private boolean isEmpty(final String value) { + return (value == null || value.isEmpty()); + } + + public void setTargetUris(String targetUris) { + this.targetUris = targetUris; + } + + @ApiModelProperty("The time period used for the timeout when communicating with the target.") + public String getCommunicationsTimeout() { + return communicationsTimeout; + } + + public void setCommunicationsTimeout(String communicationsTimeout) { + this.communicationsTimeout = communicationsTimeout; + } + + @ApiModelProperty("When yielding, this amount of time must elapse before the remote process group is scheduled again.") + public String getYieldDuration() { + return yieldDuration; + } + + public void setYieldDuration(String yieldDuration) { + this.yieldDuration = yieldDuration; + } + + @ApiModelProperty(value = "The Transport Protocol that is used for Site-to-Site communications", allowableValues = "RAW, HTTP") + public String getTransportProtocol() { + return transportProtocol; + } + + public void setTransportProtocol(String transportProtocol) { + this.transportProtocol = transportProtocol; + } + + @ApiModelProperty("A Set of Input Ports that can be connected to, in order to send data to the remote NiFi instance") + public Set getInputPorts() { + return inputPorts; + } + + public void setInputPorts(Set inputPorts) { + this.inputPorts = inputPorts; + } + + @ApiModelProperty("A Set of Output Ports that can be connected to, in order to pull data from the remote NiFi instance") + public Set getOutputPorts() { + return outputPorts; + } + + public void setOutputPorts(Set outputPorts) { + this.outputPorts = outputPorts; + } + + + @ApiModelProperty("The local network interface to send/receive data. If not specified, any local address is used. If clustered, all nodes must have an interface with this identifier.") + public String getLocalNetworkInterface() { + return localNetworkInterface; + } + + public void setLocalNetworkInterface(String localNetworkInterface) { + this.localNetworkInterface = localNetworkInterface; + } + + public String getProxyHost() { + return proxyHost; + } + + public void setProxyHost(String proxyHost) { + this.proxyHost = proxyHost; + } + + public Integer getProxyPort() { + return proxyPort; + } + + public void setProxyPort(Integer proxyPort) { + this.proxyPort = proxyPort; + } + + public String getProxyUser() { + return proxyUser; + } + + public void setProxyUser(String proxyUser) { + this.proxyUser = proxyUser; + } + + @Override + public ComponentType getComponentType() { + return ComponentType.REMOTE_PROCESS_GROUP; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/link/JaxbLink.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/link/JaxbLink.java new file mode 100644 index 0000000000..e36b7243e3 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/link/JaxbLink.java @@ -0,0 +1,139 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.link; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import javax.xml.bind.annotation.XmlAnyAttribute; +import javax.xml.bind.annotation.XmlAttribute; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; + +/** + * Copy of JAX-RS Link.JaxbLink so that Swagger annotations can be applied properly so that getUri() lines up with "href". + */ +@ApiModel +public class JaxbLink { + + private URI uri; + private Map params; + + /** + * Default constructor needed during unmarshalling. + */ + public JaxbLink() { + } + + /** + * Construct an instance from a URI and no parameters. + * + * @param uri underlying URI. + */ + public JaxbLink(URI uri) { + this.uri = uri; + } + + /** + * Construct an instance from a URI and some parameters. + * + * @param uri underlying URI. + * @param params parameters of this link. + */ + public JaxbLink(URI uri, Map params) { + this.uri = uri; + this.params = params; + } + + /** + * Get the underlying URI for this link. + * + * @return underlying URI. + */ + @XmlAttribute(name = "href") + @ApiModelProperty(name = "href", value = "The href for the link") + public URI getUri() { + return uri; + } + + /** + * Get the parameter map for this link. + * + * @return parameter map. + */ + @XmlAnyAttribute + @ApiModelProperty(name = "params", value = "The params for the link") + public Map getParams() { + if (params == null) { + params = new HashMap<>(); + } + return params; + } + + /** + * Set the underlying URI for this link. + * + * This setter is needed for JAXB unmarshalling. + */ + void setUri(URI uri) { + this.uri = uri; + } + + /** + * Set the parameter map for this link. + * + * This setter is needed for JAXB unmarshalling. + */ + void setParams(Map params) { + this.params = params; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof JaxbLink)) return false; + + JaxbLink jaxbLink = (JaxbLink) o; + + if (uri != null ? !uri.equals(jaxbLink.uri) : jaxbLink.uri != null) { + return false; + } + + if (params == jaxbLink.params) { + return true; + } + if (params == null) { + // if this.params is 'null', consider other.params equal to empty + return jaxbLink.params.isEmpty(); + } + if (jaxbLink.params == null) { + // if other.params is 'null', consider this.params equal to empty + return params.isEmpty(); + } + + return params.equals(jaxbLink.params); + } + + @Override + public int hashCode() { + int result = uri != null ? uri.hashCode() : 0; + result = 31 * result + (params != null && !params.isEmpty() ? params.hashCode() : 0); + return result; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/link/LinkAdapter.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/link/LinkAdapter.java new file mode 100644 index 0000000000..76bd708586 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/link/LinkAdapter.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.link; + +import javax.ws.rs.core.Link; +import javax.xml.bind.annotation.adapters.XmlAdapter; +import java.util.Map; + +/** + * This class is a modified version of Jersey's Link.JaxbAdapter that adds protection against nulls. + */ +public class LinkAdapter extends XmlAdapter { + + /** + * Convert a {@link JaxbLink} into a {@link Link}. + * + * @param v instance of type {@link JaxbLink}. + * @return mapped instance of type {@link Link.JaxbLink} + */ + @Override + public Link unmarshal(JaxbLink v) { + if (v == null) { + return null; + } + + Link.Builder lb = Link.fromUri(v.getUri()); + if (v.getParams() != null) { + for (Map.Entry e : v.getParams().entrySet()) { + lb.param(e.getKey(), e.getValue()); + } + } + return lb.build(); + } + + /** + * Convert a {@link Link} into a {@link JaxbLink}. + * + * @param v instance of type {@link Link}. + * @return mapped instance of type {@link JaxbLink}. + */ + @Override + public JaxbLink marshal(Link v) { + if (v == null) { + return null; + } + + final JaxbLink jl = new JaxbLink(v.getUri()); + if (v.getParams() != null) { + for (Map.Entry e : v.getParams().entrySet()) { + jl.getParams().put(e.getKey(), e.getValue()); + } + } + return jl; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/link/LinkableDocs.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/link/LinkableDocs.java new file mode 100644 index 0000000000..12d9dc439d --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/link/LinkableDocs.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.link; + +import javax.ws.rs.core.Link; + +/** + * An entity that has documentation that can be linked to. + */ +public interface LinkableDocs { + + /** + * @return the web link for the docs + */ + Link getLinkDocs(); + + /** + * @param link the web link for the docs + */ + void setLinkDocs(Link link); + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/link/LinkableEntity.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/link/LinkableEntity.java new file mode 100644 index 0000000000..3814e74395 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/link/LinkableEntity.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.link; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import javax.ws.rs.core.Link; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; + +/** + * Base classes for domain objects that want to provide a hypermedia link. + */ +@ApiModel +public abstract class LinkableEntity { + + private Link link; + + @XmlElement + @XmlJavaTypeAdapter(LinkAdapter.class) + @ApiModelProperty(value = "An WebLink to this entity.", + dataType = "org.apache.nifi.registry.link.JaxbLink", readOnly = true) + public Link getLink() { + return link; + } + + public void setLink(Link link) { + this.link = link; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/params/SortOrder.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/params/SortOrder.java new file mode 100644 index 0000000000..8e571de2b2 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/params/SortOrder.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.params; + +public enum SortOrder { + + ASC("asc"), + + DESC("desc"); + + private final String name; + + SortOrder(final String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public static SortOrder fromString(String order) { + if (ASC.getName().equals(order)) { + return ASC; + } + + if (DESC.getName().equals(order)) { + return DESC; + } + + throw new IllegalArgumentException("Unknown Sort Order: " + order); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/params/SortParameter.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/params/SortParameter.java new file mode 100644 index 0000000000..d4a1add59e --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/params/SortParameter.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.params; + +/** + * Sort parameter made up of a field and a sort order. + */ +public class SortParameter { + + public static final String API_PARAM_DESCRIPTION = + "Apply client-defined sorting to the resulting list of resource objects. " + + "The value of this parameter should be in the format \"field:order\". " + + "Valid values for 'field' can be discovered via GET :resourceURI/fields. " + + "Valid values for 'order' are 'ASC' (ascending order), 'DESC' (descending order)."; + + private final String fieldName; + + private final SortOrder order; + + public SortParameter(final String fieldName, final SortOrder order) { + this.fieldName = fieldName; + this.order = order; + + if (this.fieldName == null) { + throw new IllegalStateException("Field Name cannot be null"); + } + + if (this.fieldName.trim().isEmpty()) { + throw new IllegalStateException("Field Name cannot be blank"); + } + + if (this.order == null) { + throw new IllegalStateException("Order cannot be null"); + } + } + + public String getFieldName() { + return fieldName; + } + + public SortOrder getOrder() { + return order; + } + + /** + * Parses a sorting expression of the form field:order. + * + * @param sortExpression the expression + * @return the Sort instance + */ + public static SortParameter fromString(final String sortExpression) { + if (sortExpression == null) { + throw new IllegalArgumentException("Sort cannot be null"); + } + + final String[] sortParts = sortExpression.split("[:]"); + if (sortParts.length != 2) { + throw new IllegalArgumentException("Sort must be in the form field:order"); + } + + final String fieldName = sortParts[0]; + final SortOrder order = SortOrder.fromString(sortParts[1]); + + return new SortParameter(fieldName, order); + } + + @Override + public String toString() { + return fieldName + ":" + order.getName(); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/test/java/org/apache/nifi/registry/flow/TestVersionedRemoteProcessGroup.java b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/test/java/org/apache/nifi/registry/flow/TestVersionedRemoteProcessGroup.java new file mode 100644 index 0000000000..bbe6724561 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-data-model/src/test/java/org/apache/nifi/registry/flow/TestVersionedRemoteProcessGroup.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class TestVersionedRemoteProcessGroup { + + @Test + public void testGetTargetUriAndGetTargetUris() { + + VersionedRemoteProcessGroup vRPG = new VersionedRemoteProcessGroup(); + + + /* targetUri is null, targetUris varies */ + + vRPG.setTargetUri(null); + vRPG.setTargetUris(null); + assertEquals(null, vRPG.getTargetUri()); + assertEquals(null, vRPG.getTargetUris()); + + vRPG.setTargetUri(null); + vRPG.setTargetUris(""); + assertEquals(null, vRPG.getTargetUri()); + assertEquals(null, vRPG.getTargetUris()); + + vRPG.setTargetUri(null); + vRPG.setTargetUris("uri-2"); + //assertEquals("uri-2", vRPG.getTargetUri()); + assertEquals("uri-2", vRPG.getTargetUris()); + + vRPG.setTargetUri(null); + vRPG.setTargetUris("uri-2,uri-3"); + assertEquals("uri-2", vRPG.getTargetUri()); + assertEquals("uri-2,uri-3", vRPG.getTargetUris()); + + + /* targetUri is empty, targetUris varies */ + + vRPG.setTargetUri(""); + vRPG.setTargetUris(null); + assertEquals(null, vRPG.getTargetUri()); + assertEquals(null, vRPG.getTargetUris()); + + vRPG.setTargetUri(""); + vRPG.setTargetUris(""); + assertEquals(null, vRPG.getTargetUri()); + assertEquals(null, vRPG.getTargetUris()); + + vRPG.setTargetUri(""); + vRPG.setTargetUris("uri-2"); + assertEquals("uri-2", vRPG.getTargetUri()); + assertEquals("uri-2", vRPG.getTargetUris()); + + vRPG.setTargetUri(""); + vRPG.setTargetUris("uri-2,uri-3"); + assertEquals("uri-2", vRPG.getTargetUri()); + assertEquals("uri-2,uri-3", vRPG.getTargetUris()); + + + /* targetUri is set, targetUris varies */ + + vRPG.setTargetUri("uri-1"); + vRPG.setTargetUris(null); + assertEquals("uri-1", vRPG.getTargetUri()); + assertEquals("uri-1", vRPG.getTargetUris()); + + vRPG.setTargetUri("uri-1"); + vRPG.setTargetUris(""); + assertEquals("uri-1", vRPG.getTargetUri()); + assertEquals("uri-1", vRPG.getTargetUris()); + + vRPG.setTargetUri("uri-1"); + vRPG.setTargetUris("uri-2"); + assertEquals("uri-1", vRPG.getTargetUri()); + assertEquals("uri-2", vRPG.getTargetUris()); + + vRPG.setTargetUri("uri-1"); + vRPG.setTargetUris("uri-2,uri-3"); + assertEquals("uri-1", vRPG.getTargetUri()); + assertEquals("uri-2,uri-3", vRPG.getTargetUris()); + + } + + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/.dockerignore b/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/.dockerignore new file mode 100644 index 0000000000..30a2650305 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/.dockerignore @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +# Place files you want to exclude from the docker build here similar to .gitignore https://docs.docker.com/engine/reference/builder/#dockerignore-file +DockerBuild.sh +DockerRun.sh +DockerImage.txt \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/DockerBuild.sh b/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/DockerBuild.sh new file mode 100755 index 0000000000..c7e01e322c --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/DockerBuild.sh @@ -0,0 +1,36 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +#!/bin/bash + +DOCKER_UID=1000 +if [ -n "$1" ]; then + DOCKER_UID="$1" +fi + +DOCKER_GID=1000 +if [ -n "$2" ]; then + DOCKER_GID="$2" +fi + +MIRROR=https://archive.apache.org/dist +if [ -n "$3" ]; then + MIRROR="$3" +fi + +DOCKER_IMAGE="$(egrep -v '(^#|^\s*$|^\s*\t*#)' DockerImage.txt)" +NIFI_REGISTRY_IMAGE_VERSION="$(echo $DOCKER_IMAGE | cut -d : -f 2)" +echo "Building NiFi-Registry Image: '$DOCKER_IMAGE' Version: NIFI_REGISTRY_IMAGE_VERSION Mirror: $MIRROR" +docker build --build-arg UID="$DOCKER_UID" --build-arg GID="$DOCKER_GID" --build-arg NIFI_REGISTRY_VERSION="$NIFI_REGISTRY_IMAGE_VERSION" --build-arg MIRROR="$MIRROR" -t $DOCKER_IMAGE . diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/DockerImage.txt b/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/DockerImage.txt new file mode 100644 index 0000000000..33d85bbb30 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/DockerImage.txt @@ -0,0 +1,16 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +apache/nifi-registry:1.14.0 diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/Dockerfile b/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/Dockerfile new file mode 100644 index 0000000000..28040a0eed --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/Dockerfile @@ -0,0 +1,59 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# + +FROM openjdk:8-jdk-slim +LABEL maintainer="Apache NiFi " +LABEL site="https://nifi.apache.org" + +ARG UID=1000 +ARG GID=1000 +ARG NIFI_REGISTRY_VERSION=1.14.0 +ARG MIRROR=https://archive.apache.org/dist + +ENV NIFI_REGISTRY_BASE_DIR /opt/nifi-registry +ENV NIFI_REGISTRY_HOME=${NIFI_REGISTRY_BASE_DIR}/nifi-registry-current + +ENV NIFI_REGISTRY_BINARY_URL=nifi/nifi-registry/nifi-registry-${NIFI_REGISTRY_VERSION}/nifi-registry-${NIFI_REGISTRY_VERSION}-bin.tar.gz + +ADD sh/ ${NIFI_REGISTRY_BASE_DIR}/scripts/ + +# Setup NiFi-Registry user +RUN groupadd -g ${GID} nifi || groupmod -n nifi `getent group ${GID} | cut -d: -f1` \ + && useradd --shell /bin/bash -u ${UID} -g ${GID} -m nifi \ + && chown -R nifi:nifi ${NIFI_REGISTRY_BASE_DIR} \ + && apt-get update -y \ + && apt-get install -y curl jq xmlstarlet + +USER nifi + +# Download, validate, and expand Apache NiFi-Registry binary. +RUN curl -fSL ${MIRROR}/${NIFI_REGISTRY_BINARY_URL} -o ${NIFI_REGISTRY_BASE_DIR}/nifi-registry-${NIFI_REGISTRY_VERSION}-bin.tar.gz \ + && echo "$(curl ${MIRROR}/${NIFI_REGISTRY_BINARY_URL}.sha256) *${NIFI_REGISTRY_BASE_DIR}/nifi-registry-${NIFI_REGISTRY_VERSION}-bin.tar.gz" | sha256sum -c - \ + && tar -xvzf ${NIFI_REGISTRY_BASE_DIR}/nifi-registry-${NIFI_REGISTRY_VERSION}-bin.tar.gz -C ${NIFI_REGISTRY_BASE_DIR} \ + && rm ${NIFI_REGISTRY_BASE_DIR}/nifi-registry-${NIFI_REGISTRY_VERSION}-bin.tar.gz \ + && mv ${NIFI_REGISTRY_BASE_DIR}/nifi-registry-${NIFI_REGISTRY_VERSION} ${NIFI_REGISTRY_HOME} \ + && ln -s ${NIFI_REGISTRY_HOME} ${NIFI_REGISTRY_BASE_DIR}/nifi-registry-${NIFI_REGISTRY_VERSION} \ + && chown -R nifi:nifi ${NIFI_REGISTRY_HOME} + +# Web HTTP(s) ports +EXPOSE 18080 18443 + +WORKDIR ${NIFI_REGISTRY_HOME} + +# Apply configuration and start NiFi Registry +CMD ${NIFI_REGISTRY_BASE_DIR}/scripts/start.sh diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/README.md b/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/README.md new file mode 100644 index 0000000000..57882424f7 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/README.md @@ -0,0 +1,171 @@ + + +# Docker Image Quickstart + +## Capabilities +This image currently supports running in standalone mode either unsecured or with user authentication provided through: + * [Two-Way SSL with Client Certificates](https://nifi.apache.org/docs/nifi-registry-docs/html/administration-guide.html#security-configuration) + * [Lightweight Directory Access Protocol (LDAP)](https://nifi.apache.org/docs/nifi-registry-docs/html/administration-guide.html#ldap_identity_provider) + +## Building +The Docker image can be built using the following command: + + # user @ puter in ~/path/to/apache/nifi-registry/nifi-registry-docker/dockerhub + $ docker build -t apache/nifi-registry:latest . + +This will result in an image tagged apache/nifi-registry:latest + + $ docker images + > REPOSITORY TAG IMAGE ID CREATED SIZE + > apache/nifi-registry latest 751428cbf631 A long, long time ago 342MB + +**Note**: The default version of NiFi Registry specified by the Dockerfile is typically that of one that is unreleased if working from source. +To build an image for a prior released version, one can override the `NIFI_REGISTRY_VERSION` build-arg with the following command: + + $ docker build --build-arg NIFI_REGISTRY_VERSION={Desired NiFi Registry Version} -t apache/nifi-registry:latest . + +There is, however, no guarantee that older versions will work as properties have changed and evolved with subsequent releases. +The configuration scripts are suitable for at least 0.1.0+. + +## Running a container + +### Unsecured +The minimum to run a NiFi Registry instance is as follows: + + docker run --name nifi-registry \ + -p 18080:18080 \ + -d \ + apache/nifi-registry:latest + +This will provide a running instance, exposing the instance UI to the host system on at port 18080, +viewable at `http://localhost:18080/nifi-registry`. + +You can also pass in environment variables to change the NiFi Registry communication ports and hostname using the Docker '-e' switch as follows: + + docker run --name nifi-registry \ + -p 19090:19090 \ + -d \ + -e NIFI_REGISTRY_WEB_HTTP_PORT='19090' \ + apache/nifi-registry:latest + +For a list of the environment variables recognised in this build, look into the .sh/secure.sh and .sh/start.sh scripts + +### Secured with Two-Way TLS +In this configuration, the user will need to provide certificates and the associated configuration information. +Of particular note, is the `AUTH` environment variable which is set to `tls`. Additionally, the user must provide an +the DN as provided by an accessing client certificate in the `INITIAL_ADMIN_IDENTITY` environment variable. +This value will be used to seed the instance with an initial user with administrative privileges. +Finally, this command makes use of a volume to provide certificates on the host system to the container instance. + + docker run --name nifi-registry \ + -v /path/to/tls/certs/localhost:/opt/certs \ + -p 18443:18443 \ + -e AUTH=tls \ + -e KEYSTORE_PATH=/opt/certs/keystore.jks \ + -e KEYSTORE_TYPE=JKS \ + -e KEYSTORE_PASSWORD=QKZv1hSWAFQYZ+WU1jjF5ank+l4igeOfQRp+OSbkkrs \ + -e TRUSTSTORE_PATH=/opt/certs/truststore.jks \ + -e TRUSTSTORE_PASSWORD=rHkWR1gDNW3R9hgbeRsT3OM3Ue0zwGtQqcFKJD2EXWE \ + -e TRUSTSTORE_TYPE=JKS \ + -e INITIAL_ADMIN_IDENTITY='CN=AdminUser, OU=nifi' \ + -d \ + apache/nifi-registry:latest + +### Secured with LDAP +In this configuration, the user will need to provide certificates and the associated configuration information. Optionally, +if the LDAP provider of interest is operating in LDAPS or START_TLS modes, certificates will additionally be needed. +Of particular note, is the `AUTH` environment variable which is set to `ldap`. Additionally, the user must provide a +DN as provided by the configured LDAP server in the `INITIAL_ADMIN_IDENTITY` environment variable. This value will be +used to seed the instance with an initial user with administrative privileges. Finally, this command makes use of a +volume to provide certificates on the host system to the container instance. + +For a minimal, connection to an LDAP server using SIMPLE authentication: + + docker run --name nifi-registry \ + -v /path/to/tls/certs/localhost:/opt/certs \ + -p 18443:18443 \ + -e AUTH=ldap \ + -e KEYSTORE_PATH=/opt/certs/keystore.jks \ + -e KEYSTORE_TYPE=JKS \ + -e KEYSTORE_PASSWORD=QKZv1hSWAFQYZ+WU1jjF5ank+l4igeOfQRp+OSbkkrs \ + -e TRUSTSTORE_PATH=/opt/certs/truststore.jks \ + -e TRUSTSTORE_PASSWORD=rHkWR1gDNW3R9hgbeRsT3OM3Ue0zwGtQqcFKJD2EXWE \ + -e TRUSTSTORE_TYPE=JKS \ + -e INITIAL_ADMIN_IDENTITY='cn=nifi-admin,dc=example,dc=org' \ + -e LDAP_AUTHENTICATION_STRATEGY='SIMPLE' \ + -e LDAP_MANAGER_DN='cn=ldap-admin,dc=example,dc=org' \ + -e LDAP_MANAGER_PASSWORD='password' \ + -e LDAP_USER_SEARCH_BASE='dc=example,dc=org' \ + -e LDAP_USER_SEARCH_FILTER='cn={0}' \ + -e LDAP_IDENTITY_STRATEGY='USE_DN' \ + -e LDAP_URL='ldap://ldap:389' \ + -d \ + apache/nifi-registry:latest + +The following, optional environment variables may be added to the above command when connecting to a secure LDAP server configured with START_TLS or LDAPS + + -e LDAP_TLS_KEYSTORE: '' + -e LDAP_TLS_KEYSTORE_PASSWORD: '' + -e LDAP_TLS_KEYSTORE_TYPE: '' + -e LDAP_TLS_TRUSTSTORE: '' + -e LDAP_TLS_TRUSTSTORE_PASSWORD: '' + -e LDAP_TLS_TRUSTSTORE_TYPE: '' + +### Additional Configuration Options + +#### Database Configuration + +The following, optional environment variables can be used to configure the database. + +| nifi-registry.properties entry | Variable | +|----------------------------------------|----------------------------| +| nifi.registry.db.url | NIFI_REGISTRY_DB_URL | +| nifi.registry.db.driver.class | NIFI_REGISTRY_DB_CLASS | +| nifi.registry.db.driver.directory | NIFI_REGISTRY_DB_DIR | +| nifi.registry.db.username | NIFI_REGISTRY_DB_USER | +| nifi.registry.db.password | NIFI_REGISTRY_DB_PASS | +| nifi.registry.db.maxConnections | NIFI_REGISTRY_DB_MAX_CONNS | +| nifi.registry.db.sql.debug | NIFI_REGISTRY_DB_DEBUG_SQL | + +#### Flow Persistence Configuration + +The following, optional environment variables may be added to configure flow persistence provider. + +| Environment Variable | Configuration Property | +|--------------------------------|--------------------------------------| +| NIFI_REGISTRY_FLOW_STORAGE_DIR | Flow Storage Directory | +| NIFI_REGISTRY_FLOW_PROVIDER | (Class tag); valid values: git, file | +| NIFI_REGISTRY_GIT_REMOTE | Remote to Push | +| NIFI_REGISTRY_GIT_USER | Remote Access User | +| NIFI_REGISTRY_GIT_PASSWORD | Remote Access Password | +| NIFI_REGISTRY_GIT_REPO | Remote Clone Repository | + +#### Extension Bundle Persistence Configuration + +The following, optional environment variables may be added to configure extension bundle persistence provider. + +| Environment Variable | Configuration Property | +|---------------------------------------|-------------------------------------| +| NIFI_REGISTRY_BUNDLE_STORAGE_DIR | Extension Bundle Storage Directory | +| NIFI_REGISTRY_BUNDLE_PROVIDER | (Class tag); valid values: file, s3 | +| NIFI_REGISTRY_S3_REGION | Region | +| NIFI_REGISTRY_S3_BUCKET_NAME | Bucket Name | +| NIFI_REGISTRY_S3_KEY_PREFIX | Key Prefix | +| NIFI_REGISTRY_S3_CREDENTIALS_PROVIDER | Credentials Provider | +| NIFI_REGISTRY_S3_ACCESS_KEY | Access Key | +| NIFI_REGISTRY_S3_SECRET_ACCESS_KEY | Secret Access Key | +| NIFI_REGISTRY_S3_ENDPOINT_URL | Endpoint URL | + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/sh/common.sh b/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/sh/common.sh new file mode 100755 index 0000000000..0f594d9aed --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/sh/common.sh @@ -0,0 +1,28 @@ +#!/bin/sh -e +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +# 1 - value to search for +# 2 - value to replace +# 3 - file to perform replacement inline +prop_replace () { + target_file=${3:-${nifi_registry_props_file}} + echo 'replacing target file ' ${target_file} + sed -i -e "s|^$1=.*$|$1=$2|" ${target_file} +} + +# NIFI_REGISTRY_HOME is defined by an ENV command in the backing Dockerfile +export nifi_registry_props_file=${NIFI_REGISTRY_HOME}/conf/nifi-registry.properties +export hostname=$(hostname) diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/sh/secure.sh b/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/sh/secure.sh new file mode 100644 index 0000000000..8a7a5bbed5 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/sh/secure.sh @@ -0,0 +1,57 @@ +#!/bin/sh -e + +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +scripts_dir='/opt/nifi-registry/scripts' + +[ -f "${scripts_dir}/common.sh" ] && . "${scripts_dir}/common.sh" + +# Perform idempotent changes of configuration to support secure environments +echo 'Configuring environment with SSL settings' + +: ${KEYSTORE_PATH:?"Must specify an absolute path to the keystore being used."} +if [ ! -f "${KEYSTORE_PATH}" ]; then + echo "Keystore file specified (${KEYSTORE_PATH}) does not exist." + exit 1 +fi +: ${KEYSTORE_TYPE:?"Must specify the type of keystore (JKS, PKCS12, PEM) of the keystore being used."} +: ${KEYSTORE_PASSWORD:?"Must specify the password of the keystore being used."} + +: ${TRUSTSTORE_PATH:?"Must specify an absolute path to the truststore being used."} +if [ ! -f "${TRUSTSTORE_PATH}" ]; then + echo "Keystore file specified (${TRUSTSTORE_PATH}) does not exist." + exit 1 +fi +: ${TRUSTSTORE_TYPE:?"Must specify the type of truststore (JKS, PKCS12, PEM) of the truststore being used."} +: ${TRUSTSTORE_PASSWORD:?"Must specify the password of the truststore being used."} + +prop_replace 'nifi.registry.security.keystore' "${KEYSTORE_PATH}" +prop_replace 'nifi.registry.security.keystoreType' "${KEYSTORE_TYPE}" +prop_replace 'nifi.registry.security.keystorePasswd' "${KEYSTORE_PASSWORD}" +prop_replace 'nifi.registry.security.keyPasswd' "${KEY_PASSWORD:-$KEYSTORE_PASSWORD}" +prop_replace 'nifi.registry.security.truststore' "${TRUSTSTORE_PATH}" +prop_replace 'nifi.registry.security.truststoreType' "${TRUSTSTORE_TYPE}" +prop_replace 'nifi.registry.security.truststorePasswd' "${TRUSTSTORE_PASSWORD}" + +# Disable HTTP and enable HTTPS +prop_replace 'nifi.registry.web.http.port' '' +prop_replace 'nifi.registry.web.http.host' '' +prop_replace 'nifi.registry.web.https.port' "${NIFI_REGISTRY_WEB_HTTPS_PORT:-18443}" +prop_replace 'nifi.registry.web.https.host' "${NIFI_REGISTRY_WEB_HTTPS_HOST:-$HOSTNAME}" + +# Establish initial user and an associated admin identity +sed -i -e 's|.*|'"${INITIAL_ADMIN_IDENTITY}"'|' ${NIFI_REGISTRY_HOME}/conf/authorizers.xml +sed -i -e 's|.*|'"${INITIAL_ADMIN_IDENTITY}"'|' ${NIFI_REGISTRY_HOME}/conf/authorizers.xml diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/sh/start.sh b/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/sh/start.sh new file mode 100755 index 0000000000..c65f3ea926 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/sh/start.sh @@ -0,0 +1,56 @@ +#!/bin/sh -e + +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +scripts_dir='/opt/nifi-registry/scripts' + +[ -f "${scripts_dir}/common.sh" ] && . "${scripts_dir}/common.sh" + +# Establish baseline properties +prop_replace 'nifi.registry.web.http.port' "${NIFI_REGISTRY_WEB_HTTP_PORT:-18080}" +prop_replace 'nifi.registry.web.http.host' "${NIFI_REGISTRY_WEB_HTTP_HOST:-$HOSTNAME}" + +. ${scripts_dir}/update_database.sh + +# Check if we are secured or unsecured +case ${AUTH} in + tls) + echo 'Enabling Two-Way SSL user authentication' + . "${scripts_dir}/secure.sh" + ;; + ldap) + echo 'Enabling LDAP user authentication' + # Reference ldap-provider in properties + prop_replace 'nifi.registry.security.identity.provider' 'ldap-identity-provider' + prop_replace 'nifi.registry.security.needClientAuth' 'false' + + . "${scripts_dir}/secure.sh" + . "${scripts_dir}/update_login_providers.sh" + ;; +esac + +. "${scripts_dir}/update_flow_provider.sh" +. "${scripts_dir}/update_bundle_provider.sh" + +# Continuously provide logs so that 'docker logs' can produce them +tail -F "${NIFI_REGISTRY_HOME}/logs/nifi-registry-app.log" & +"${NIFI_REGISTRY_HOME}/bin/nifi-registry.sh" run & +nifi_registry_pid="$!" + +trap "echo Received trapped signal, beginning shutdown...;" KILL TERM HUP INT EXIT; + +echo NiFi-Registry running with PID ${nifi_registry_pid}. +wait ${nifi_registry_pid} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/sh/update_bundle_provider.sh b/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/sh/update_bundle_provider.sh new file mode 100644 index 0000000000..27d5c940ac --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/sh/update_bundle_provider.sh @@ -0,0 +1,48 @@ +#!/bin/sh -e + +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +providers_file=${NIFI_REGISTRY_HOME}/conf/providers.xml +property_xpath='/providers/extensionBundlePersistenceProvider' + +add_property() { + property_name=$1 + property_value=$2 + + if [ -n "${property_value}" ]; then + xmlstarlet ed --inplace --subnode "${property_xpath}" --type elem -n property -v "${property_value}" \ + -i \$prev --type attr -n name -v "${property_name}" \ + "${providers_file}" + fi +} + +xmlstarlet ed --inplace -u "${property_xpath}/property[@name='Extension Bundle Storage Directory']" -v "${NIFI_REGISTRY_BUNDLE_STORAGE_DIR:-./extension_bundles}" "${providers_file}" + +case ${NIFI_REGISTRY_BUNDLE_PROVIDER} in + file) + xmlstarlet ed --inplace -u "${property_xpath}/class" -v "org.apache.nifi.registry.provider.extension.FileSystemBundlePersistenceProvider" "${providers_file}" + ;; + s3) + xmlstarlet ed --inplace -u "${property_xpath}/class" -v "org.apache.nifi.registry.aws.S3BundlePersistenceProvider" "${providers_file}" + add_property "Region" "${NIFI_REGISTRY_S3_REGION:-}" + add_property "Bucket Name" "${NIFI_REGISTRY_S3_BUCKET_NAME:-}" + add_property "Key Prefix" "${NIFI_REGISTRY_S3_KEY_PREFIX:-}" + add_property "Credentials Provider" "${NIFI_REGISTRY_S3_CREDENTIALS_PROVIDER:-DEFAULT_CHAIN}" + add_property "Access Key" "${NIFI_REGISTRY_S3_ACCESS_KEY:-}" + add_property "Secret Access Key" "${NIFI_REGISTRY_S3_SECRET_ACCESS_KEY:-}" + add_property "Endpoint URL" "${NIFI_REGISTRY_S3_ENDPOINT_URL:-}" + ;; +esac diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/sh/update_database.sh b/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/sh/update_database.sh new file mode 100644 index 0000000000..59d94d7b39 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/sh/update_database.sh @@ -0,0 +1,24 @@ +#!/bin/sh -e + +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +prop_replace 'nifi.registry.db.url' "${NIFI_REGISTRY_DB_URL:-jdbc:h2:./database/nifi-registry-primary;AUTOCOMMIT=OFF;DB_CLOSE_ON_EXIT=FALSE;LOCK_MODE=3;LOCK_TIMEOUT=25000;WRITE_DELAY=0;AUTO_SERVER=FALSE}" +prop_replace 'nifi.registry.db.driver.class' "${NIFI_REGISTRY_DB_CLASS:-org.h2.Driver}" +prop_replace 'nifi.registry.db.driver.directory' "${NIFI_REGISTRY_DB_DIR:-}" +prop_replace 'nifi.registry.db.username' "${NIFI_REGISTRY_DB_USER:-nifireg}" +prop_replace 'nifi.registry.db.password' "${NIFI_REGISTRY_DB_PASS:-nifireg}" +prop_replace 'nifi.registry.db.maxConnections' "${NIFI_REGISTRY_DB_MAX_CONNS:-5}" +prop_replace 'nifi.registry.db.sql.debug' "${NIFI_REGISTRY_DB_DEBUG_SQL:-false}" diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/sh/update_flow_provider.sh b/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/sh/update_flow_provider.sh new file mode 100644 index 0000000000..92a921422d --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/sh/update_flow_provider.sh @@ -0,0 +1,48 @@ +#!/bin/sh -e + +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +providers_file=${NIFI_REGISTRY_HOME}/conf/providers.xml +property_xpath='/providers/flowPersistenceProvider' + +add_property() { + property_name=$1 + property_value=$2 + + if [ -n "${property_value}" ]; then + xmlstarlet ed --inplace --subnode "${property_xpath}" --type elem -n property -v "${property_value}" \ + -i \$prev --type attr -n name -v "${property_name}" \ + "${providers_file}" + fi +} + +xmlstarlet ed --inplace -u "${property_xpath}/property[@name='Flow Storage Directory']" -v "${NIFI_REGISTRY_FLOW_STORAGE_DIR:-./flow_storage}" "${providers_file}" + +case ${NIFI_REGISTRY_FLOW_PROVIDER} in + file) + xmlstarlet ed --inplace -u "${property_xpath}/class" -v "org.apache.nifi.registry.provider.flow.FileSystemFlowPersistenceProvider" "${providers_file}" + ;; + git) + xmlstarlet ed --inplace -u "${property_xpath}/class" -v "org.apache.nifi.registry.provider.flow.git.GitFlowPersistenceProvider" "${providers_file}" + add_property "Remote To Push" "${NIFI_REGISTRY_GIT_REMOTE:-}" + add_property "Remote Access User" "${NIFI_REGISTRY_GIT_USER:-}" + add_property "Remote Access Password" "${NIFI_REGISTRY_GIT_PASSWORD:-}" + + if [ ! -z "$NIFI_REGISTRY_GIT_REPO" ]; then + add_property "Remote Clone Repository" "${NIFI_REGISTRY_GIT_REPO:-}" + fi + ;; +esac diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/sh/update_login_providers.sh b/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/sh/update_login_providers.sh new file mode 100755 index 0000000000..e3280b56aa --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-docker/dockerhub/sh/update_login_providers.sh @@ -0,0 +1,47 @@ +#!/bin/sh -e + +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +login_providers_file=${NIFI_REGISTRY_HOME}/conf/identity-providers.xml +property_xpath='//identityProviders/provider/property' + +# Update a given property in the login-identity-providers file if a value is specified +edit_property() { + property_name=$1 + property_value=$2 + + if [ -n "${property_value}" ]; then + xmlstarlet ed --inplace -u "${property_xpath}[@name='${property_name}']" -v "${property_value}" "${login_providers_file}" + fi +} + +# Remove comments to enable the ldap-provider +sed -i '/To enable the ldap-identity-provider remove/d' "${login_providers_file}" + +edit_property 'Authentication Strategy' "${LDAP_AUTHENTICATION_STRATEGY}" +edit_property 'Manager DN' "${LDAP_MANAGER_DN}" +edit_property 'Manager Password' "${LDAP_MANAGER_PASSWORD}" +edit_property 'TLS - Keystore' "${LDAP_TLS_KEYSTORE}" +edit_property 'TLS - Keystore Password' "${LDAP_TLS_KEYSTORE_PASSWORD}" +edit_property 'TLS - Keystore Type' "${LDAP_TLS_KEYSTORE_TYPE}" +edit_property 'TLS - Truststore' "${LDAP_TLS_TRUSTSTORE}" +edit_property 'TLS - Truststore Password' "${LDAP_TLS_TRUSTSTORE_PASSWORD}" +edit_property 'TLS - Truststore Type' "${LDAP_TLS_TRUSTSTORE_TYPE}" +edit_property 'TLS - Protocol' "${LDAP_TLS_PROTOCOL}" +edit_property 'Url' "${LDAP_URL}" +edit_property 'User Search Base' "${LDAP_USER_SEARCH_BASE}" +edit_property 'User Search Filter' "${LDAP_USER_SEARCH_FILTER}" +edit_property 'Identity Strategy' "${LDAP_IDENTITY_STRATEGY}" diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docker/pom.xml b/nifi-registry/nifi-registry-core/nifi-registry-docker/pom.xml new file mode 100644 index 0000000000..662bbd084f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-docker/pom.xml @@ -0,0 +1,27 @@ + + + + + nifi-registry-core + org.apache.nifi.registry + 1.14.0-SNAPSHOT + + 4.0.0 + + nifi-registry-docker + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/LICENSE b/nifi-registry/nifi-registry-core/nifi-registry-docs/LICENSE new file mode 100644 index 0000000000..675d86585e --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-docs/LICENSE @@ -0,0 +1,235 @@ + + Apache License + Version 2.0, January 2004 + https://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: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) 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 + + (d) 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 + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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 + + https://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. + +APACHE NIFI SUBCOMPONENTS: + +The Apache NiFi project contains subcomponents with separate copyright +notices and license terms. Your use of the source code for the these +subcomponents is subject to the terms and conditions of the following +licenses. + +This product bundles source from 'Asciidoctor'. Specifically the 'asciidoc-mod.css'. +The source is available under an MIT LICENSE. + + Copyright (C) 2012-2015 Dan Allen, Ryan Waldron and the Asciidoctor Project + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/NOTICE b/nifi-registry/nifi-registry-core/nifi-registry-docs/NOTICE new file mode 100644 index 0000000000..060bd5e11f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-docs/NOTICE @@ -0,0 +1,5 @@ +nifi-registry-docs +Copyright 2014-2017 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (https://www.apache.org/). diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/pom.xml b/nifi-registry/nifi-registry-core/nifi-registry-docs/pom.xml new file mode 100644 index 0000000000..ef853d64d6 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-docs/pom.xml @@ -0,0 +1,215 @@ + + + + 4.0.0 + + org.apache.nifi.registry + nifi-registry-core + 1.14.0-SNAPSHOT + + pom + nifi-registry-docs + + + + org.apache.nifi.registry + nifi-registry-web-api + 1.14.0-SNAPSHOT + war + + + + + + + org.apache.maven.plugins + maven-resources-plugin + + + copy-asciidoc + generate-resources + + copy-resources + + + + + src/main/asciidoc + + + ${project.build.directory}/asciidoc + + + + + + org.asciidoctor + asciidoctor-maven-plugin + 1.5.2 + + + output-html + prepare-package + + process-asciidoc + + + + + ${project.build.directory}/asciidoc + ${project.build.directory}/generated-docs + html5 + + ./images + font + true + ${project.version} + true + + - + true + asciidoc-mod.css + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.1.1 + + + unpack-rest-api-doc + compile + + unpack-dependencies + + + org.apache.nifi.registry + nifi-registry-web-api + ${project.build.directory}/nifi-registry-web-api/ + **/rest-api.html + false + false + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.8 + + + copy-rest-api-doc + compile + + run + + + + + Copy unpacked rest-api.html to generated-docs dir + + + + + + + + + + + + + + com.google.code.maven-replacer-plugin + replacer + 1.5.3 + + + package + + replace + + + + + ${project.build.directory}/generated-docs/**.html + true + + DOTALL + MULTILINE + + ^(.*)$ + +<!-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You 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. + --> + $1 + + + + + org.apache.rat + apache-rat-plugin + + + + src/main/asciidoc/asciidoc-mod.css + + + + + maven-assembly-plugin + + true + + + + make shared resource + + single + + package + + + src/main/assembly/dependencies.xml + + + + + + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/administration-guide.adoc b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/administration-guide.adoc new file mode 100644 index 0000000000..5e960d97ef --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/administration-guide.adoc @@ -0,0 +1,1654 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You 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. +// += Apache NiFi Registry System Administrator's Guide +Apache NiFi Team +:homepage: https://nifi.apache.org +:linkattrs: + +== System Requirements + +NiFi Registry has the following minimum system requirements: + +* Requires Java Development Kit (JDK) 8, newer than 1.8.0_45 + +WARNING: When running Registry with only a JRE you may encounter the following error as Flyway (database migration tool) attempts to utilize a resource from the JDK: + + + + `java.lang.RuntimeException: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'flywayInitializer' defined in class path resource [org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration$FlywayConfiguration.class]: Invocation of init method failed; nested exception is org.flywaydb.core.api.FlywayException: Validate failed: Detected failed migration to version 1.3 (DropBucketItemNameUniqueness)` + +* Supported Operating Systems: +** Linux +** Unix +** Mac OS X +* Supported Web Browsers: +** Google Chrome: Current & (Current - 1) +** Mozilla FireFox: Current & (Current - 1) +** Safari: Current & (Current - 1) + + + +== How to install and start NiFi Registry + +* Linux/Unix/OS X +** Decompress and untar into desired installation directory +** Make any desired edits in files found under `/conf` +** From the `/bin` directory, execute the following commands by typing `./nifi-registry.sh `: +*** `start`: starts NiFi Registry in the background +*** `stop`: stops NiFi Registry that is running in the background +*** `status`: provides the current status of NiFi Registry +*** `run`: runs NiFi Registry in the foreground and waits for a Ctrl-C to initiate shutdown of NiFi Registry +*** `install`: installs NiFi Registry as a service that can then be controlled via +**** `service nifi-registry start` +**** `service nifi-registry stop` +**** `service nifi-registry status` + + +When NiFi Registry first starts up, the following directories are created: + +* `flow_storage` +* `database` +* `work` +* `logs` +* `run` + +See the <> section of this guide for more information about NiFi Registry configuration files. + +== Recommended Antivirus Exclusions +Antivirus software can take a long time to scan large directories and the numerous files within them. Additionally, if the antivirus software locks files or directories during a scan, those resources are unavailable to NiFi Registry processes, causing latency or unavailability of these resources in a NiFi Registry instance. To prevent these performance and reliability issues from occurring, it is highly recommended to configure your antivirus software to skip scans on the following NiFi Registry directories: + +* `database` +* `extension_bundles` +* `flow_storage` +* `logs` + +NOTE: The directories listed are generated at startup for a default NiFi Registry installation. Consider your configuration when determining directories to exclude during antivirus scans. For example, if an external database has been setup or if a different flow storage directory is specified in your configuration. + +[[security_configuration]] +== Security Configuration + +NiFi Registry provides several different configuration options for security purposes. The most important properties are those under the +"security properties" heading in the _nifi-registry.properties_ file. In order to run securely, the following properties must be set: + +[options="header,footer"] +|================================================================================================================================================== +| Property Name | Description +|`nifi.registry.security.keystore` | Filename of the Keystore that contains the server's private key. +|`nifi.registry.security.keystoreType` | The type of Keystore. Must be `PKCS12` or `JKS` or `BCFKS`. JKS is the preferred type, BCFKS and PKCS12 files will be loaded with BouncyCastle provider. +|`nifi.registry.security.keystorePasswd` | The password for the Keystore. +|`nifi.registry.security.keyPasswd` | The password for the certificate in the Keystore. If not set, the value of `nifi.registry.security.keystorePasswd` will be used. +|`nifi.registry.security.truststore` | Filename of the Truststore that will be used to authorize those connecting to NiFi Registry. A secured instance with no Truststore will refuse all incoming connections. +|`nifi.registry.security.truststoreType` | The type of the Truststore. Must be `PKCS12` or `JKS` or `BCFKS`. JKS is the preferred type, BCFKS and PKCS12 files will be loaded with BouncyCastle provider. +|`nifi.registry.security.truststorePasswd` | The password for the Truststore. +|`nifi.registry.security.needClientAuth` | This specifies that connecting clients must authenticate with a client cert. Setting this to `false` will specify that connecting clients may optionally authenticate with a client cert, but may also login with a username and password against a configured identity provider. The default value is `true`. +|================================================================================================================================================== + +Once the above properties have been configured, we can enable the User Interface to be accessed over HTTPS instead of HTTP. This is accomplished +by setting the `nifi.registry.web.https.host` and `nifi.registry.web.https.port` properties. The `nifi.registry.web.https.host` property indicates which hostname the server +should run on. If it is desired that the HTTPS interface be accessible from all network interfaces, a value of `0.0.0.0` should be used for `nifi.registry.web.https.host`. + +NOTE: It is important when enabling HTTPS that the `nifi.registry.web.http.port` property be unset. + +[[user_authentication]] +== User Authentication + +A secured instance of NiFi Registry cannot be accessed anonymously, so a method of user authentication must be configured. + +NOTE: NiFi Registry does not perform user authentication over HTTP. Using HTTP, all users will have full permissions. + +Any secured instance of NiFi Registry supports authentication via client certificates that are trusted by the NiFi Registry's SSL Context Truststore. +Alternatively, a secured NiFi Registry can be configured to authenticate users via username/password. + +Username/password authentication is performed by an 'Identity Provider'. The Identity Provider is a pluggable mechanism for +authenticating users via their username/password. Which Identity Provider to use is configured in the _nifi-registry.properties_ file. +Currently NiFi Registry offers Identity Providers for LDAP and Kerberos. + +Identity Providers are configured using two properties in the _nifi-registry.properties_ file: + +* The `nifi.registry.security.identity.providers.configuration.file` property specifies the configuration file where identity providers are defined. By default, the _identity-providers.xml_ file located in the root installation `conf` directory is selected. +* The `nifi.registry.security.identity.provider` property indicates which of the configured identity providers in the _identity-providers.xml_ file to use. By default, this property is not configured meaning that username/password must be explicitly enabled. + +NOTE: NiFi Registry can only be configured to use one Identity Provider at a given time. + +[[ldap_identity_provider]] +=== Lightweight Directory Access Protocol (LDAP) + +Below is an example and description of configuring a Identity Provider that integrates with a Directory Server to authenticate users. + +Set the following in _nifi-registry.properties_ to enable LDAP username/password authentication: + +---- +nifi.registry.security.identity.provider=ldap-identity-provider +---- + +Modify _identity-providers.xml_ to enable the `ldap-identity-provider`. Here is the sample provided in the file: + +---- + + ldap-identity-provider + org.apache.nifi.registry.security.ldap.LdapIdentityProvider + START_TLS + + + + + + + + + + + + + + + FOLLOW + 10 secs + 10 secs + + + + + + USE_DN + 12 hours + +---- + +The `ldap-identity-provider` has the following properties: + +[options="header,footer"] +|================================================================================================================================================== +| Property Name | Description +|`Authentication Strategy` | How the connection to the LDAP server is authenticated. Possible values are `ANONYMOUS`, `SIMPLE`, `LDAPS`, or `START_TLS`. +|`Manager DN` | The DN of the manager that is used to bind to the LDAP server to search for users. +|`Manager Password` | The password of the manager that is used to bind to the LDAP server to search for users. +|`TLS - Keystore` | Path to the Keystore that is used when connecting to LDAP using LDAPS or START_TLS. +|`TLS - Keystore Password` | Password for the Keystore that is used when connecting to LDAP using LDAPS or START_TLS. +|`TLS - Keystore Type` | Type of the Keystore that is used when connecting to LDAP using LDAPS or START_TLS (i.e. `JKS` or `PKCS12`). +|`TLS - Truststore` | Path to the Truststore that is used when connecting to LDAP using LDAPS or START_TLS. +|`TLS - Truststore Password` | Password for the Truststore that is used when connecting to LDAP using LDAPS or START_TLS. +|`TLS - Truststore Type` | Type of the Truststore that is used when connecting to LDAP using LDAPS or START_TLS (i.e. `JKS` or `PKCS12`). +|`TLS - Client Auth` | Client authentication policy when connecting to LDAP using LDAPS or START_TLS. Possible values are `REQUIRED`, `WANT`, `NONE`. +|`TLS - Protocol` | Protocol to use when connecting to LDAP using LDAPS or START_TLS. (i.e. `TLS`, `TLSv1.1`, `TLSv1.2`, etc). +|`TLS - Shutdown Gracefully` | Specifies whether the TLS should be shut down gracefully before the target context is closed. Defaults to `false`. +|`Referral Strategy` | Strategy for handling referrals. Possible values are `FOLLOW`, `IGNORE`, `THROW`. +|`Connect Timeout` | Duration of connect timeout. (i.e. `10 secs`). +|`Read Timeout` | Duration of read timeout. (i.e. `10 secs`). +|`Url` | Space-separated list of URLs of the LDAP servers (i.e. `ldap://:`). +|`User Search Base` | Base DN for searching for users (i.e. `CN=Users,DC=example,DC=com`). +|`User Search Filter` | Filter for searching for users against the `User Search Base`. (i.e. `sAMAccountName={0}`). The user specified name is inserted into '{0}'. +|`Identity Strategy` | Strategy to identify users. Possible values are `USE_DN` and `USE_USERNAME`. The default functionality if this property is missing is `USE_DN` in order to retain backward +compatibility. `USE_DN` will use the full DN of the user entry if possible. `USE_USERNAME` will use the username the user logged in with. +|`Authentication Expiration` | The duration of how long the user authentication is valid for. If the user never logs out, they will be required to log back in following this duration. +|================================================================================================================================================== + +[[kerberos_identity_provider]] +=== Kerberos + +Below is an example and description of configuring an Identity Provider that integrates with a Kerberos Key Distribution Center (KDC) to authenticate users. + +Set the following in _nifi-registry.properties_ to enable Kerberos username/password authentication: + +---- +nifi.registry.security.user.identity.provider=kerberos-identity-provider +---- + +Modify _identity-providers.xml_ to enable the `kerberos-identity-provider`. Here is the sample provided in the file: + +---- + + kerberos-identity-provider + org.apache.nifi.registry.web.security.authentication.kerberos.KerberosIdentityProvider + NIFI.APACHE.ORG + 12 hours + false + +---- + +The `kerberos-identity-provider` has the following properties: + +[options="header,footer"] +|================================================================================================================================================== +| Property Name | Description +|`Default Realm` | Default realm to provide when user enters incomplete user principal (i.e. `NIFI.APACHE.ORG`). +|`Authentication Expiration`| The duration for which the user authentication is valid. If the user never logs out, they will be required to log back in following this duration. +|`Enable Debug`| Enables debug logging output for the SunJaasKerberosClient used internally by the KerberosIdentityProvider. By default, this is set to `false`. +|================================================================================================================================================== + +See also <> to allow single sign-on access via client Kerberos tickets. + +[[authorization]] +== Authorization + +After you have configured NiFi Registry to run securely and with an authentication mechanism, you must configure who has access to the system and their level of access. +This is done by defining policies that give users and groups permissions to perform a particular action. These policies are defined in an 'authorizer'. + +[[authorizer-configuration]] +=== Authorizer Configuration + +An 'authorizer' manages known users and their access policies. Authorizers are configured using two properties in the _nifi-registry.properties_ file: + +* The `nifi.registry.security.authorizers.configuration.file` property specifies the configuration file where authorizers are defined. By default, the _authorizers.xml_ file located in the root installation conf directory is selected. +* The `nifi.registry.security.authorizer` property indicates which of the configured authorizers in the _authorizers.xml_ file to use. + +[[authorizers-setup]] +=== Authorizers.xml Setup + +The _authorizers.xml_ file is used to define and configure available authorizers. + +==== StandardManagedAuthorizer +The default Authorizer is the StandardManagedAuthorizer, however, you can develop additional Authorizers as extensions. The StandardManagedAuthorizer has the following properties: + +[options="header,footer"] +|================================================================================================================================================== +| Property Name | Description +|`Access Policy Provider` | The identifier for an Access Policy Provider defined above. +|================================================================================================================================================== + +The managed authorizer is comprised of a UserGroupProvider and a AccessPolicyProvider. The users, group, and access policies will be loaded and optionally configured through these providers. The managed authorizer will make all access decisions based on these provided users, groups, and access policies. + +During startup there is a check to ensure that there are no two users/groups with the same identity/name. This check is executed regardless of the configured implementation. This is necessary because this is how users/groups are identified and authorized during access decisions. + +==== UserGroupProvider + +===== FileUserGroupProvider + +The default UserGroupProvider is the FileUserGroupProvider, however, you can develop additional UserGroupProviders as extensions. The FileUserGroupProvider has the following properties: + +[options="header,footer"] +|================================================================================================================================================== +| Property Name | Description +|`Users File` | The file where the FileUserGroupProvider stores users and groups. + By default, _users.xml_ in the `conf` directory is chosen. +|`Initial User Identity`| The identity of a user or system to seed an empty Users File. + Multiple Initial User Identity properties can be specified, but the name of each property must be unique, for example: ``"Initial User Identity A"``, ``"Initial User Identity B"``, ``"Initial User Identity C"`` or ``"Initial User Identity 1"``, ``"Initial User Identity 2"``, ``"Initial User Identity 3"``. +|================================================================================================================================================== + +NOTE: Initial User Identities are only created if the specified Users File is missing or empty during NiFi Registry startup. Changes to the configured Initial Users Identities will not take effect if the Users File is populated. + +===== LdapUserGroupProvider + +Another option for the UserGroupProvider is the LdapUserGroupProvider. By default, this option is commented out but can be configured in lieu of the FileUserGroupProvider. +This will sync users and groups from a directory server and will present them in NiFi Registry UI in read only form. The LdapUserGroupProvider has the following properties: + +[options="header,footer"] +|================================================================================================================================================== +| Property Name | Description +|`Authentication Strategy` | How the connection to the LDAP server is authenticated. Possible values are `ANONYMOUS`, `SIMPLE`, `LDAPS`, or `START_TLS`. +|`Manager DN`| The DN of the manager that is used to bind to the LDAP server to search for users. +|`Manager Password`| The password of the manager that is used to bind to the LDAP server to search for users. +|`TLS - Keystore` | Path to the Keystore that is used when connecting to LDAP using LDAPS or START_TLS. +|`TLS - Keystore Password`| Password for the Keystore that is used when connecting to LDAP using LDAPS or START_TLS. +|`TLS - Keystore Type`| Type of the Keystore that is used when connecting to LDAP using LDAPS or START_TLS (i.e. `JKS` or `PKCS12`). +|`TLS - Truststore` | Path to the Truststore that is used when connecting to LDAP using LDAPS or START_TLS. +|`TLS - Truststore Password`| Password for the Truststore that is used when connecting to LDAP using LDAPS or START_TLS. +|`TLS - Truststore Type`| Type of the Truststore that is used when connecting to LDAP using LDAPS or START_TLS (i.e. `JKS` or `PKCS12`). +|`TLS - Client Auth`| Client authentication policy when connecting to LDAP using LDAPS or START_TLS. Possible values are `REQUIRED`, `WANT`, `NONE`. +|`TLS - Protocol`| Protocol to use when connecting to LDAP using LDAPS or START_TLS. (i.e. `TLS`, `TLSv1.1`, `TLSv1.2`, etc). +|`TLS - Shutdown Gracefully`| Specifies whether the TLS should be shut down gracefully before the target context is closed. Defaults to `false`. +|`Referral Strategy`| Strategy for handling referrals. Possible values are `FOLLOW`, `IGNORE`, `THROW`. +|`Connect Timeout`| Duration of connect timeout. (i.e. `10 secs`). +|`Read Timeout`| Duration of read timeout. (i.e. `10 secs`). +|`Url`| Space-separated list of URLs of the LDAP servers (i.e. `ldap://:`). +|`Page Size`| Sets the page size when retrieving users and groups. If not specified, no paging is performed. +|`Sync Interval`| Duration of time between syncing users and groups. (i.e. `30 mins`). +|`Group Membership - Enforce Case Sensitivity` | Sets whether group membership decisions are case sensitive. When a user or group is inferred (by not specifying or user or group search base or user identity attribute or group name attribute) case sensitivity is enforced since the value to use for the user identity or group name would be ambiguous. Defaults to false. +|`User Search Base`| Base DN for searching for users (i.e. `ou=users,o=nifi`). Required to search users. +|`User Object Class`| Object class for identifying users (i.e. `person`). Required if searching users. +|`User Search Scope`| Search scope for searching users (`ONE_LEVEL`, `OBJECT`, or `SUBTREE`). Required if searching users. +|`User Search Filter`| Filter for searching for users against the `User Search Base` (i.e. `(memberof=cn=team1,ou=groups,o=nifi)`). Optional. +|`User Identity Attribute`| Attribute to use to extract user identity (i.e. `cn`). Optional. If not set, the entire DN is used. +|`User Group Name Attribute`| Attribute to use to define group membership (i.e. `memberof`). Optional. If not set group membership will not be calculated through the users. Will rely on group membership being defined through `Group Member Attribute` if set. The value of this property is the name of the attribute in the user LDAP entry that associates them with a group. The value of that user attribute could be a dn or group name for instance. What value is expected is configured in the `User Group Name Attribute - Referenced Group Attribute`. +|`User Group Name Attribute - Referenced Group Attribute`| If blank, the value of the attribute defined in `User Group Name Attribute` is expected to be the full dn of the group. If not blank, this property will define the attribute of the group LDAP entry that the value of the attribute defined in `User Group Name Attribute` is referencing (i.e. `name`). Use of this property requires that `Group Search Base` is also configured. +|`Group Search Base`| Base DN for searching for groups (i.e. `ou=groups,o=nifi`). Required to search groups. +|`Group Object Class`| Object class for identifying groups (i.e. `groupOfNames`). Required if searching groups. +|`Group Search Scope`| Search scope for searching groups (`ONE_LEVEL`, `OBJECT`, or `SUBTREE`). Required if searching groups. +|`Group Search Filter`| Filter for searching for groups against the `Group Search Base`. Optional. +|`Group Name Attribute`| Attribute to use to extract group name (i.e. `cn`). Optional. If not set, the entire DN is used. +|`Group Member Attribute`| Attribute to use to define group membership (i.e. `member`). Optional. If not set group membership will not be calculated through the groups. Will rely on group membership being defined through `User Group Name Attribute` if set. The value of this property is the name of the attribute in the group LDAP entry that associates them with a user. The value of that group attribute could be a dn or memberUid for instance. What value is expected is configured in the `Group Member Attribute - Referenced User Attribute`. (i.e. `member: cn=User 1,ou=users,o=nifi` vs. `memberUid: user1`) +|`Group Member Attribute - Referenced User Attribute`| If blank, the value of the attribute defined in `Group Member Attribute` is expected to be the full dn of the user. If not blank, this property will define the attribute of the user LDAP entry that the value of the attribute defined in `Group Member Attribute` is referencing (i.e. `uid`). Use of this property requires that `User Search Base` is also configured. (i.e. `member: cn=User 1,ou=users,o=nifi` vs. `memberUid: user1`) +|================================================================================================================================================== + +===== Composite Implementations + +Another option for the UserGroupProvider are composite implementations. This means that multiple sources/implementations can be configured and composed. For instance, an admin can configure users/groups to be loaded from a file and a directory server. There are two composite implementations, one that supports multiple UserGroupProviders and one that supports multiple UserGroupProviders and a single configurable UserGroupProvider. + +The CompositeUserGroupProvider will provide support for retrieving users and groups from multiple sources. The CompositeUserGroupProvider has the following properties: + +[options="header,footer"] +|================================================================================================================================================== +| Property Name | Description +|`User Group Provider` | The identifier of user group providers to load from. The name of each property must be unique, for example: ``"User Group Provider A"``, ``"User Group Provider B"``, ``"User Group Provider C"`` or ``"User Group Provider 1"``, ``"User Group Provider 2"``, ``"User Group Provider 3"`` +|================================================================================================================================================== + +The CompositeConfigurableUserGroupProvider will provide support for retrieving users and groups from multiple sources. Additionally, a single configurable user group provider is required. Users from the configurable user group provider are configurable, however users loaded from one of the User Group Provider [unique key] will not be. The CompositeConfigurableUserGroupProvider has the following properties: + +[options="header,footer"] +|================================================================================================================================================== +| Property Name | Description +|`Configurable User Group Provider` | A configurable user group provider. +|`User Group Provider` | The identifier of user group providers to load from. The name of each property must be unique, for example: ``"User Group Provider A"``, ``"User Group Provider B"``, ``"User Group Provider C"`` or ``"User Group Provider 1"``, ``"User Group Provider 2"``, ``"User Group Provider 3"`` +|================================================================================================================================================== + +==== AccessPolicyProvider + +After you have configured a UserGroupProvider, you must configure an AccessPolicyProvider that will control Access Policies for the identities in the UserGroupProvider. + +===== FileAccessPolicyProvider + +The default AccessPolicyProvider is the FileAccessPolicyProvider, however, you can develop additional AccessPolicyProvider as extensions. The FileAccessPolicyProvider has the following properties: + +[options="header,footer"] +|================================================================================================================================================== +| Property Name | Description +|`User Group Provider` | The identifier for an User Group Provider defined above that will be used to access users and groups for use in the managed access policies. +|`Authorizations File`| The file where the FileAccessPolicyProvider will store policies. By default, _authorizations.xml_ in the `conf` directory is chosen. +|`Initial Admin Identity`| The identity of an initial admin user that will be granted access to the UI and given the ability to create additional users, groups, and policies. For example, a certificate DN, LDAP identity, or Kerberos principal. +|`NiFi Identity`| The identity of a NiFi instance/node that will be accessing this registry. Each NiFi Identity will be granted permission to proxy user requests, as well as read any bucket to perform synchronization status checks. +|`NiFi Group Name`| The name of the group, whose members are NiFi instance/node identities, that will be accessing this registry. The members of this group will be granted permission to proxy user requests, as well as read any bucket to perform synchronization checks. +|================================================================================================================================================== + +NOTE: The identities configured in the Initial Admin Identity and NiFi Identity properties must be available in the configured User Group Provider. Initial Admin Identity and NiFi Identity properties are only read by NiFi Registry when the Authorizations File is missing or empty on startup in order to seed the initial Authorizations File. +Changes to the configured Initial Admin Identity and NiFi Identities will not take effect if the Authorizations File is populated. + +[[initial-admin-identity]] +==== Initial Admin Identity (New NiFi Registry Instance) + +If you are setting up a secured NiFi Registry instance for the first time, you must manually designate an “Initial Admin Identity” in the _authorizers.xml_ file. +This initial admin user is granted access to the UI and given the ability to create additional users, groups, and policies. +The value of this property could be a certificate DN , LDAP identity (DN or username), or a Kerberos principal. +If you are the NiFi Registry administrator, add yourself as the “Initial Admin Identity”. + +After you have edited and saved the _authorizers.xml_ file, restart NiFi Registry. +The _users.xml_ and _authorizations.xml_ files will be created, and the “Initial Admin Identity” user and administrative policies are added during start up. +Once NiFi Registry starts, the “Initial Admin Identity” user is able to access the UI and begin managing users, groups, and policies. + +NOTE: If initial NiFi identities are not provided, they can be added through the UI at a later time by first creating a user for the given +NiFi identity, and then giving that user both Proxy permissions and permission to Buckets/READ in order to read all buckets. + +Some common use cases are described below. + +===== File-based (LDAP Authentication) +Here is an example certificate DN entry using the name John Smith: + +---- + + + + file-user-group-provider + org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider + ./conf/users.xml + + cn=John Smith,ou=people,dc=example,dc=com + + + + file-access-policy-provider + org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider + file-user-group-provider + ./conf/authorizations.xml + cn=John Smith,ou=people,dc=example,dc=com + + + + managed-authorizer + org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer + file-access-policy-provider + + +---- + +===== File-based (Kerberos Authentication) +Here is an example Kerberos entry using the name John Smith and realm `NIFI.APACHE.ORG`: + +---- + + + + file-user-group-provider + org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider + ./conf/users.xml + johnsmith@NIFI.APACHE.ORG + + + + file-access-policy-provider + org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider + file-user-group-provider + ./conf/authorizations.xml + johnsmith@NIFI.APACHE.ORG + + + + + managed-authorizer + org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer + file-access-policy-provider + + +---- + +===== LDAP-based Users/Groups Referencing User DN +Here is an example loading users and groups from LDAP. Group membership will be driven through the member attribute of each group. +Authorization will still use file-based access policies. + +Given the following LDAP entries exist: + +---- +dn: cn=User 1,ou=users,o=nifi +objectClass: organizationalPerson +objectClass: person +objectClass: inetOrgPerson +objectClass: top +cn: User 1 +sn: User1 +uid: user1 + +dn: cn=User 2,ou=users,o=nifi +objectClass: organizationalPerson +objectClass: person +objectClass: inetOrgPerson +objectClass: top +cn: User 2 +sn: User2 +uid: user2 + +dn: cn=users,ou=groups,o=nifi +objectClass: groupOfNames +objectClass: top +cn: users +member: cn=User 1,ou=users,o=nifi +member: cn=User 2,ou=users,o=nifi +---- + +An Authorizer using an LdapUserGroupProvider would be configured as: + +---- + + + ldap-user-group-provider + org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider + ANONYMOUS + + + + + + + + + + + + + + + FOLLOW + 10 secs + 10 secs + + ldap://localhost:10389 + + 30 mins + false + + ou=users,o=nifi + person + ONE_LEVEL + + cn + + + + ou=groups,o=nifi + groupOfNames + ONE_LEVEL + + cn + member + + + + + file-access-policy-provider + org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider + ldap-user-group-provider + ./conf/authorizations.xml + User 1 + + + + + managed-authorizer + org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer + file-access-policy-provider + + +---- + +The `Initial Admin Identity` value would have loaded from the cn of the User 1 entry based on the `User Identity Attribute` value. + +===== Composite - File and LDAP-based Users/Groups +Here is an example composite implementation loading users and groups from LDAP and a local file. Group membership will be driven through +the member attribute of each group. The users from LDAP will be read only while the users loaded from the file will be configurable in UI. + +---- + + + + file-user-group-provider + org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider + ./conf/users.xml + cn=nifi-node1,ou=servers,dc=example,dc=com + cn=nifi-node2,ou=servers,dc=example,dc=com + + + + ldap-user-group-provider + org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider + ANONYMOUS + + + + + + + + + + + + + + + FOLLOW + 10 secs + 10 secs + + ldap://localhost:10389 + + 30 mins + false + + ou=users,o=nifi + person + ONE_LEVEL + + cn + + + + ou=groups,o=nifi + groupOfNames + ONE_LEVEL + + cn + member + + + + + composite-user-group-provider + org.apache.nifi.registry.security.authorization.CompositeUserGroupProvider + file-user-group-provider + ldap-user-group-provider + + + + file-access-policy-provider + org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider + composite-user-group-provider + ./conf/authorizations.xml + User 1/property> + cn=nifi-node1,ou=servers,dc=example,dc=com + cn=nifi-node2,ou=servers,dc=example,dc=com + + + + managed-authorizer + org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer + file-access-policy-provider + + +---- + +In this example, the users and groups are loaded from LDAP but the servers are managed in a local file. The `Initial Admin Identity` value came +from an attribute in a LDAP entry based on the `User Identity Attribute`. The `NiFi Identity` values are established in the local file using the +`Initial User Identity` properties. + +=== Access Policies + +You can manage the ability for users and groups to view or modify NiFi Registry resources using 'access policies'. Access policies can be created to control access to buckets, as well as to grant special privileges to users for managing a NiFi Registry instance. + +==== Bucket Policies + +Bucket policies govern the following bucket level authorizations: + +|=== +|Policy |Privilege |Resource Descriptor + +| Read Bucket +| Allows users to read items in the bucket +| `resource="/buckets/" action="R"` + +| Write Bucket +| Allows users to write items to the bucket +| `resource="/buckets/" action="W"` + +| Delete Bucket +| Allows users to delete the bucket +| `resource="/buckets/" action="D"` + + +|=== + + +==== Special Privilege Policies + +Special privilege policies govern the following system level authorizations: + +|=== +|Policy |Privilege |Resource Descriptor + +| Can Manage Buckets (Read) +| Allows users to read from all buckets +| `resource="/buckets" action="R"` + +| Can Manage Buckets (Write) +| Allows users to write to all buckets +| `resource="/buckets" action="W"` + +| Can Manage Buckets (Delete) +| Allows users to delete all buckets +| `resource="/buckets" action="D"` + +| Can Manage Users (Read) +| Allows users to view users +| `resource="/tenants" action="R"` + +| Can Manage Users (Write) +| Allows users to create and modify users +| `resource="/tenants" action="W"` + +| Can Manage Users (Delete) +| Allows users to delete users +| `resource="/tenants" action="D"` + +| Can Manage Policies (Read) +| Allows users to view policies +| `resource="/policies" action="R"` + +| Can Manage Policies (Write) +| Allows users to create and modify policies +| `resource="/policies" action="W"` + +| Can Manage Policies (Delete) +| Allows users to delete policies +| `resource="/policies" action="D"` + +| Can Proxy Requests (Read) +| Allows users to proxy read requests (GET) +| `resource="/proxy" action="R"` + +| Can Proxy Requests (Write) +| Allows users to proxy write requests (POST, PUT, PATCH) +| `resource="/proxy" action="W"` + +| Can Proxy Requests (Delete) +| Allows users to proxy delete requests (DELETE) +| `resource="/proxy" action="D"` + +| View Swagger +| Allows users to access the self-hosted Swagger UI +| `resource="/swagger" action="R"` + +| View Actuator +| Allows users to access the Spring Boot Actuator end-points +| `resource="/actuator" action="R"` + +|=== + + +== Encrypted Passwords in Configuration Files + +In order to facilitate the secure setup of NiFi Registry, you can use the `encrypt-config` command line utility to encrypt raw configuration values +that NiFi Registry decrypts in memory on startup. This extensible protection scheme transparently allows NiFi Registry to use raw values in operation, +while protecting them at rest. In the future, hardware security modules (HSM) and external secure storage mechanisms will be integrated, but for now, +an AES encryption provider is the default implementation. + +If no administrator action is taken, the configuration values remain unencrypted. + +NOTE: The `encrypt-config` tool for NiFi Registry is implemented as an additional mode to the existing tool in the `nifi-toolkit`. The following sections +assume you have downloaded the binary for the nifi-toolkit. + +[[encrypt-config_tool]] +=== Encrypt-Config Tool + +The `encrypt-config` command line tool can be used to encrypt NiFi Registry configuration by invoking the tool with the following command: + +---- +./bin/encrypt-config --nifiRegistry [options] +---- + +You can use the following command line options with the `encrypt-config` tool: + +* `-h`,`--help` Show usage information (this message) +* `-v`,`--verbose` Sets verbose mode (default false) +* `-p`,`--password ` Protect the files using a password-derived key. If an argument is not provided to this flag, interactive mode will be triggered to prompt the user to enter the password. +* `-k`,`--key ` Protect the files using a raw hexadecimal key. If an argument is not provided to this flag, interactive mode will be triggered to prompt the user to enter the key. +* `--oldPassword ` If the input files are already protected using a password-derived key, this specifies the old password so that the files can be unprotected before re-protecting. +* `--oldKey ` If the input files are already protected using a key, this specifies the raw hexadecimal key so that the files can be unprotected before re-protecting. +* `-b`,`--bootstrapConf ` The _bootstrap.conf_ file containing no root key or an existing root key. If a new password or key is specified (using `-p` or `-k`) and no output _bootstrap.conf_ file is specified, then this file will be overwritten to persist the new root key. +* `-B`,`--outputBootstrapConf ` The destination _bootstrap.conf_ file to persist root key. If specified, the input _bootstrap.conf_ will not be modified. +* `-r`,`--nifiRegistryProperties ` The _nifi-registry.properties_ file containing unprotected config values, overwritten if no output file specified. +* `-R`,`--outputNifiRegistryProperties ` The destination _nifi-registry.properties_ file containing protected config values. +* `-a`,`--authorizersXml ` The _authorizers.xml_ file containing unprotected config values, overwritten if no output file specified. +* `-A`,`--outputAuthorizersXml ` The destination _authorizers.xml_ file containing protected config values. +* `-i`,`--identityProvidersXml ` The _identity-providers.xml_ file containing unprotected config values, overwritten if no output file specified. +* `-I`,`--outputIdentityProvidersXml ` The destination _identity-providers.xml_ file containing protected config values. +* `--decrypt` Can be used with `-r` to decrypt a previously encrypted NiFi Registry Properties file. Decrypted content is printed to STDOUT. + +As an example of how the tool works, assume that you have installed the tool on a machine supporting 256-bit encryption and with the following existing values in the _nifi-registry.properties_ file: + +---- +# security properties # +nifi.registry.security.keystore=/path/to/keystore.jks +nifi.registry.security.keystoreType=JKS +nifi.registry.security.keystorePasswd=thisIsABadKeystorePassword +nifi.registry.security.keyPasswd=thisIsABadKeyPassword +nifi.registry.security.truststore= +nifi.registry.security.truststoreType= +nifi.registry.security.truststorePasswd= +---- + +Enter the following arguments when using the tool: + +---- +./bin/encrypt-config.sh --nifiRegistry \ +-b bootstrap.conf \ +-k 0123456789ABCDEFFEDCBA98765432100123456789ABCDEFFEDCBA9876543210 \ +-r nifi-registry.properties +---- + +As a result, the _nifi-registry.properties_ file is overwritten with protected properties and sibling encryption identifiers (`aes/gcm/256`, the currently supported algorithm): + +---- +# security properties # +nifi.registry.security.keystore=/path/to/keystore.jks +nifi.registry.security.keystoreType=JKS +nifi.registry.security.keystorePasswd=oBjT92hIGRElIGOh||MZ6uYuWNBrOA6usq/Jt3DaD2e4otNirZDytac/w/KFe0HOkrJR03vcbo +nifi.registry.security.keystorePasswd.protected=aes/gcm/256 +nifi.registry.security.keyPasswd=ac/BaE35SL/esLiJ||+ULRvRLYdIDA2VqpE0eQXDEMjaLBMG2kbKOdOwBk/hGebDKlVg== +nifi.registry.security.keyPasswd.protected=aes/gcm/256 +nifi.registry.security.truststore= +nifi.registry.security.truststoreType= +nifi.registry.security.truststorePasswd= +---- + +When applied to _identity-providers.xml_ or _authorizers.xml_, the property elements are updated with an `encryption` attribute. For example: + +---- + + + ldap-provider + org.apache.nifi.registry.security.ldap.LdapProvider + START_TLS + someuser + q4r7WIgN0MaxdAKM||SGgdCTPGSFEcuH4RraMYEdeyVbOx93abdWTVSWvh1w+klA + /path/to/keystore.jks + Uah59TWX+Ru5GY5p||B44RT/LJtC08QWA5ehQf01JxIpf0qSJUzug25UwkF5a50g + JKS + ... + +---- + +Additionally, the _bootstrap.conf_ file is updated with the encryption key as follows: + +---- +# Root key in hexadecimal format for encrypted sensitive configuration values +nifi.registry.bootstrap.sensitive.key=0123456789ABCDEFFEDCBA98765432100123456789ABCDEFFEDCBA9876543210 +---- + +Sensitive configuration values are encrypted by the tool by default, however you can encrypt any additional properties, if desired. +To encrypt additional properties, specify them as comma-separated values in the `nifi.registry.sensitive.props.additional.keys` property. + + +If the _nifi-registry.properties_ file already has valid protected values and you wish to protect additional values using the +same root key already present in your _bootstrap.conf_, then run the tool without specifying a new key: + +---- +# bootstrap.conf already contains root key property +# nifi-registy.properties has been updated for nifi.registry.sensitive.props.additional.keys=... + +./bin/encrypt-config.sh --nifiRegistry -b bootstrap.conf -r nifi-registry.properties +---- + +[sensitive_property_key_migration] +=== Sensitive Property Key Migration + +In order to change the key used to encrypt the sensitive values, provide the new key or password using the `-k` or `-p` flags as usual, +and provide the existing key or password using `--old-key` or `--old-password` respectively. This will allow the toolkit to decrypt the +existing values and re-encrypt them, and update _bootstrap.conf_ with the new key. Only one of the key or password needs to be specified +for each phase (old vs. new), and any combination is sufficient: + +* old key -> new key +* old key -> new password +* old password -> new key +* old password -> new password + +[[bootstrap_properties]] +== Bootstrap Properties + +The _bootstrap.conf_ file in the `conf` directory allows users to configure settings for how NiFi Registry should be started. This includes parameters, such as the size of the Java Heap, what Java command to run, and Java System Properties. + +Here, we will address the different properties that are made available in the file. Any changes to this file will take effect only after NiFi Registry has been stopped and restarted. + +|==== +|*Property*|*Description* +|`java`|Specifies the fully qualified java command to run. By default, it is simply `java` but could be changed to an absolute path or a reference an environment variable, such as `$JAVA_HOME/bin/java` +|`run.as`|The username to run NiFi Registry as. For instance, if NiFi Registry should be run as the `nifi_registry` user, setting this value to `nifi_registry` will cause the NiFi Registry Process to be run as the `nifi_registry` user. This property is ignored on Windows. For Linux, the specified user may require sudo permissions. +|`lib.dir`|The _lib_ directory to use for NiFi Registry. By default, this is set to `./lib` +|`conf.dir`|The `conf` directory to use for NiFi Registry. By default, this is set to `./conf` +|`graceful.shutdown.seconds`|When NiFi Registry is instructed to shutdown, the Bootstrap will wait this number of seconds for the process to shutdown cleanly. At this amount of time, if the service is still running, the Bootstrap will `kill` the process, or terminate it abruptly. By default, this is set to `20`. +|`java.arg.N`|Any number of JVM arguments can be passed to the NiFi Registry JVM when the process is started. These arguments are defined by adding properties to _bootstrap.conf_ that begin with `java.arg.`. The rest of the property name is not relevant, other than to different property names, and will be ignored. The default includes properties for minimum and maximum Java Heap size, the garbage collector to use, etc. +|`nifi.registry.bootstrap.sensitive.key`|The root key (in hexadecimal format) for encrypted sensitive configuration values. When NiFi Registry is started, this root key is used to decrypt sensitive values from the _nifi-registry.properties_ file into memory for later use. + +The <> can be used to specify the root key, encrypt sensitive values in _nifi-registry.properties_ and update _bootstrap.conf_. +|==== + + +[[proxy_configuration]] +== Proxy Configuration + +​When running Apache NiFi Registry behind a proxy there are a couple of key items to be aware of during deployment. + +* NiFi Registry is comprised of a number of web applications (web UI, web API, documentation), so the mapping needs to be configured for the *root path*. +That way all context paths are passed through accordingly. + +* If NiFi Registry is running securely, any proxy needs to be authorized to proxy user requests. These can be configured in the NiFi Registry UI through the +Users administration section, by selecting 'Proxy' for the given user. Once these permissions are in place, proxies can begin proxying user requests. +The end user identity must be relayed in a HTTP header. For example, if the end user sent a request to the proxy, the proxy must authenticate the user. Following +this the proxy can send the request to NiFi Registry. In this request an HTTP header should be added as follows. + +.... +X-ProxiedEntitiesChain: +.... + +If the proxy is configured to send to another proxy, the request to NiFi Registry from the second proxy should contain a header as follows. + +.... +X-ProxiedEntitiesChain: +.... + +An example Apache proxy configuration that sets the required properties may look like the following. Complete proxy configuration is outside of the scope of this document. +Please refer to the documentation of the proxy for guidance with your deployment environment and use case. + +.... +... + + ... + SSLEngine On + SSLCertificateFile /path/to/proxy/certificate.crt + SSLCertificateKeyFile /path/to/proxy/key.key + SSLCACertificateFile /path/to/ca/certificate.crt + SSLVerifyClient require + RequestHeader add X-ProxyScheme "https" + RequestHeader add X-ProxyHost "proxy-host" + RequestHeader add X-ProxyPort "443" + RequestHeader add X-ProxyContextPath "/my-nifi-registry" + RequestHeader add X-ProxiedEntitiesChain "<%{SSL_CLIENT_S_DN}>" + ProxyPass https://nifi-registry-host:8443 + ProxyPassReverse https://nifi-registry-host:8443 + ... + +... +.... + +[[kerberos_service]] +== Kerberos Service + +NiFi Registry can be configured to use Kerberos SPNEGO (or "Kerberos Service") for authentication. In this scenario, users will hit the REST endpoint `/access/token/kerberos` +and the server will respond with a `401` status code and the challenge response header `WWW-Authenticate: Negotiate`. This communicates to the browser to use the GSS-API +and load the user's Kerberos ticket and provide it as a Base64-encoded header value in the subsequent request. It will be of the form `Authorization: Negotiate YII...`. +NiFi Registry will attempt to validate this ticket with the KDC. If it is successful, the user's _principal_ will be returned as the identity, and the flow will follow +login/credential authentication, in that a JWT will be issued in the response to prevent the unnecessary overhead of Kerberos authentication on every subsequent request. +If the ticket cannot be validated, it will return with the appropriate error response code. The user will then be able to provide their Kerberos credentials to the login +form if the `KerberosIdentityProvider` has been configured. See <> for more details. + +NiFi Registry will only respond to Kerberos SPNEGO negotiation over an HTTPS connection, as unsecured requests are never authenticated. + +See <> for complete documentation. + +[[kerberos_service_notes]] +=== Notes + +* Kerberos is case-sensitive in many places and the error messages (or lack thereof) may not be sufficiently explanatory. + Check the case sensitivity of the service principal in your configuration files. The convention is `HTTP/fully.qualified.domain@REALM`. +* Browsers have varying levels of restriction when dealing with SPNEGO negotiations. + Some will provide the local Kerberos ticket to any domain that requests it, while others whitelist the trusted domains. See link:https://docs.spring.io/autorepo/docs/spring-security-kerberos/1.0.2.BUILD-SNAPSHOT/reference/htmlsingle/#browserspnegoconfig[Spring Security Kerberos - Reference Documentation: Appendix E. Configure browsers for SPNEGO Negotiation^] for common browsers. +* Some browsers (legacy IE) do not support recent encryption algorithms such as AES, and are restricted to legacy algorithms (DES). This should be noted when generating keytabs. +* The KDC must be configured and a service principal defined for NiFi and a keytab exported. Comprehensive instructions for Kerberos server configuration and administration are beyond the scope of this document (see link:https://web.mit.edu/kerberos/krb5-current/doc/admin/index.html[MIT Kerberos Admin Guide^]), but an example is below. +* Kerberos tickets may use AES encryption with keys up to 256-bits in length, and therefore unlimited strength encryption policies may be required for the Jave Runtime Environment (JRE) used for NiFi Registry when Kerberos SPNEGO is configured. + +Adding a service principal for a server at `nifi.nifi.apache.org` and exporting the keytab from the KDC: + +.... +root@kdc:/etc/krb5kdc# kadmin.local +Authenticating as principal admin/admin@NIFI.APACHE.ORG with password. +kadmin.local: listprincs +K/M@NIFI.APACHE.ORG +admin/admin@NIFI.APACHE.ORG +... +kadmin.local: addprinc -randkey HTTP/nifi.nifi.apache.org +WARNING: no policy specified for HTTP/nifi.nifi.apache.org@NIFI.APACHE.ORG; defaulting to no policy +Principal "HTTP/nifi.nifi.apache.org@NIFI.APACHE.ORG" created. +kadmin.local: ktadd -k /http-nifi.keytab HTTP/nifi.nifi.apache.org +Entry for principal HTTP/nifi.nifi.apache.org with kvno 2, encryption type des3-cbc-sha1 added to keytab WRFILE:/http-nifi.keytab. +Entry for principal HTTP/nifi.nifi.apache.org with kvno 2, encryption type des-cbc-crc added to keytab WRFILE:/http-nifi.keytab. +kadmin.local: listprincs +HTTP/nifi.nifi.apache.org@NIFI.APACHE.ORG +K/M@NIFI.APACHE.ORG +admin/admin@NIFI.APACHE.ORG +... +kadmin.local: q +root@kdc:~# ll /http* +-rw------- 1 root root 162 Mar 14 21:43 /http-nifi.keytab +root@kdc:~# +.... + +[[system_properties]] +== System Properties + +The _nifi-registry.properties_ file in the `conf` directory is the main configuration file for controlling how NiFi Registry runs. This section +provides an overview of the properties in this file and includes some notes on how to configure it in a way that will make upgrading easier. +*After making changes to this file, restart NiFi Registry in order for the changes to take effect.* + +NOTE: Values for periods of time and data sizes must include the unit of measure, for example "10 secs" or "10 MB", not simply "10". + +=== Web Properties + +These properties pertain to the web-based User Interface. + +|==== +|*Property*|*Description* +|`nifi.registry.web.war.directory`|This is the location of the web war directory. The default value is `./lib`. +|`nifi.registry.web.http.host`|The HTTP host. It is blank by default. +|`nifi.registry.web.http.port`|The HTTP port. The default value is `18080`. +|`nifi.registry.web.https.host`|The HTTPS host. It is blank by default. +|`nifi.registry.web.https.port`|The HTTPS port. It is blank by default. When configuring NiFi Registry to run securely, this port should be configured. +|`nifi.registry.web.jetty.working.directory`|The location of the Jetty working directory. The default value is `./work/jetty`. +|`nifi.registry.web.jetty.threads`|The number of Jetty threads. The default value is `200`. +|==== + +=== Security Properties + +These properties pertain to various security features in NiFi Registry. Many of these properties are covered in more detail in the +<> section. + +|==== +|*Property*|*Description* +|`nifi.registry.security.keystore`|The full path and name of the keystore. It is blank by default. +|`nifi.registry.security.keystoreType`|The keystore type. It is blank by default. +|`nifi.registry.security.keystorePasswd`|The keystore password. It is blank by default. +|`nifi.registry.security.keyPasswd`|The key password. It is blank by default. +|`nifi.registry.security.truststore`|The full path and name of the truststore. It is blank by default. +|`nifi.registry.security.truststoreType`|The truststore type. It is blank by default. +|`nifi.registry.security.truststorePasswd`|The truststore password. It is blank by default. +|`nifi.registry.security.needClientAuth`| This specifies that connecting clients must authenticate with a client cert. Setting this to `false` will specify that connecting clients may optionally authenticate with a client cert, but may also login with a username and password against a configured identity provider. The default value is `true`. +|`nifi.registry.security.authorizers.configuration.file`|This is the location of the file that specifies how authorizers are defined. The default value is `./conf/authorizers.xml`. +|`nifi.registry.security.authorizer`|Specifies which of the configured Authorizers in the _authorizers.xml_ file to use. By default, it is set to `managed-authorizer`. +|`nifi.registry.security.identity.providers.configuration.file`|This is the location of the file that specifies how username/password authentication is performed. This file is only considered if `nifi.registry.security.identity.provider` is configured with a provider identifier. The default value is `./conf/identity-providers.xml`. +|`nifi.registry.security.identity.provider`|This indicates what type of identity provider to use. The default value is blank, can be set to the identifier from a provider in the file specified in `nifi.registry.security.identity.providers.configuration.file`. Setting this property will trigger NiFi Registry to support username/password authentication. +|==== + +=== Identity Mapping Properties + +These properties can be utilized to normalize user identities. When implemented, identities authenticated by different identity providers (certificates, LDAP, Kerberos) are treated the same internally in NiFi Registry. As a result, duplicate users are avoided and user-specific configurations such as authorizations only need to be setup once per user. + +The following examples demonstrate normalizing DNs from certificates and principals from Kerberos: + +---- +nifi.registry.security.identity.mapping.pattern.dn=^CN=(.*?), OU=(.*?), O=(.*?), L=(.*?), ST=(.*?), C=(.*?)$ +nifi.registry.security.identity.mapping.value.dn=$1@$2 +nifi.registry.security.identity.mapping.transform.dn=NONE +nifi.registry.security.identity.mapping.pattern.kerb=^(.*?)/instance@(.*?)$ +nifi.registry.security.identity.mapping.value.kerb=$1@$2 +nifi.registry.security.identity.mapping.transform.kerb=NONE +---- + +The last segment of each property is an identifier used to associate the pattern with the replacement value. When a user makes a request to NiFi Registry, their identity is checked to see if it matches each of those patterns in lexicographical order. For the first one that matches, the replacement specified in the `nifi.registry.security.identity.mapping.value.xxxx` property is used. So a login with `CN=localhost, OU=Apache NiFi, O=Apache, L=Santa Monica, ST=CA, C=US` matches the DN mapping pattern above and the DN mapping value `$1@$2` is applied. The user is normalized to `localhost@Apache NiFi`. + +In addition to mapping, a transform may be applied. The supported versions are `NONE` (no transform applied), `LOWER` (identity lowercased), and `UPPER` (identity uppercased). If not specified, the default value is `NONE`. + +NOTE: These mappings are also applied to the "Initial Admin Identity" in the _authorizers.xml_ file, as well as users imported from LDAP (See <>). + +Group names can also be mapped. The following example will accept the existing group name but will lowercase it. This may be helpful when used in conjunction with an external authorizer. + +---- +nifi.registry.security.group.mapping.pattern.anygroup=^(.*)$ +nifi.registry.security.group.mapping.value.anygroup=$1 +nifi.registry.security.group.mapping.transform.anygroup=LOWER +---- + +NOTE: These mappings are applied to groups imported from LDAP. + + +=== Providers Properties + +These properties pertain to flow persistence providers. NiFi Registry uses a pluggable flow persistence provider to store the +content of the flows saved to the registry. For further details on persistence providers, refer <>. + +|==== +|*Property*|*Description* +|`nifi.registry.providers.configuration.file`|This is the location of the file where flow persistence providers are configured. The default value is `./conf/providers.xml`. +|==== + +=== Alias Properties + +These properties pertain to the support for URL aliasing. For further details, refer to <>. + +|==== +|*Property*|*Description* +|`nifi.registry.registry.alias.configuration.file`|This is the location of the file where URL aliases are configured. The default value is `./conf/registry-aliases.xml`. +|==== + + +=== Database Properties + +These properties define the settings for the Registry database, which keeps track of metadata about buckets and all items stored in buckets. + +The 0.1.0 release leveraged an embedded H2 database that was configured via the following properties: + +|==== +|*Property*|*Description* +|`nifi.registry.db.directory`|The location of the Registry database directory. The default value is `./database`. +|`nifi.registry.db.url.append`|This property specifies additional arguments to add to the connection string for the Registry database. The default value should be used and should not be changed. It is: `;LOCK_TIMEOUT=25000;WRITE_DELAY=0;AUTO_SERVER=FALSE`. +|==== + +The 0.2.0 release introduced a more flexible approach which allows leveraging an external database. This new approach +is configured via the following properties: + +|==== +|*Property*|*Description* +|`nifi.registry.db.url`| The full JDBC connection string. The default value will specify a new H2 database in the same location as the previous one. For example, `jdbc:h2:./database/nifi-registry-primary;`. +|`nifi.registry.db.driver.class`| The class name of the JDBC driver. The default value is `org.h2.Driver`. +|`nifi.registry.db.driver.directory`| An optional directory containing one or more JARs to add to the classpath. If not specified, it is assumed that the driver JAR is already on the classpath by copying it to the `lib` directory. The H2 driver is bundled with Registry so it is not necessary to do anything for the default case. +|`nifi.registry.db.username`| The username for the database. The default value is `nifireg`. +|`nifi.registry.db.password`| The password for the database. The default value is `nifireg`. +|`nifi.registry.db.maxConnections`| The max number of connections for the connection pool. The default value is `5`. +|`nifi.registry.db.sql.debug`| Whether or not enable debug logging for SQL statements. The default value is `false`. +|==== + +NOTE: When upgrading from 0.1.0 to a future version, if `nifi.registry.db.directory` remains populated, the application will +attempt to migrate the data from the original database to the new database specified with the new properties. This will only +happen the first time the application starts with the new database properties. + +=== Extension Directories + +Each property beginning with `nifi.registry.extension.dir.` will be treated as location for an extension, and a class loader will be created for each location, with the system class loader as the parent. + +|==== +|*Property*|*Description* +|`nifi.registry.extension.dir.1`| The full path on the filesystem to the location of the JARs for the given extension +|==== + +NOTE: Multiple extension directories can be specified by using the `nifi.registry.extension.dir.` prefix with unique suffixes and separate paths as values. +For example, to provide an additional extension directory, a user could also specify additional properties with keys of: `nifi.registry.extension.dir.2=/path/to/extension2`, +providing 2 total locations, including `nifi.registry.extension.dir.1`. + + +[[kerberos_properties]] +=== Kerberos Properties + +|==== +|*Property*|*Description* +|`nifi.registry.kerberos.krb5.file`|The location of the krb5 file, if used. It is blank by default. At this time, only a single krb5 file is allowed to + be specified per NiFi instance, so this property is configured here to support SPNEGO and service principals rather than in individual Processors. + If necessary the krb5 file can support multiple realms. + Example: `/etc/krb5.conf` +|`nifi.registry.kerberos.spnego.principal`|The name of the NiFi Registry Kerberos SPNEGO principal, if used. It is blank by default. Note that this property is used to authenticate NiFi Registry users. + Example: `HTTP/nifi.registry.example.com` or `HTTP/nifi.registry.example.com@EXAMPLE.COM` +|`nifi.registry.kerberos.spnego.keytab.location`|The file path of the NiFi Registry Kerberos SPNEGO keytab, if used. It is blank by default. Note that this property is used to authenticate NiFi Registry users. + Example: `/etc/http-nifi-registry.keytab` +|`nifi.registry.kerberos.spengo.authentication.expiration`|The expiration duration of a successful Kerberos user authentication, if used. The default value is `12 hours`. +|==== + +== Metadata Database + +The metadata database maintains the knowledge of which buckets exist, which versioned items belong to which buckets, as well as the version history for each item. + +Currently, NiFi Registry supports using H2, Postgres (9.x, 10.x), and MySQL (5.6, 5.7, 8.0) for the relational database engine. + +NOTE: NiFi Registry 0.1.0 only supports H2. + +=== H2 + +H2 is an embedded database that is pre-configured in the default _nifi-registry.properties_ file. The contents of the H2 database are stored in a file on the local filesystem. + +For NiFi Registry 0.1.0, the location of the H2 database is specified by the property: + +`nifi.registry.db.directory=./database` + +For NiFi Registry 0.2.0 and forward, the location of the H2 database is specified as part of the JDBC URL property: + +`nifi.registry.db.url=jdbc:h2:./database/nifi-registry-primary;` + +=== Postgres + +Postgres provides the option to use an externally located database that also supports high availability. + +The following steps are required to use Postgres: + +1. Download the Postgres JDBC driver and place it somewhere accessible to NiFi Registry + + /path/to/drivers/postgresql-42.2.2.jar + +2. Create a database inside Postgres + + createdb nifireg + +3. Create a database user and grant privileges + + psql nifireg + CREATE USER nifireg WITH PASSWORD 'changeme'; + GRANT ALL PRIVILEGES ON DATABASE nifireg to nifireg; + \q + +4. Configure the database properties in _nifi-registry.properties_ + + nifi.registry.db.url=jdbc:postgresql:///nifireg + nifi.registry.db.driver.class=org.postgresql.Driver + nifi.registry.db.driver.directory=/path/to/drivers + nifi.registry.db.username=nifireg + nifi.registry.db.password=changeme + +=== MySQL + +MySQL also provides the option to use an externally located database that also supports high availability. + +The following steps are required to use MySQL: + +1. Download the MySQL JDBC driver and place it somewhere accessible to NiFi Registry + + /path/to/drivers/mysql-connector-java-8.0.16.jar + +2. Create a database inside MySQL (enter mysql shell using `mysql -u root -p` + + CREATE DATABASE nifi_registry; + +3. Create a database user and grant privileges (for remote users, use `nifireg'@'`, or `nifireg'@'%` for any remote host) + + GRANT ALL PRIVILEGES ON nifi_registry.* TO 'nifireg'@'localhost' IDENTIFIED BY 'changeme'; + +4. Configure the database properties in _nifi-registry.properties_ + + nifi.registry.db.url=jdbc:mysql:///nifi_registry + nifi.registry.db.driver.class=com.mysql.cj.jdbc.Driver + nifi.registry.db.driver.directory=/path/to/drivers + nifi.registry.db.username=nifireg + nifi.registry.db.password=changeme + +== Schema Differences & Limitations + +Due to differences across database implementations, there are two versions of the schema for NiFi Registry's metadata database. The original version supports H2 and Postgres, and a second versions supports MySQL. + +MySQL has limitations on the maximum size of text columns that are part of an index, or unique key. This means the maximum length of some columns is significantly less when using MySQL vs. H2/Postgres. + +NOTE: If choosing to use MySQL it is important to understand these limitations and accept them. + +The following tables summarizes the schema differences in column lengths: + +|==== +|*Table.Column*|*H2/Postgres*|*MySQL* +|BUCKET.NAME|1000|767 +|FLOW_SNAPSHOT.CREATED_BY|4096|767 +|SIGNING_KEY.TENANT_IDENTITY|4096|767 +|BUNDLE.GROUP_ID|500|200 +|BUNDLE.ARTIFACT_ID|500|200 +|BUNDLE_VERSION.CREATED_BY|4096|767 +|BUNDLE_VERSION.BUILT_BY|4096|767 +|BUNDLE_VERSION_DEPENDENCY.GROUP_ID|500|200 +|BUNDLE_VERSION_DEPENDENCY.ARTIFACT_ID|500|200 +|EXTENSION_PROVIDED_SERVICE_API.CLASS_NAME|500|200 +|EXTENSION_PROVIDED_SERVICE_API.GROUP_ID|500|200 +|EXTENSION_PROVIDED_SERVICE_API.ARTIFACT_ID|500|200 +|==== + + +== Persistence Providers + +NiFi Registry uses a pluggable persistence provider to store the content of each versioned item. Each type of versioned item, such as a versioned flow or extension bundle, has its own persistence provider. + +Each persistence provider has its own configuration parameters, which can be configured in an XML file specified in _<>_. + +=== Flow Persistence Providers + +The flow persistence provider stores the content of the flows saved to the registry. NiFi Registry provides `<>` and `<>`. + +The XML configuration file looks like below. It has a `flowPersistenceProvider` element in which qualified class name of a persistence provider implementation and its configuration properties are defined. See following sections for available configurations for each provider. + +.Example flow persistence provider in providers.xml +[source,xml] +.... + + persistence-provider-qualified-class-name + property-value-1 + property-value-2 + property-value-n + +.... + +==== FileSystemFlowPersistenceProvider + +FileSystemFlowPersistenceProvider simply stores serialized Flow contents into `{bucket-id}/{flow-id}/{version}` directories. + +Example of persisted files: +.... +Flow Storage Directory/ +├── {bucket-id}/ +│ └── {flow-id}/ +│ ├── {version}/{version}.snapshot +└── d1beba88-32e9-45d1-bfe9-057cc41f7ce8/ + └── 219cf539-427f-43be-9294-0644fb07ca63/ + ├── 1/1.snapshot + └── 2/2.snapshot +.... + +Qualified class name: `org.apache.nifi.registry.provider.flow.FileSystemFlowPersistenceProvider` + +|==== +|*Property*|*Description* +|`Flow Storage Directory`|REQUIRED: File system path for a directory where flow contents files are persisted to. If the directory does not exist when NiFi Registry starts, it will be created. If the directory exists, it must be readable and writable from NiFi Registry. +|==== + + +==== GitFlowPersistenceProvider + +`GitFlowPersistenceProvider` stores flow contents under a Git directory. + +In contrast to `FileSystemFlowPersistenceProvider`, this provider uses human friendly Bucket and Flow names so that those files can be accessed by external tools. However, it is NOT supported to modify stored files outside of NiFi Registry. Persisted files are only read when NiFi Registry starts up. + +Buckets are represented as directories and Flow contents are stored as files in a Bucket directory they belong to. Flow snapshot histories are managed as Git commits, meaning only the latest version of Buckets and Flows exist in the Git directory. Old versions are retrieved from Git commit histories. + +.Example persisted files +.... +Flow Storage Directory/ +├── .git/ +├── Bucket_A/ +│ ├── bucket.yml +│ ├── Flow_1.snapshot +│ └── Flow_2.snapshot +└── Bucket_B/ + ├── bucket.yml + └── Flow_4.snapshot +.... + +Each Bucket directory contains a YAML file named `bucket.yml`. The file manages links from NiFi Registry Bucket and Flow IDs to actual directory and file names. When NiFi Registry starts, this provider reads through Git commit histories and lookup these `bucket.yml` files to restore Buckets and Flows for each snapshot version. + +.Example bucket.yml +[source,yml] +.... +layoutVer: 1 +bucketId: d1beba88-32e9-45d1-bfe9-057cc41f7ce8 +flows: + 219cf539-427f-43be-9294-0644fb07ca63: {ver: 7, file: Flow_1.snapshot} + 22cccb6c-3011-4493-a996-611f8f112969: {ver: 3, file: Flow_2.snapshot} +.... + +Qualified class name: `org.apache.nifi.registry.provider.flow.git.GitFlowPersistenceProvider` + +|==== +|*Property*|*Description* +|`Flow Storage Directory`|REQUIRED: File system path for a directory where flow contents files are persisted to. The directory must exist when NiFi registry starts. Also must be initialized as a Git directory. See <> for detail. +|`Remote To Push`|When a new flow snapshot is created, this persistence provider updates files in the specified Git directory, then creates a commit to the local repository. If `Remote To Push` is defined, it also pushes to the specified remote repository (e.g. `origin`). To define more detailed remote spec such as branch names, use `Refspec` (see +link:https://git-scm.com/book/en/v2/Git-Internals-The-Refspec[https://git-scm.com/book/en/v2/Git-Internals-The-Refspec^]). +|`Remote Access User`|This username is used to make push requests to the remote repository when `Remote To Push` is enabled, and the remote repository is accessed by HTTP protocol. If SSH is used, user authentication is done with SSH keys. +|`Remote Access Password`|The password for the `Remote Access User`. +|`Remote Clone Repository`|Remote repository URI to use to clone into `Flow Storage Directory`, if local repository is not present in `Flow Storage Directory`. If left empty the git directory needs to be configured as per <>. If URI is provided then `Remote Access User` and `Remote Access Password` also should be present. +Currently, default branch of remote will be cloned. +|==== + +===== Initialize Git directory + +In order to use `GitFlowPersistenceRepository`, you need to prepare a Git directory on the local file system. You can do so by initializing a directory with `git init` command, or clone an existing Git project from a remote Git repository by `git clone` command. If you want to clone the default branch of remote repository automatically, set the `Remote Clone Repository` as described above. + +- `git init` command +link:https://git-scm.com/docs/git-init[https://git-scm.com/docs/git-init^] +- `git clone` command +link:https://git-scm.com/docs/git-clone[https://git-scm.com/docs/git-clone^] + +===== Git user configuration + +This persistence provider uses preconfigured Git user name and user email address when it creates Git commits. NiFi Registry user name is added to commit messages. + +.Example commit +.... +commit 774d4bd125f2b1200f0a5ee1f1e9fedc6a415e83 +Author: git-user +Date: Tue May 8 14:30:31 2018 +0900 + + Commit message. + + By NiFi Registry user: nifi-registry-user-1 +.... + + +You can configure Git user name and email address by `git config` command. + +- `git config` command +link:https://git-scm.com/docs/git-config[https://git-scm.com/docs/git-config^] + + +===== Git user authentication + +By default, this persistence repository only create commits to local repository. No user authentication is needed to do so. However, if 'Commit To Push' is enabled, user authentication to the remote Git repository is required. + +If the remote repository is accessed by HTTP, then username and password for authentication can be configured in the providers XML configuration file. + +When SSH is used, SSH keys are used to identify a Git user. In order to pick the right key to a remote server, the SSH configuration file `${USER_HOME}/.ssh/config` is used. The SSH configuration file can contain multiple `Host` entries to specify a key file to login to a remote Git server. The `Host` must match with the target remote Git server hostname. + +.example SSH config file +.... +Host git.example.com + HostName git.example.com + IdentityFile ~/.ssh/id_rsa + +Host github.com + HostName github.com + IdentityFile ~/.ssh/key-for-github + +Host bitbucket.org + HostName bitbucket.org + IdentityFile ~/.ssh/key-for-bitbucket +.... + +==== DatabaseFlowPersistenceProvider + +`DatabaseFlowPersistenceProvider` stores flow contents in a database table. + +This provider leverages the same database used for the metadata database, so there is no configuration to provide since the +connection details will come from the database properties in `nifi-registry.properties`. + +The database table is named `FLOW_PERSISTENCE_PROVIDER` and has the following schema: + +|==== +|*Column*|*Description* +|BUCKET_ID|The identifier of the bucket where the flow is located. +|FLOW_ID|The identifier of the flow. +|VERSION|The version of the flow. +|FLOW_CONTENT|The serialized bytes of the flow content stored as a BLOB. +|==== + +==== Switching from other Flow Persistence Provider + +In order to switch the Flow Persistence Provider, it is necessary to reset NiFi Registry. +For example, to switch from `FileSystemFlowPersistenceProvider` to `GitFlowPersistenceProvider`, follow these steps: + +. Stop version control on all ProcessGroups in NiFi +. Stop NiFi Registry +. Move the H2 DB (specified as `nifi.registry.db.directory` in _nifi-registry.properties_) and `Flow Storage Directory` for `FileSystemFlowPersistenceProvider` directories somewhere for back up +. Configure `GitFlowPersistenceProvider` provider in _providers.xml_ +. Start NiFi Registry +. Recreate any buckets +. Start version control on all ProcessGroups again + +==== Data model version of serialized Flow snapshots + +Serialized Flow snapshots saved by these persistence providers have versions, so that the data format and schema can evolve over time. Data model version update is done automatically by NiFi Registry when it reads and stores each Flow content. + +Here is the data model version histories: + +|==== +|*Data model version*|*Since NiFi Registry*|*Description* +|2|0.2|JSON formatted text file. The root object contains header and Flow content object. +|1|0.1|Binary format having header bytes at the beginning followed by Flow content represented as XML. +|==== + +=== Bundle Persistence Providers + +The bundle persistence provider stores the content of extension bundles saved to the registry. NiFi Registry provides `<>` and `<>`. + +The XML configuration file looks like below. It has a `extensionBundlePersistenceProvider` element in which the qualified class name of a persistence provider implementation and its configuration properties are defined. See following sections for available configurations for each provider. + +.Example extension bundle persistence provider in providers.xml +[source,xml] +.... + + persistence-provider-qualified-class-name + property-value-1 + property-value-2 + property-value-n + +.... + +==== FileSystemBundlePersistenceProvider + +The `FileSystemBundlePersistenceProvider` stores the content of extension bundles on the local file system. The bundles are organized in directories according to bucket id, group, artifact, and version. + +Example of persisted extension bundles: +.... +Extension Bundle Storage Directory/ +├── {bucket-id}/ + └── {group-id}/ + └── {artifact-id} + └── {version}/{artifact-id}-{version}.{extension} +├── d1beba88-32e9-45d1-bfe9-057cc41f7ce8/ + └── org.apache.nifi + └── nifi-example-nar + └── 1.0.0/nifi-example-nar-1.0.0.nar + └── 2.0.0/nifi-example-nar-2.0.0.nar +.... + +===== Configuration + +Qualified class name: `org.apache.nifi.registry.provider.extension.FileSystemBundlePersistenceProvider` + +|==== +|*Property*|*Description* +|`Extension Bundle Storage Directory`|REQUIRED: File system path for a directory where extension bundle contents files are persisted to. If the directory does not exist when NiFi Registry starts, it will be created. If the directory exists, it must be readable and writable from NiFi Registry. +|==== + +==== S3BundlePersistenceProvider + +The `S3BundlePersistenceProvider` stores the content of extension bundles in a AWS S3 bucket. The bucket is expected to already exist and be accessible to the credentials provided to the persistence providcer. + +NOTE: This provider must be added to the classpath by specifying a custom extension directory in _nifi-registry.properties_, such as `nifi.registry.extension.dir.aws=./ext/aws/lib`, where `./ext/aws/` contains the contents of the extracted _nifi-registry-aws-assembly--bin.zip_. + +The key of an extension bundle in the S3 bucket will be the following: +.... +/{registry-bucket-id}/{group-id}/{artifact-id}/{version}/{artifact-id}-{version}.{extension} +.... + +If an optional Key Prefix is specified, then that prefix will be applied to the beginning of the above key. + +===== Configuration + +Qualified class name: `org.apache.nifi.registry.aws.S3BundlePersistenceProvider` + +|==== +|*Property*|*Description* +|`Region`|REQUIRED: The name of the S3 region where the bucket exists. +|`Bucket Name`|REQUIRED: The name of an existing bucket to store extension bundles. +|`Key Prefix`|An optional prefix that if specified will be added to the beginning of all S3 keys. +|`Credentials Provider`|REQUIRED: Indicates how credentials will be provided, must be a value of `DEFAULT_CHAIN` or `STATIC`. `DEFAULT_CHAIN` will consider in order: Java system properties, environment variables, credential profiles (`~/.aws/credentials`). `STATIC` requires that `Access Key` and `Secret Access Key` be specified directly in this file. +|`Access Key`| The access key to use when using `STATIC` credentials provider. +|`Secret Access Key`| The secret access key to use when using `STATIC` credentials provider. +|`Endpoint URL`| An optional URL that overrides the default AWS S3 endpoint URL. Set this when using an AWS S3 API compatible service hosted at a different URL. +|==== + +== Event Hooks +Event hooks are an integration point that allows for custom code to to be triggered when NiFi Registry application events occur. + +[options="header,footer"] +|================================================================================================================================================== +| Event Name | Description +|`CREATE_BUCKET` | A new registry bucket is created. +|`CREATE_FLOW` | A new flow is created in a specified bucket. Only triggered on first time creation of a flow with a given name. +|`CREATE_FLOW_VERSION` | A new version for a flow has been saved in the registry. +|`UPDATE_BUCKET` | A bucket has been updated. +|`UPDATE_FLOW` | A flow that exist in a bucket has been updated. +|`DELETE_BUCKET` | An existing bucket in the registry is deleted. +|`DELETE_FLOW` | An existing flow in the registry is deleted. +|`REGISTRY_START` | Invoked once the NiFi Registry application has been successfully started. This is only invoked after a complete and successful start. +|================================================================================================================================================== + +=== Shared Event Hook Properties +There are certain properties that are shared amongst all of the NiFi Registry provided Event Hook implementations. Those properties and +their purpose are listed below. + +[options="header,footer"] +|================================================================================================================================================== +| Property Name | Description +|`Whitelisted Event Type` | Event types the hook provider configured with this property should respond to. If this property is left blank or not provided, all events will fire for the configured hook provider. Multiple `Whitelisted Event Type` can be specified and often are. For example, +`CREATE_FLOW` and `UPDATE_FLOW` would invoke the configured hook provider for the `CREATE_FLOW` and `UPDATE_FLOW` event types. +|================================================================================================================================================== + +=== ScriptEventHookProvider +The `ScriptEventHookProvider` invokes a shell script that has been written by a user and placed on a file system that is accessible +by the NiFi Registry instance that the provider is configured for. + +.... + + org.apache.nifi.registry.provider.hook.ScriptEventHookProvider + + + + CREATE_FLOW + UPDATE_FLOW + +.... + +[options="header,footer"] +|================================================================================================================================================== +| Property Name | Description +|`Script Path` | Full path to a script that will executed for each event. The arguments to the script will be the event fields in the order they are specified for the given event type. +|`Working Directory` | Working directory from where the commands will be executed. +|================================================================================================================================================== + +=== LoggingEventHookProvider +The `LoggingEventHookProvider` logs a string representation of each event using an SLF4J logger. The logger can be configured +via NiFi Registry’s _logback.xml_, which by default contains an appender that writes to a log file named _nifi-registry-event.log_ in the `logs` directory. + +.... + + org.apache.nifi.registry.provider.hook.LoggingEventHookProvider + +.... + +== URL Aliasing +A versioned item may contain the URL of a registry instance embedded in the content of the item. For example, flows with nested versioning contain the URL of the registry where the nested versioned flow is located. If the location of the registry instances changes, then the content is no longer accurate. + +URL aliasing can be used to dynamically handle this situation so that URLs are never written to the stored content, and can be re-written with the correct value when being retrieved by a client. + +The aliases are configured in an XML file which can be specified in _<>_. + +.Example aliases in registry-aliases.xml +[source,xml] +.... + + + NIFI_REGISTRY_1 + http://registry1.nifi.apache.org:18080 + + + NIFI_REGISTRY_2 + http://registry2.nifi.apache.org:18080 + + +.... + +If a flow is saved to registry with two child process groups, each under version control, the incoming flow would contain something like the following: +.... +"processGroups" : [ { + ... + "versionedFlowCoordinates" : { + "bucketId" : "ca20e058-f6e7-404c-aee0-e30833e792c7", + "flowId" : "178a6657-e1a7-4cce-8f83-4e615e38f57a", + "registryUrl" : "http://registry1.nifi.apache.org:18080", + "version" : 1 + }, + { + ... + "versionedFlowCoordinates" : { + "bucketId" : "ca20e058-f6e7-404c-aee0-e30833e792c7", + "flowId" : "985cb44b-3aec-32be-860f-d2a0f2c72aac", + "registryUrl" : "http://registry2.nifi.apache.org:18080", + "version" : 1 + } +] +.... + +With the example aliases configuration above, the URLs would be written to the flow persistence provider as the following: +.... +"processGroups" : [ { + ... + "versionedFlowCoordinates" : { + "bucketId" : "ca20e058-f6e7-404c-aee0-e30833e792c7", + "flowId" : "178a6657-e1a7-4cce-8f83-4e615e38f57a", + "registryUrl" : "NIFI_REGISTRY_1", + "version" : 1 + }, + { + ... + "versionedFlowCoordinates" : { + "bucketId" : "ca20e058-f6e7-404c-aee0-e30833e792c7", + "flowId" : "985cb44b-3aec-32be-860f-d2a0f2c72aac", + "registryUrl" : "NIFI_REGISTRY_2", + "version" : 1 + } +] +.... + +When this flow is retrieved from any API call, the internal values would be rewritten to the external values. + +== Backup & Recovery + +In order to prevent data loss it is important to consider backup and recovery options. The data that needs to be considered is the following: + + - Metadata Database + - Persistence providers + - Configuration files + +=== Metadata Database + +If using H2, the database file should be backed up periodically to an external location. In order to ensure a proper backup, NiFi Registry should be stopped to ensure no write operations are occurring while copying the file. + +If using Postgres, backups may be taken on the Postgres database, or Postgres may be configured for high availability such that there is a failover or backup instance. + +If starting a brand new NiFi Registry instance, the metadata database can be automatically rebuilt from the information in the `GitFlowPersistenceProvider`. This is a one-time operation during the first start of the application, and is not meant to keep the DB in sync with external changes made in Git. This feature only applies to flows and would not be able to restore information about extension bundles. + +=== Persistence Providers + +Each persistence provider may have its own option for backup & recovery. + +==== Flow Persistence + +If using the `FileSystemFlowPersistenceProvider`, the directory where flows are stored should be backed up periodically to an external location. In order to ensure a proper backup, NiFi Registry should be stopped to ensure no flows are being written to disk. If using H2 for metadata, H2 should be backed up at the same time to ensure consistency between the flows on disk and the contents in H2. + +If using the `GitFlowPersistenceProvider`, the ability to automatically push to a remote may be configured. This provides an automatic backup of the data in the remote repo. + +=== Bundle Persistence + +If using the `FileSystemBundlePersistenceProvider`, the directory where bundles are stored should be backed up periodically to an external location. In order to ensure a proper backup, NiFi Registry should be stopped to ensure no bundles are being written to disk. If using H2 for metadata, H2 should be backed up at the same time to ensure consistency between the bundles on disk and the contents in H2. + +If using the `S3BundlePersistenceProvider`, data will be stored remotely and automatically replicated. + +=== Configuration Files + +If using NiFi Registry's policy based authorization, the users, groups, and policies are stored in files on disk named _users.xml_ and _authorizations.xml_. These files should be periodically backed up to an external location. In order to ensure a proper backup, NiFi Registry should be stopped to ensure no authorization data is being written to disk. + +If using Ranger, then all authorization information is stored externally and there is nothing to back up. diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/asciidoc-mod.css b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/asciidoc-mod.css new file mode 100644 index 0000000000..4b6fd49f70 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/asciidoc-mod.css @@ -0,0 +1,418 @@ +/* Asciidoctor default stylesheet | MIT License | https://asciidoctor.org */ +/* Copyright (C) 2012-2015 Dan Allen, Ryan Waldron and the Asciidoctor Project + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ +/* Remove the comments around the @import statement below when using this as a custom stylesheet */ +@import "https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400"; +article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block} +audio,canvas,video{display:inline-block} +audio:not([controls]){display:none;height:0} +[hidden],template{display:none} +script{display:none!important} +html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%} +body{margin:0} +a{background:transparent} +a:focus{outline:thin dotted} +a:active,a:hover{outline:0} +h1{font-size:2em;margin:.67em 0} +abbr[title]{border-bottom:1px dotted} +b,strong{font-weight:bold} +dfn{font-style:italic} +hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0} +mark{background:#ff0;color:#000} +code,kbd,pre,samp{font-family:monospace;font-size:1em} +pre{white-space:pre-wrap} +q{quotes:"\201C" "\201D" "\2018" "\2019"} +small{font-size:80%} +sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline} +sup{top:-.5em} +sub{bottom:-.25em} +img{border:0} +svg:not(:root){overflow:hidden} +figure{margin:0} +fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em} +legend{border:0;padding:0} +button,input,select,textarea{font-family:inherit;font-size:100%;margin:0} +button,input{line-height:normal} +button,select{text-transform:none} +button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer} +button[disabled],html input[disabled]{cursor:default} +input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0} +input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box} +input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none} +button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0} +textarea{overflow:auto;vertical-align:top} +table{border-collapse:collapse;border-spacing:0} +*,*:before,*:after{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box} +html,body{font-size:100%} +body{background:#fff;color:rgba(0,0,0,.8);padding:0;margin:0;font-family:"Noto Serif","DejaVu Serif",serif;font-weight:400;font-style:normal;line-height:1;position:relative;cursor:auto} +a:hover{cursor:pointer} +img,object,embed{max-width:100%;height:auto} +object,embed{height:100%} +img{-ms-interpolation-mode:bicubic} +#map_canvas img,#map_canvas embed,#map_canvas object,.map_canvas img,.map_canvas embed,.map_canvas object{max-width:none!important} +.left{float:left!important} +.right{float:right!important} +.text-left{text-align:left!important} +.text-right{text-align:right!important} +.text-center{text-align:center!important} +.text-justify{text-align:justify!important} +.hide{display:none} +.antialiased,body{-webkit-font-smoothing:antialiased} +img{display:inline-block;vertical-align:middle} +textarea{height:auto;min-height:50px} +select{width:100%} +p.lead,.paragraph.lead>p,#preamble>.sectionbody>.paragraph:first-of-type p{font-size:1.21875em;line-height:1.6} +.subheader,.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{line-height:1.45;color:#7a2518;font-weight:400;margin-top:0;margin-bottom:.25em} +div,dl,dt,dd,ul,ol,li,h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6,pre,form,p,blockquote,th,td{margin:0;padding:0;direction:ltr} +a{color:#2156a5;text-decoration:underline;line-height:inherit} +a:hover,a:focus{color:#1d4b8f} +a img{border:none} +p{font-family:inherit;font-weight:400;font-size:1em;line-height:1.6;margin-bottom:1.25em;text-rendering:optimizeLegibility} +p aside{font-size:.875em;line-height:1.35;font-style:italic} +h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{font-family:"Open Sans","DejaVu Sans",sans-serif;font-weight:300;font-style:normal;color:#ba3925;text-rendering:optimizeLegibility;margin-top:1em;margin-bottom:.5em;line-height:1.0125em} +h1 small,h2 small,h3 small,#toctitle small,.sidebarblock>.content>.title small,h4 small,h5 small,h6 small{font-size:60%;color:#e99b8f;line-height:0} +h1{font-size:2.125em} +h2{font-size:1.6875em} +h3,#toctitle,.sidebarblock>.content>.title{font-size:1.375em} +h4,h5{font-size:1.125em} +h6{font-size:1em} +hr{border:solid #ddddd8;border-width:1px 0 0;clear:both;margin:1.25em 0 1.1875em;height:0} +em,i{font-style:italic;line-height:inherit} +strong,b{font-weight:bold;line-height:inherit} +small{font-size:60%;line-height:inherit} +code{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;color:rgba(0,0,0,.9);padding-right: 1px;} +ul,ol,dl{font-size:1em;line-height:1.6;margin-bottom:1.25em;list-style-position:outside;font-family:inherit} +ul,ol,ul.no-bullet,ol.no-bullet{margin-left:1.5em} +ul li ul,ul li ol{margin-left:1.25em;margin-bottom:0;font-size:1em} +ul.square li ul,ul.circle li ul,ul.disc li ul{list-style:inherit} +ul.square{list-style-type:square} +ul.circle{list-style-type:circle} +ul.disc{list-style-type:disc} +ul.no-bullet{list-style:none} +ol li ul,ol li ol{margin-left:1.25em;margin-bottom:0} +dl dt{margin-bottom:.3125em;font-weight:bold} +dl dd{margin-bottom:1.25em} +abbr,acronym{text-transform:uppercase;font-size:90%;color:rgba(0,0,0,.8);border-bottom:1px dotted #ddd;cursor:help} +abbr{text-transform:none} +blockquote{margin:0 0 1.25em;padding:.5625em 1.25em 0 1.1875em;border-left:1px solid #ddd} +blockquote cite{display:block;font-size:.9375em;color:rgba(0,0,0,.6)} +blockquote cite:before{content:"\2014 \0020"} +blockquote cite a,blockquote cite a:visited{color:rgba(0,0,0,.6)} +blockquote,blockquote p{line-height:1.6;color:rgba(0,0,0,.85)} +@media only screen and (min-width:768px){h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2} +h1{font-size:2.75em} +h2{font-size:2.3125em} +h3,#toctitle,.sidebarblock>.content>.title{font-size:1.6875em} +h4{font-size:1.4375em}}table{background:#fff;margin-bottom:1.25em;border:solid 1px #dedede} +table thead,table tfoot{background:#f7f8f7;font-weight:bold} +table thead tr th,table thead tr td,table tfoot tr th,table tfoot tr td{padding:.5em .625em .625em;font-size:inherit;color:rgba(0,0,0,.8);text-align:left} +table tr th,table tr td{padding:.5625em .625em;font-size:inherit;color:rgba(0,0,0,.8)} +table tr.even,table tr.alt,table tr:nth-of-type(even){background:#f8f8f7} +table thead tr th,table tfoot tr th,table tbody tr td,table tr td,table tfoot tr td{display:table-cell;line-height:1.6} +h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2;word-spacing:-.05em} +h1 strong,h2 strong,h3 strong,#toctitle strong,.sidebarblock>.content>.title strong,h4 strong,h5 strong,h6 strong{font-weight:400} +.clearfix:before,.clearfix:after,.float-group:before,.float-group:after{content:" ";display:table} +.clearfix:after,.float-group:after{clear:both} +*:not(pre)>code{font-size:.9375em;font-style:normal!important;letter-spacing:0;word-spacing:-.15em;background-color:#f7f7f8;-webkit-border-radius:4px;border-radius:4px;line-height:1.45;text-rendering:optimizeSpeed} +pre,pre>code{line-height:1.45;color:rgba(0,0,0,.9);font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;text-rendering:optimizeSpeed} +.keyseq{color:rgba(51,51,51,.8)} +kbd{display:inline-block;color:rgba(0,0,0,.8);font-size:.75em;line-height:1.4;background-color:#f7f7f7;border:1px solid #ccc;-webkit-border-radius:3px;border-radius:3px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,.2),0 0 0 .1em white inset;box-shadow:0 1px 0 rgba(0,0,0,.2),0 0 0 .1em #fff inset;margin:-.15em .15em 0 .15em;padding:.2em .6em .2em .5em;vertical-align:middle;white-space:nowrap} +.keyseq kbd:first-child{margin-left:0} +.keyseq kbd:last-child{margin-right:0} +.menuseq,.menu{color:rgba(0,0,0,.8)} +b.button:before,b.button:after{position:relative;top:-1px;font-weight:400} +b.button:before{content:"[";padding:0 3px 0 2px} +b.button:after{content:"]";padding:0 2px 0 3px} +p a>code:hover{color:rgba(0,0,0,.9)} +#header,#content,#footnotes,#footer{width:100%;margin-left:auto;margin-right:auto;margin-top:0;margin-bottom:0;max-width:62.5em;*zoom:1;position:relative;padding-left:.9375em;padding-right:.9375em} +#header:before,#header:after,#content:before,#content:after,#footnotes:before,#footnotes:after,#footer:before,#footer:after{content:" ";display:table} +#header:after,#content:after,#footnotes:after,#footer:after{clear:both} +#content{margin-top:1.25em} +#content:before{content:none} +#header>h1:first-child{color:rgba(0,0,0,.85);margin-top:2.25rem;margin-bottom:0} +#header>h1:first-child+#toc{margin-top:8px;border-top:1px solid #ddddd8} +#header>h1:only-child,body.toc2 #header>h1:nth-last-child(2){border-bottom:1px solid #ddddd8;padding-bottom:8px} +#header .details{border-bottom:1px solid #ddddd8;line-height:1.45;padding-top:.25em;padding-bottom:.25em;padding-left:.25em;color:rgba(0,0,0,.6);display:-ms-flexbox;display:-webkit-flex;display:flex;-ms-flex-flow:row wrap;-webkit-flex-flow:row wrap;flex-flow:row wrap} +#header .details span:first-child{margin-left:-.125em} +#header .details span.email a{color:rgba(0,0,0,.85)} +#header .details br{display:none} +#header .details br+span:before{content:"\00a0\2013\00a0"} +#header .details br+span.author:before{content:"\00a0\22c5\00a0";color:rgba(0,0,0,.85)} +#header .details br+span#revremark:before{content:"\00a0|\00a0"} +#header #revnumber{text-transform:capitalize} +#header #revnumber:after{content:"\00a0"} +#content>h1:first-child:not([class]){color:rgba(0,0,0,.85);border-bottom:1px solid #ddddd8;padding-bottom:8px;margin-top:0;padding-top:1rem;margin-bottom:1.25rem} +#toc{border-bottom:1px solid #efefed;padding-bottom:.5em} +#toc>ul{margin-left:.125em} +#toc ul.sectlevel0>li>a{font-style:italic} +#toc ul.sectlevel0 ul.sectlevel1{margin:.5em 0} +#toc ul{font-family:"Open Sans","DejaVu Sans",sans-serif;list-style-type:none} +#toc a{text-decoration:none} +#toc a:active{text-decoration:underline} +#toctitle{color:#7a2518;font-size:1.2em} +@media only screen and (min-width:768px){#toctitle{font-size:1.375em} +body.toc2{padding-left:15em;padding-right:0} +#toc.toc2{margin-top:0!important;background-color:#f8f8f7;position:fixed;width:15em;left:0;top:0;border-right:1px solid #efefed;border-top-width:0!important;border-bottom-width:0!important;z-index:1000;padding:1.25em 1em;height:100%;overflow:auto} +#toc.toc2 #toctitle{margin-top:0;font-size:1.2em} +#toc.toc2>ul{font-size:.9em;margin-bottom:0} +#toc.toc2 ul ul{margin-left:0;padding-left:1em} +#toc.toc2 ul.sectlevel0 ul.sectlevel1{padding-left:0;margin-top:.5em;margin-bottom:.5em} +body.toc2.toc-right{padding-left:0;padding-right:15em} +body.toc2.toc-right #toc.toc2{border-right-width:0;border-left:1px solid #efefed;left:auto;right:0}}@media only screen and (min-width:1280px){body.toc2{padding-left:20em;padding-right:0} +#toc.toc2{width:20em} +#toc.toc2 #toctitle{font-size:1.375em} +#toc.toc2>ul{font-size:.95em} +#toc.toc2 ul ul{padding-left:1.25em} +body.toc2.toc-right{padding-left:0;padding-right:20em}}#content #toc{border-style:solid;border-width:1px;border-color:#e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;-webkit-border-radius:4px;border-radius:4px} +#content #toc>:first-child{margin-top:0} +#content #toc>:last-child{margin-bottom:0} +#footer{max-width:100%;background-color:rgba(0,0,0,.8);padding:1.25em} +#footer-text{color:rgba(255,255,255,.8);line-height:1.44} +.sect1{padding-bottom:.625em} +@media only screen and (min-width:768px){.sect1{padding-bottom:1.25em}}.sect1+.sect1{border-top:1px solid #efefed} +#content h1>a.anchor,h2>a.anchor,h3>a.anchor,#toctitle>a.anchor,.sidebarblock>.content>.title>a.anchor,h4>a.anchor,h5>a.anchor,h6>a.anchor{position:absolute;z-index:1001;width:1.5ex;margin-left:-1.5ex;display:block;text-decoration:none!important;visibility:hidden;text-align:center;font-weight:400} +#content h1>a.anchor:before,h2>a.anchor:before,h3>a.anchor:before,#toctitle>a.anchor:before,.sidebarblock>.content>.title>a.anchor:before,h4>a.anchor:before,h5>a.anchor:before,h6>a.anchor:before{content:"\00A7";font-size:.85em;display:block;padding-top:.1em} +#content h1:hover>a.anchor,#content h1>a.anchor:hover,h2:hover>a.anchor,h2>a.anchor:hover,h3:hover>a.anchor,#toctitle:hover>a.anchor,.sidebarblock>.content>.title:hover>a.anchor,h3>a.anchor:hover,#toctitle>a.anchor:hover,.sidebarblock>.content>.title>a.anchor:hover,h4:hover>a.anchor,h4>a.anchor:hover,h5:hover>a.anchor,h5>a.anchor:hover,h6:hover>a.anchor,h6>a.anchor:hover{visibility:visible} +#content h1>a.link,h2>a.link,h3>a.link,#toctitle>a.link,.sidebarblock>.content>.title>a.link,h4>a.link,h5>a.link,h6>a.link{color:#ba3925;text-decoration:none} +#content h1>a.link:hover,h2>a.link:hover,h3>a.link:hover,#toctitle>a.link:hover,.sidebarblock>.content>.title>a.link:hover,h4>a.link:hover,h5>a.link:hover,h6>a.link:hover{color:#a53221} +.audioblock,.imageblock,.literalblock,.listingblock,.stemblock,.videoblock{margin-bottom:1.25em} +.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{text-rendering:optimizeLegibility;text-align:left;font-family:"Noto Serif","DejaVu Serif",serif;font-size:1rem;font-style:italic} +table.tableblock>caption.title{white-space:nowrap;overflow:visible;max-width:0} +.paragraph.lead>p,#preamble>.sectionbody>.paragraph:first-of-type p{color:rgba(0,0,0,.85)} +table.tableblock #preamble>.sectionbody>.paragraph:first-of-type p{font-size:inherit} +.admonitionblock>table{border-collapse:separate;border:0;background:none;width:100%} +.admonitionblock>table td.icon{text-align:center;width:80px} +.admonitionblock>table td.icon img{max-width:none} +.admonitionblock>table td.icon .title{font-weight:bold;font-family:"Open Sans","DejaVu Sans",sans-serif;text-transform:uppercase} +.admonitionblock>table td.content{padding-left:1.125em;padding-right:1.25em;border-left:1px solid #ddddd8;color:rgba(0,0,0,.6)} +.admonitionblock>table td.content>:last-child>:last-child{margin-bottom:0} +.exampleblock>.content{border-style:solid;border-width:1px;border-color:#e6e6e6;margin-bottom:1.25em;padding:1.25em;background:#fff;-webkit-border-radius:4px;border-radius:4px} +.exampleblock>.content>:first-child{margin-top:0} +.exampleblock>.content>:last-child{margin-bottom:0} +.sidebarblock{border-style:solid;border-width:1px;border-color:#e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;-webkit-border-radius:4px;border-radius:4px} +.sidebarblock>:first-child{margin-top:0} +.sidebarblock>:last-child{margin-bottom:0} +.sidebarblock>.content>.title{color:#7a2518;margin-top:0;text-align:center} +.exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0} +.literalblock pre,.listingblock pre:not(.highlight),.listingblock pre[class="highlight"],.listingblock pre[class^="highlight "],.listingblock pre.CodeRay,.listingblock pre.prettyprint{background:#f7f7f8} +.sidebarblock .literalblock pre,.sidebarblock .listingblock pre:not(.highlight),.sidebarblock .listingblock pre[class="highlight"],.sidebarblock .listingblock pre[class^="highlight "],.sidebarblock .listingblock pre.CodeRay,.sidebarblock .listingblock pre.prettyprint{background:#f2f1f1} +.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{-webkit-border-radius:4px;border-radius:4px;word-wrap:break-word;padding:1em;font-size:.8125em} +.literalblock pre.nowrap,.literalblock pre[class].nowrap,.listingblock pre.nowrap,.listingblock pre[class].nowrap{overflow-x:auto;white-space:pre;word-wrap:normal} +@media only screen and (min-width:768px){.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{font-size:.90625em}}@media only screen and (min-width:1280px){.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{font-size:1em}}.literalblock.output pre{color:#f7f7f8;background-color:rgba(0,0,0,.9)} +.listingblock pre.highlightjs{padding:0} +.listingblock pre.highlightjs>code{padding:1em;-webkit-border-radius:4px;border-radius:4px} +.listingblock pre.prettyprint{border-width:0} +.listingblock>.content{position:relative} +.listingblock code[data-lang]:before{display:none;content:attr(data-lang);position:absolute;font-size:.75em;top:.425rem;right:.5rem;line-height:1;text-transform:uppercase;color:#999} +.listingblock:hover code[data-lang]:before{display:block} +.listingblock.terminal pre .command:before{content:attr(data-prompt);padding-right:.5em;color:#999} +.listingblock.terminal pre .command:not([data-prompt]):before{content:"$"} +table.pyhltable{border-collapse:separate;border:0;margin-bottom:0;background:none} +table.pyhltable td{vertical-align:top;padding-top:0;padding-bottom:0} +table.pyhltable td.code{padding-left:.75em;padding-right:0} +pre.pygments .lineno,table.pyhltable td:not(.code){color:#999;padding-left:0;padding-right:.5em;border-right:1px solid #ddddd8} +pre.pygments .lineno{display:inline-block;margin-right:.25em} +table.pyhltable .linenodiv{background:none!important;padding-right:0!important} +.quoteblock{margin:0 1em 1.25em 1.5em;display:table} +.quoteblock>.title{margin-left:-1.5em;margin-bottom:.75em} +.quoteblock blockquote,.quoteblock blockquote p{color:rgba(0,0,0,.85);font-size:1.15rem;line-height:1.75;word-spacing:.1em;letter-spacing:0;font-style:italic;text-align:justify} +.quoteblock blockquote{margin:0;padding:0;border:0} +.quoteblock blockquote:before{content:"\201c";float:left;font-size:2.75em;font-weight:bold;line-height:.6em;margin-left:-.6em;color:#7a2518;text-shadow:0 1px 2px rgba(0,0,0,.1)} +.quoteblock blockquote>.paragraph:last-child p{margin-bottom:0} +.quoteblock .attribution{margin-top:.5em;margin-right:.5ex;text-align:right} +.quoteblock .quoteblock{margin-left:0;margin-right:0;padding:.5em 0;border-left:3px solid rgba(0,0,0,.6)} +.quoteblock .quoteblock blockquote{padding:0 0 0 .75em} +.quoteblock .quoteblock blockquote:before{display:none} +.verseblock{margin:0 1em 1.25em 1em} +.verseblock pre{font-family:"Open Sans","DejaVu Sans",sans;font-size:1.15rem;color:rgba(0,0,0,.85);font-weight:300;text-rendering:optimizeLegibility} +.verseblock pre strong{font-weight:400} +.verseblock .attribution{margin-top:1.25rem;margin-left:.5ex} +.quoteblock .attribution,.verseblock .attribution{font-size:.9375em;line-height:1.45;font-style:italic} +.quoteblock .attribution br,.verseblock .attribution br{display:none} +.quoteblock .attribution cite,.verseblock .attribution cite{display:block;letter-spacing:-.05em;color:rgba(0,0,0,.6)} +.quoteblock.abstract{margin:0 0 1.25em 0;display:block} +.quoteblock.abstract blockquote,.quoteblock.abstract blockquote p{text-align:left;word-spacing:0} +.quoteblock.abstract blockquote:before,.quoteblock.abstract blockquote p:first-of-type:before{display:none} +table.tableblock{max-width:100%;border-collapse:separate} +table.tableblock td>.paragraph:last-child p>p:last-child,table.tableblock th>p:last-child,table.tableblock td>p:last-child{margin-bottom:0} +table.spread{width:100%} +table.tableblock,th.tableblock,td.tableblock{border:0 solid #dedede} +table.grid-all th.tableblock,table.grid-all td.tableblock{border-width:0 1px 1px 0} +table.grid-all tfoot>tr>th.tableblock,table.grid-all tfoot>tr>td.tableblock{border-width:1px 1px 0 0} +table.grid-cols th.tableblock,table.grid-cols td.tableblock{border-width:0 1px 0 0} +table.grid-all *>tr>.tableblock:last-child,table.grid-cols *>tr>.tableblock:last-child{border-right-width:0} +table.grid-rows th.tableblock,table.grid-rows td.tableblock{border-width:0 0 1px 0} +table.grid-all tbody>tr:last-child>th.tableblock,table.grid-all tbody>tr:last-child>td.tableblock,table.grid-all thead:last-child>tr>th.tableblock,table.grid-rows tbody>tr:last-child>th.tableblock,table.grid-rows tbody>tr:last-child>td.tableblock,table.grid-rows thead:last-child>tr>th.tableblock{border-bottom-width:0} +table.grid-rows tfoot>tr>th.tableblock,table.grid-rows tfoot>tr>td.tableblock{border-width:1px 0 0 0} +table.frame-all{border-width:1px} +table.frame-sides{border-width:0 1px} +table.frame-topbot{border-width:1px 0} +th.halign-left,td.halign-left{text-align:left} +th.halign-right,td.halign-right{text-align:right} +th.halign-center,td.halign-center{text-align:center} +th.valign-top,td.valign-top{vertical-align:top} +th.valign-bottom,td.valign-bottom{vertical-align:bottom} +th.valign-middle,td.valign-middle{vertical-align:middle} +table thead th,table tfoot th{font-weight:bold} +tbody tr th{display:table-cell;line-height:1.6;background:#f7f8f7} +tbody tr th,tbody tr th p,tfoot tr th,tfoot tr th p{color:rgba(0,0,0,.8);font-weight:bold} +p.tableblock>code:only-child{background:none;padding:0} +p.tableblock{font-size:1em} +td>div.verse{white-space:pre} +ol{margin-left:1.75em} +ul li ol{margin-left:1.5em} +dl dd{margin-left:1.125em} +dl dd:last-child,dl dd:last-child>:last-child{margin-bottom:0} +ol>li p,ul>li p,ul dd,ol dd,.olist .olist,.ulist .ulist,.ulist .olist,.olist .ulist{margin-bottom:.625em} +ul.unstyled,ol.unnumbered,ul.checklist,ul.none{list-style-type:none} +ul.unstyled,ol.unnumbered,ul.checklist{margin-left:.625em} +ul.checklist li>p:first-child>.fa-square-o:first-child,ul.checklist li>p:first-child>.fa-check-square-o:first-child{width:1em;font-size:.85em} +ul.checklist li>p:first-child>input[type="checkbox"]:first-child{width:1em;position:relative;top:1px} +ul.inline{margin:0 auto .625em auto;margin-left:-1.375em;margin-right:0;padding:0;list-style:none;overflow:hidden} +ul.inline>li{list-style:none;float:left;margin-left:1.375em;display:block} +ul.inline>li>*{display:block} +.unstyled dl dt{font-weight:400;font-style:normal} +ol.arabic{list-style-type:decimal} +ol.decimal{list-style-type:decimal-leading-zero} +ol.loweralpha{list-style-type:lower-alpha} +ol.upperalpha{list-style-type:upper-alpha} +ol.lowerroman{list-style-type:lower-roman} +ol.upperroman{list-style-type:upper-roman} +ol.lowergreek{list-style-type:lower-greek} +.hdlist>table,.colist>table{border:0;background:none} +.hdlist>table>tbody>tr,.colist>table>tbody>tr{background:none} +td.hdlist1{padding-right:.75em;font-weight:bold} +td.hdlist1,td.hdlist2{vertical-align:top} +.literalblock+.colist,.listingblock+.colist{margin-top:-.5em} +.colist>table tr>td:first-of-type{padding:0 .75em;line-height:1} +.colist>table tr>td:last-of-type{padding:.25em 0} +.thumb,.th{line-height:0;display:inline-block;border:solid 4px #fff;-webkit-box-shadow:0 0 0 1px #ddd;box-shadow:0 0 0 1px #ddd} +.imageblock.left,.imageblock[style*="float: left"]{margin:.25em .625em 1.25em 0} +.imageblock.right,.imageblock[style*="float: right"]{margin:.25em 0 1.25em .625em} +.imageblock>.title{margin-bottom:0} +.imageblock.thumb,.imageblock.th{border-width:6px} +.imageblock.thumb>.title,.imageblock.th>.title{padding:0 .125em} +.image.left,.image.right{margin-top:.25em;margin-bottom:.25em;display:inline-block;line-height:0} +.image.left{margin-right:.625em} +.image.right{margin-left:.625em} +a.image{text-decoration:none} +span.footnote,span.footnoteref{vertical-align:super;font-size:.875em} +span.footnote a,span.footnoteref a{text-decoration:none} +span.footnote a:active,span.footnoteref a:active{text-decoration:underline} +#footnotes{padding-top:.75em;padding-bottom:.75em;margin-bottom:.625em} +#footnotes hr{width:20%;min-width:6.25em;margin:-.25em 0 .75em 0;border-width:1px 0 0 0} +#footnotes .footnote{padding:0 .375em;line-height:1.3;font-size:.875em;margin-left:1.2em;text-indent:-1.2em;margin-bottom:.2em} +#footnotes .footnote a:first-of-type{font-weight:bold;text-decoration:none} +#footnotes .footnote:last-of-type{margin-bottom:0} +#content #footnotes{margin-top:-.625em;margin-bottom:0;padding:.75em 0} +.gist .file-data>table{border:0;background:#fff;width:100%;margin-bottom:0} +.gist .file-data>table td.line-data{width:99%} +div.unbreakable{page-break-inside:avoid} +.big{font-size:larger} +.small{font-size:smaller} +.underline{text-decoration:underline} +.overline{text-decoration:overline} +.line-through{text-decoration:line-through} +.aqua{color:#00bfbf} +.aqua-background{background-color:#00fafa} +.black{color:#000} +.black-background{background-color:#000} +.blue{color:#0000bf} +.blue-background{background-color:#0000fa} +.fuchsia{color:#bf00bf} +.fuchsia-background{background-color:#fa00fa} +.gray{color:#606060} +.gray-background{background-color:#7d7d7d} +.green{color:#006000} +.green-background{background-color:#007d00} +.lime{color:#00bf00} +.lime-background{background-color:#00fa00} +.maroon{color:#600000} +.maroon-background{background-color:#7d0000} +.navy{color:#000060} +.navy-background{background-color:#00007d} +.olive{color:#606000} +.olive-background{background-color:#7d7d00} +.purple{color:#600060} +.purple-background{background-color:#7d007d} +.red{color:#bf0000} +.red-background{background-color:#fa0000} +.silver{color:#909090} +.silver-background{background-color:#bcbcbc} +.teal{color:#006060} +.teal-background{background-color:#007d7d} +.white{color:#bfbfbf} +.white-background{background-color:#fafafa} +.yellow{color:#bfbf00} +.yellow-background{background-color:#fafa00} +span.icon>.fa{cursor:default} +.admonitionblock td.icon [class^="fa icon-"]{font-size:2.5em;text-shadow:1px 1px 2px rgba(0,0,0,.5);cursor:default} +.admonitionblock td.icon .icon-note:before{content:"\f05a";color:#19407c} +.admonitionblock td.icon .icon-tip:before{content:"\f0eb";text-shadow:1px 1px 2px rgba(155,155,0,.8);color:#111} +.admonitionblock td.icon .icon-warning:before{content:"\f071";color:#bf6900} +.admonitionblock td.icon .icon-caution:before{content:"\f06d";color:#bf3400} +.admonitionblock td.icon .icon-important:before{content:"\f06a";color:#bf0000} +.conum[data-value]{display:inline-block;color:#fff!important;background-color:rgba(0,0,0,.8);-webkit-border-radius:100px;border-radius:100px;text-align:center;font-size:.75em;width:1.67em;height:1.67em;line-height:1.67em;font-family:"Open Sans","DejaVu Sans",sans-serif;font-style:normal;font-weight:bold} +.conum[data-value] *{color:#fff!important} +.conum[data-value]+b{display:none} +.conum[data-value]:after{content:attr(data-value)} +pre .conum[data-value]{position:relative;top:-.125em} +b.conum *{color:inherit!important} +.conum:not([data-value]):empty{display:none} +h1,h2{letter-spacing:-.01em} +dt,th.tableblock,td.content{text-rendering:optimizeLegibility} +p,td.content{letter-spacing:-.01em} +p strong,td.content strong{letter-spacing:-.005em} +p,blockquote,dt,td.content{font-size:1.0625rem} +p{margin-bottom:1.25rem} +.sidebarblock p,.sidebarblock dt,.sidebarblock td.content,p.tableblock{font-size:1em} +.exampleblock>.content{background-color:#fffef7;border-color:#e0e0dc;-webkit-box-shadow:0 1px 4px #e0e0dc;box-shadow:0 1px 4px #e0e0dc} +.print-only{display:none!important} +@media print{@page{margin:1.25cm .75cm} +*{-webkit-box-shadow:none!important;box-shadow:none!important;text-shadow:none!important} +a{color:inherit!important;text-decoration:underline!important} +a.bare,a[href^="#"],a[href^="mailto:"]{text-decoration:none!important} +a[href^="http:"]:not(.bare):after,a[href^="https:"]:not(.bare):after{content:"(" attr(href) ")";display:inline-block;font-size:.875em;padding-left:.25em} +abbr[title]:after{content:" (" attr(title) ")"} +pre,blockquote,tr,img{page-break-inside:avoid} +thead{display:table-header-group} +img{max-width:100%!important} +p,blockquote,dt,td.content{font-size:1em;orphans:3;widows:3} +h2,h3,#toctitle,.sidebarblock>.content>.title{page-break-after:avoid} +#toc,.sidebarblock,.exampleblock>.content{background:none!important} +#toc{border-bottom:1px solid #ddddd8!important;padding-bottom:0!important} +.sect1{padding-bottom:0!important} +.sect1+.sect1{border:0!important} +#header>h1:first-child{margin-top:1.25rem} +body.book #header{text-align:center} +body.book #header>h1:first-child{border:0!important;margin:2.5em 0 1em 0} +body.book #header .details{border:0!important;display:block;padding:0!important} +body.book #header .details span:first-child{margin-left:0!important} +body.book #header .details br{display:block} +body.book #header .details br+span:before{content:none!important} +body.book #toc{border:0!important;text-align:left!important;padding:0!important;margin:0!important} +body.book #toc,body.book #preamble,body.book h1.sect0,body.book .sect1>h2{page-break-before:always} +.listingblock code[data-lang]:before{display:block} +#footer{background:none!important;padding:0 .9375em} +#footer-text{color:rgba(0,0,0,.6)!important;font-size:.9em} +.hide-on-print{display:none!important} +.print-only{display:block!important} +.hide-for-print{display:none!important} +.show-for-print{display:inherit!important}} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/getting-started.adoc b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/getting-started.adoc new file mode 100644 index 0000000000..6eb3ab3915 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/getting-started.adoc @@ -0,0 +1,171 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You 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. +// += Getting Started with Apache NiFi Registry +Apache NiFi Team +:homepage: https://nifi.apache.org +:linkattrs: + + +== Who is This Guide For? +This guide is written for users who have basic experience with NiFi but have little familiarity with the NiFi Registry. This guide is not intended to be an exhaustive instruction manual or a reference guide. The link:user-guide.html[NiFi Registry User Guide] and link:https://nifi.apache.org/docs/nifi-docs/html/user-guide.html[NiFi User Guide^] provide a great deal more information about using the Registry and integrating it with NiFi. This guide, in comparison, is intended to provide users with just the information needed in order to understand how to configure NiFi Registry, connect with NiFi and start using versioned NiFi dataflows. + + +== Terminology Used in This Guide +In order to talk about NiFi Registry, there are a few key terms that readers should be familiar with: + +*Flow*: A process group level NiFi dataflow that has been placed under version control and saved to the Registry. + +*Bucket*: A container that stores and organizes flows. + + +== Downloading and Installing NiFi Registry +NiFi Registry can be downloaded from the link:https://nifi.apache.org/registry.html[NiFi Registry Page^]. There are two packaging options available: a tarball and a zip file. Supported operating systems include Linux, Unix and Mac OS X. + +For users who are not running OS X, after downloading NiFi Registry simply extract the archive to the location that you wish to run the application from. The registry is unsecured by default. + +For information on how to configure an instance of NiFi Registry (for example, to implement security or change the port that NiFi Registry is running on), see the link:administration-guide.html[System Administrator's Guide]. + + +== Starting NiFi Registry +Once NiFi Registry has been downloaded and installed as described above, it can be started by using the mechanism appropriate for your operating system. + + +=== For Linux/Unix/Mac OS X users +Use a Terminal window to navigate to the directory where NiFi Registry was installed. To run NiFi Registry in the foreground, run `bin/nifi-registry.sh run`. This will leave the application running until the user presses Ctrl-C. At that time, it will initiate shutdown of the application. + +To run NiFi Registry in the background, instead run `bin/nifi-registry.sh start`. This will initiate the application to begin running. To check the status and see if NiFi Registry is currently running, execute the command `bin/nifi-registry.sh status`. +NiFi Registry can be shutdown by executing the command `bin/nifi-registry.sh stop`. + + +=== Installing as a Service +To install the application as a service, navigate to the installation directory in a Terminal window and execute the command `bin/nifi-registry.sh install` to install the service with the default name `nifi-registry`. To specify a custom name for the service, execute the command with an optional second argument that is the name of the service. For example, to install NiFi Registry as a service with the name `flow-registry`, use the command `bin/nifi-registry.sh install flow-registry`. + +Once installed, the service can be started and stopped using the appropriate commands, such as `sudo service nifi-registry start` and `sudo service nifi-registry stop`. Additionally, the running status can be checked via `sudo service nifi-registry status`. + + +== I Started NiFi Registry. Now What? +Now that NiFi Registry has been started, we can bring up the User Interface (UI). To get started, open a web browser and navigate to +link:http://localhost:18080/nifi-registry[`http://localhost:18080/nifi-registry`^]. The port can be changed by editing the `nifi-registry.properties` file in the NiFi Registry _conf_ directory, but the default port is `18080`. + +This will bring up the Registry UI, which at this point is empty as there are no flow resources available to share yet: + +image:empty_registry.png["Empty Registry"] + + +=== Create a Bucket +A bucket is needed in our registry to store and organize NiFi dataflows. To create one, select the Settings icon (image:iconSettings.png["Settings Icon"])in the top right corner of the screen. In the Buckets window, select the "New Bucket" button. + +image::new_test_bucket.png["New Bucket"] + +Enter the bucket name "Test" and select the "Create" button. + +image::test_bucket_dialog.png["Test Bucket Dialog"] + +The "Test" bucket is created: + +image:test_bucket.png["Test Bucket"] + +There are no permissions configured by default, so anyone is able to view, create and modify buckets in this instance. For information on securing the system, see the link:administration-guide.html[System Administrator's Guide]. + + +=== Connect NiFi to the Registry +Now it is time to tell NiFi about the local registry instance. + +Start a NiFi instance if one isn't already running and bring up the UI. Go to controller settings from the top-right menu: + +image::controller-settings-selection.png["Global Menu - Controller Settings"] + +Select the Registry Clients tab and add a new Registry Client giving it a name and the URL of link:http://localhost:18080[`http://localhost:18080`^]: + +image::local_registry.png["Local Registry Client"] + + +=== Start Version Control on a Process Group +With NiFi connected to a NiFi Registry, dataflows can be version controlled on the *process group level*. + +Right-click on a process group and select "Version->Start version control" from the context menu: + +image::ABCD_process_group_menu.png["ABCD Process Group Menu"] + +The local registry instance and "Test" bucket are chosen by default to store your flow since they are the only registry connected and bucket available. Enter a flow name, flow description, comments and select "Save": + +image::save_ABCD_flow_dialog.png["Initial Save of ABCD Flow"] + +As indicated by the Version State icon (image:iconUpToDate.png["Up To Date Icon"]) in the top left corner of the component, the process group is now saved as a versioned flow in the registry. + +image::ABCD_flow_saved.png["ABCD Flow Saved"] + +Go back to the Registry UI and return to the main page to see +the versioned flow you just saved (a refresh may be required): + +image::ABCD_flow_in_test_bucket.png["ABCD Flow in Test Bucket"] + + +=== Save Changes to a Versioned Flow +Changes made to the versioned process group can be reviewed, reverted or saved. + +For example, if changes are made to the ABCD flow, the Version State changes to "Locally modified" (image:iconLocallyModified.png["Locally Modified Icon"]). The right-click menu will now show the options "Commit local changes", "Show local changes" or "Revert local changes": + +image::changed_flow_options.png["Changed Flow Options"] + +Select "Show local changes" to see the details of the changes made: + +image::ABCD_flow_changes.png["Show ABCD Flow Changes"] + +Select "Commit local changes", enter comments and select "Save" to save the changes: + +image::ABCD_save_flow_version_2.png["Save ABCD Version 2"] + +Version 2 of the flow is saved: + +image::ABCD_version_2.png["ABCD Version 2"] + + +=== Import a Versioned Flow +With a flow existing in the registry, we can use it to illustrate how to import a versioned process group. + +In NiFi, select Process Group from the Components toolbar and drag it onto the canvas: + +image::drag_process_group.png["Drag Process Group"] + +Instead of entering a name, click the Import link: + +image::import_flow_from_registry.png["Import Flow From Registry"] + +Choose the version of the flow you want imported and select "Import": + +image:import_ABCD_version_2.png["Import ABCD Version 2"] + +A second identical PG is now added: + +image::two_ABCD_flows.png["Two ABCD Flow on Canvas"] + + +== Where To Go For More Information +In addition to this Getting Started Guide, more information about NiFi Registry and related features in NiFi can be found in the following guides: + +- link:user-guide.html[Apache NiFi Registry User Guide] - This guide provides information on how to navigate the Registry UI and explains in detail how to manage flows/policies/special privileges and configure users/groups when the Registry is secured. +- link:administration-guide.html[Apache NiFi Registry System Administrator's Guide] - A guide for setting up and administering Apache NiFi Registry. Topics covered include: system requirements, security configuration, user authentication, authorization, proxy configuration and details about the different system-level settings. +- link:https://nifi.apache.org/docs/nifi-docs/html/user-guide.html[Apache NiFi User Guide^] - A fairly extensive guide that is often used more as a Reference Guide, as it provides information on each of the different components available in NiFi and explains how to use the different features provided by the application. It includes the section link:https://nifi.apache.org/docs/nifi-docs/html/user-guide.html#versioning_dataflow["Versioning a Dataflow"] which covers the integration of NiFi with NiFi Registry. Topics covered include: connecting to a registry, version states, importing a versioned flow and managing local changes. +- link:https://cwiki.apache.org/confluence/display/NIFI/Contributor+Guide[Contributor's Guide^] - A guide for explaining how to contribute work back to the Apache NiFi community so that others can make use of it. + +In addition to the guides provided here, you can browse the different +link:https://nifi.apache.org/mailing_lists.html[NiFi Mailing Lists^] or send an e-mail to one of the mailing lists at +link:mailto:users@nifi.apache.org[users@nifi.apache.org] or +link:mailto:dev@nifi.apache.org[dev@nifi.apache.org]. + +Many of the members of the NiFi community are also available on Twitter and actively monitor for tweets that mention @apachenifi. diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_flow_changes.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_flow_changes.png new file mode 100644 index 0000000000..14b7d4d7d7 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_flow_changes.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_flow_in_test_bucket.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_flow_in_test_bucket.png new file mode 100644 index 0000000000..da65abbbd6 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_flow_in_test_bucket.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_flow_saved.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_flow_saved.png new file mode 100644 index 0000000000..3d1f714a78 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_flow_saved.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_process_group_menu.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_process_group_menu.png new file mode 100644 index 0000000000..ace96ca70d Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_process_group_menu.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_save_flow_version_2.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_save_flow_version_2.png new file mode 100644 index 0000000000..7f8c7722c1 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_save_flow_version_2.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_version_2.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_version_2.png new file mode 100644 index 0000000000..870ed02e77 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_version_2.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/add_user_button.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/add_user_button.png new file mode 100644 index 0000000000..69b29faa8f Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/add_user_button.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/add_user_dialog.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/add_user_dialog.png new file mode 100644 index 0000000000..0e1d6ea1c0 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/add_user_dialog.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/add_user_to_groups_dialog.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/add_user_to_groups_dialog.png new file mode 100644 index 0000000000..8890bbbbdd Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/add_user_to_groups_dialog.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/bucket_menu.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/bucket_menu.png new file mode 100644 index 0000000000..29a52dd1bb Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/bucket_menu.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/bucket_nav_allow_bundle_overwrite.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/bucket_nav_allow_bundle_overwrite.png new file mode 100644 index 0000000000..61cdb360ee Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/bucket_nav_allow_bundle_overwrite.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/bucket_nav_make_public.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/bucket_nav_make_public.png new file mode 100644 index 0000000000..95a45890f4 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/bucket_nav_make_public.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/bucket_nav_name_edit.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/bucket_nav_name_edit.png new file mode 100644 index 0000000000..249b37f531 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/bucket_nav_name_edit.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/buckets_filter_by_name.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/buckets_filter_by_name.png new file mode 100644 index 0000000000..ed1c65ae27 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/buckets_filter_by_name.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/buckets_sort_by_name.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/buckets_sort_by_name.png new file mode 100644 index 0000000000..4d40f9a17f Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/buckets_sort_by_name.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/changed_flow_options.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/changed_flow_options.png new file mode 100644 index 0000000000..88f6abbbe3 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/changed_flow_options.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/check_multiple_buckets.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/check_multiple_buckets.png new file mode 100644 index 0000000000..cf08d444fd Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/check_multiple_buckets.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/check_multiple_users.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/check_multiple_users.png new file mode 100644 index 0000000000..230b5b6cbe Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/check_multiple_users.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/controller-settings-selection.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/controller-settings-selection.png new file mode 100644 index 0000000000..80dca40619 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/controller-settings-selection.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/create_new_group.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/create_new_group.png new file mode 100644 index 0000000000..288c5886c7 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/create_new_group.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/create_new_group_dialog.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/create_new_group_dialog.png new file mode 100644 index 0000000000..f29fc9f472 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/create_new_group_dialog.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_bucket_dialog.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_bucket_dialog.png new file mode 100644 index 0000000000..7e43135204 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_bucket_dialog.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_bucket_policy.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_bucket_policy.png new file mode 100644 index 0000000000..1238f8d1c6 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_bucket_policy.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_bucket_policy_dialog.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_bucket_policy_dialog.png new file mode 100644 index 0000000000..6b6e237397 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_bucket_policy_dialog.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_bucket_single.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_bucket_single.png new file mode 100644 index 0000000000..9fc4a2a205 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_bucket_single.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_buckets_dialog.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_buckets_dialog.png new file mode 100644 index 0000000000..cc55a83c3a Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_buckets_dialog.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_multiple_buckets.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_multiple_buckets.png new file mode 100644 index 0000000000..8b5f9b2985 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_multiple_buckets.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_multiple_users.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_multiple_users.png new file mode 100644 index 0000000000..31f4ad22db Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_multiple_users.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_user_dialog.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_user_dialog.png new file mode 100644 index 0000000000..35f2253748 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_user_dialog.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_user_single.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_user_single.png new file mode 100644 index 0000000000..82599e461f Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_user_single.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_users_groups_dialog.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_users_groups_dialog.png new file mode 100644 index 0000000000..0c00989bb0 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_users_groups_dialog.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/drag_process_group.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/drag_process_group.png new file mode 100644 index 0000000000..d1876160bd Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/drag_process_group.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/empty_registry.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/empty_registry.png new file mode 100644 index 0000000000..e2959ae5f8 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/empty_registry.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flow_change_log.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flow_change_log.png new file mode 100644 index 0000000000..ece10bdea1 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flow_change_log.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flow_delete_action.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flow_delete_action.png new file mode 100644 index 0000000000..5ae40a9da7 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flow_delete_action.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flow_delete_confirm.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flow_delete_confirm.png new file mode 100644 index 0000000000..491f4d3510 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flow_delete_confirm.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flows_all.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flows_all.png new file mode 100644 index 0000000000..0faef8f9a9 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flows_all.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flows_filter_by_name.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flows_filter_by_name.png new file mode 100644 index 0000000000..5d53d73a1e Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flows_filter_by_name.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flows_sort_menu.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flows_sort_menu.png new file mode 100644 index 0000000000..517a762144 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flows_sort_menu.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/group_added.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/group_added.png new file mode 100644 index 0000000000..7627f6e341 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/group_added.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconDelete.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconDelete.png new file mode 100644 index 0000000000..cf4d048a0f Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconDelete.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconHelp.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconHelp.png new file mode 100644 index 0000000000..601ec421d9 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconHelp.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconLocallyModified.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconLocallyModified.png new file mode 100644 index 0000000000..4f72251904 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconLocallyModified.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconManage.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconManage.png new file mode 100644 index 0000000000..d70dedfaf3 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconManage.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconSettings.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconSettings.png new file mode 100644 index 0000000000..693255cdfd Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconSettings.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconUpToDate.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconUpToDate.png new file mode 100644 index 0000000000..78e52eba83 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconUpToDate.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/import_ABCD_version_2.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/import_ABCD_version_2.png new file mode 100644 index 0000000000..fa88678622 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/import_ABCD_version_2.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/import_flow_from_registry.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/import_flow_from_registry.png new file mode 100644 index 0000000000..c2fa67abf4 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/import_flow_from_registry.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/local_registry.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/local_registry.png new file mode 100644 index 0000000000..047dd71026 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/local_registry.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/loginRegistry.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/loginRegistry.png new file mode 100644 index 0000000000..9fa3f9d156 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/loginRegistry.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/manage_bucket.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/manage_bucket.png new file mode 100644 index 0000000000..e2159dacc4 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/manage_bucket.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/manage_user.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/manage_user.png new file mode 100644 index 0000000000..d8174c0a7d Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/manage_user.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_button.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_button.png new file mode 100644 index 0000000000..147d5d084e Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_button.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_dialog.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_dialog.png new file mode 100644 index 0000000000..ac0a41ea28 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_dialog.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_policy_added.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_policy_added.png new file mode 100644 index 0000000000..578ffd3600 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_policy_added.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_policy_create.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_policy_create.png new file mode 100644 index 0000000000..9a6aedd760 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_policy_create.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_policy_user_permission.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_policy_user_permission.png new file mode 100644 index 0000000000..3367009bbd Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_policy_user_permission.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_test_bucket.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_test_bucket.png new file mode 100644 index 0000000000..26aa4b1694 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_test_bucket.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/nifi-registry-components.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/nifi-registry-components.png new file mode 100644 index 0000000000..2225ab6dc6 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/nifi-registry-components.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/nifi_user1_template.snagproj b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/nifi_user1_template.snagproj new file mode 100644 index 0000000000..5fd34a8e0c Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/nifi_user1_template.snagproj differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/nifi_user_template.snagproj b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/nifi_user_template.snagproj new file mode 100644 index 0000000000..9aee06d606 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/nifi_user_template.snagproj differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/remove_group_from_user.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/remove_group_from_user.png new file mode 100644 index 0000000000..c1ccef33a2 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/remove_group_from_user.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/remove_user_from_group.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/remove_user_from_group.png new file mode 100644 index 0000000000..2270cae1d8 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/remove_user_from_group.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/save_ABCD_flow_dialog.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/save_ABCD_flow_dialog.png new file mode 100644 index 0000000000..0c20d2e550 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/save_ABCD_flow_dialog.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/select_users_create_new_group.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/select_users_create_new_group.png new file mode 100644 index 0000000000..36f8dd6d29 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/select_users_create_new_group.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/select_users_create_new_group_dialog.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/select_users_create_new_group_dialog.png new file mode 100644 index 0000000000..33f9142b45 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/select_users_create_new_group_dialog.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/select_users_new_group_added.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/select_users_new_group_added.png new file mode 100644 index 0000000000..b9dc4b4156 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/select_users_new_group_added.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/test_bucket.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/test_bucket.png new file mode 100644 index 0000000000..bfccd63602 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/test_bucket.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/test_bucket_dialog.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/test_bucket_dialog.png new file mode 100644 index 0000000000..0250717e6d Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/test_bucket_dialog.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/two_ABCD_flows.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/two_ABCD_flows.png new file mode 100644 index 0000000000..afec033a7e Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/two_ABCD_flows.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/user_nav_add_to_group.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/user_nav_add_to_group.png new file mode 100644 index 0000000000..6c958989cb Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/user_nav_add_to_group.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/user_nav_name_edit.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/user_nav_name_edit.png new file mode 100644 index 0000000000..006300ba14 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/user_nav_name_edit.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/user_special_privileges.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/user_special_privileges.png new file mode 100644 index 0000000000..2bf6410eac Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/user_special_privileges.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/users_filter_by_name.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/users_filter_by_name.png new file mode 100644 index 0000000000..7b994a3a9a Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/users_filter_by_name.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/users_non_configurable.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/users_non_configurable.png new file mode 100644 index 0000000000..f2500a183e Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/users_non_configurable.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/users_sort_by_name.png b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/users_sort_by_name.png new file mode 100644 index 0000000000..4548813e34 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/users_sort_by_name.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/user-guide.adoc b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/user-guide.adoc new file mode 100644 index 0000000000..69c79da9eb --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/user-guide.adoc @@ -0,0 +1,442 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You 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. +// += Apache NiFi Registry User Guide +Apache NiFi Team +:homepage: https://nifi.apache.org + + +== Introduction +Apache NiFi Registry—a subproject of Apache NiFi—is a complementary application that provides a central location for storage and management of shared resources across one or more instances of NiFi and/or MiNiFi. + +The first implementation of the Registry supports versioned flows. Process group level dataflows created in NiFi can be placed under version control and stored in a registry. The registry organizes where flows are stored and manages the permissions to access, create, modify or delete them. + +See the link:administration-guide.html[System Administrator’s Guide] for information about Registry system requirements, installation, and configuration. Once NiFi Registry is installed, use a supported web browser to view the UI. + + +== Browser Support +[options="header"] +|====================== +|Browser |Version +|Chrome |Current and Current - 1 +|FireFox |Current and Current - 1 +|Safari |Current and Current - 1 +|====================== + +Current and Current - 1 indicates that the UI is supported in the current stable release of that browser and the preceding one. For instance, if the current stable release is 62.X then the officially supported versions will be 62.X and 61.X. + +For Safari, which releases major versions much less frequently, Current and Current - 1 simply represent the two latest releases. + +The supported browser versions are driven by the capabilities the UI employs and the dependencies it uses. UI features will be developed and tested against the supported browsers. Any problem using a supported browser should be reported to Apache NiFi. + +=== Unsupported Browsers + +While the UI may run successfully in unsupported browsers, it is not actively tested against them. Additionally, the UI is designed as a desktop experience and is not currently supported in mobile browsers. + +=== Viewing the UI in Variably Sized Browsers +In most environments, all of the UI is visible in your browser. However, the UI has a responsive design that allows you to scroll through screens as needed, in smaller sized browsers or tablet environments. + +NOTE: The minimum recommended screen size is 1080px X 445px. + +== Terminology + +*Flow*: A process group level NiFi dataflow that has been placed under version control and saved to the Registry. + +*Bundle*: A binary artifact containing one or more extensions that can be run in NiFi or MiNiFi. + +*Bucket*: A container that stores and organizes versioned items, such as flows and bundles. + +*Policy*: Defines a user or group's ability to perform a given action. + + +[[User_Interface]] +== NiFi Registry User Interface + +The NiFi Registry UI displays the shared resources available and provides mechanisms for creating and administering users/groups, buckets and policies. + +When the application is started, the user is able to navigate to the UI by going to the default address of `http://:18080/nifi-registry` in a web browser. There are no permissions configured by default, so anyone is able to view and modify the flows and buckets. For information on securing the system, see the link:administration-guide.html[System Administrator’s Guide]. + +When an administrator navigates to the UI for the first time, the registry is empty as there are no flow resources available to share yet: + +image::nifi-registry-components.png["NiFi Registry Components"] + +The Buckets menu is available at the top left of the screen. It allows the user to display flows based on which bucket they are contained in. On the top right of the screen is the Settings button (image:iconSettings.png["Settings Icon"]) which accesses functionality for managing users, groups, buckets and policies. Next to the Settings button is the Help button (image:iconHelp.png["Help Icon"]) which accesses the NiFi Registry Documentation. + +[[logging-in]] +== Logging In + +If NiFi Registry is configured to run securely, users will have to be granted permissions to buckets by an administrator. For information on configuring NiFi Registry to run securely, see the link:administration-guide.html[System Administrator’s Guide]. + +If the user is logging in with their username/password they will be presented with a screen to do so. + +image::loginRegistry.png["NiFi Registry Login"] + + +== Manage Flows + +=== View a Flow +Flows in all buckets are listed in the main window of the UI by default. If the registry is secured, only the flows in the buckets that the user has access to are listed. + +image::flows_all.png["All Flows"] + +To see the flows in a particular bucket, select that bucket from the drop-down menu at the top left of the UI. + +image::bucket_menu.png["Bucket Menu"] + +Click on a flow to see its Description and Change Log: + +image::flow_change_log.png["Flow Change Log"] + +The Change Log includes all versions that were saved for a flow. Clicking on the version reveals details about when the version was saved, which user committed the save, and any comments entered by the user. + +==== Sorting & Filtering Flows +Flows can be sorted alphabetically by Name (ascending or descending) or by Update (newest or oldest) using the drop-down at the top right of the UI. + +image::flows_sort_menu.png["Flows Sort Menu"] + +The flow list can be filtered by: + +* flow name +* flow description +* flow ID +* bucket name +* bucket ID + +Here is an example filtering by flow name: + +image::flows_filter_by_name.png["Flows Filter By Name"] + +=== Delete a Flow +To delete a flow from the registry: + +1. Click on the flow to see its details. +2. Select the "Actions" drop-down and click the "Delete" menu option. ++ +image::flow_delete_action.png["Flow Delete Action"] +3. Select "Delete" to confirm. ++ +image::flow_delete_confirm.png["Flow Delete Confirm"] + +WARNING: It is possible to delete a flow that is actively being used in NiFi. + + +== Manage Buckets + +To manage buckets, enter the Administration section of the Registry by clicking the Settings button (image:iconSettings.png["Settings Icon"]) on the top right of the UI. The Buckets window appears by default. + +=== Sorting & Filtering Buckets +Buckets can be sorted alphabetically by Name (ascending or descending) using the up/down arrows. + +image::buckets_sort_by_name.png["Buckets Sort By Name"] + +The buckets listed can be filtered by: + +* bucket name +* bucket description +* bucket ID + +Here is an example filtering by bucket name: + +image::buckets_filter_by_name.png["Buckets Filter By Name"] + +=== Create a Bucket +1. Select the "New Bucket" button. ++ +image::new_bucket_button.png["New Bucket Button"] +2. Enter the desired bucket name and select the "Create" button. ++ +image::new_bucket_dialog.png["New Bucket Dialog"] + +NOTE: Check "Make publicly available" to allow read access to items in the bucket by unauthenticated users. This will override any specific <> granting read access. + +NOTE: To quickly create multiple buckets, check "Keep this dialog open after creating bucket". + + +=== Delete a Bucket +1. Select the Delete button (image:iconDelete.png["Delete Icon"]) in the row of the bucket. ++ +image::delete_bucket_single.png["Delete Single Bucket"] +2. From the Delete Bucket dialog, select "Delete". ++ +image::delete_bucket_dialog.png["Delete Bucket Dialog"] + +=== Delete Multiple Buckets +1. Select the checkboxes in the rows of the desired buckets to delete. ++ +image::check_multiple_buckets.png["Check Multiple Buckets"] +2. Select the "Actions" drop-down and click the "Delete" option. ++ +image::delete_multiple_buckets.png["Delete Multiple Buckets"] +3. From the Delete Buckets dialog, select "Delete". ++ +image::delete_buckets_dialog.png["Delete Buckets Dialog"] + +=== Edit a Bucket Name +1. Select the Manage button (image:iconManage.png["Manage Icon"]) in the row of the bucket. ++ +image::manage_bucket.png["Manage Bucket"] +2. Enter a new name for the bucket and select the "Save" button. ++ +image::bucket_nav_name_edit.png["Edit Bucket Name"] + +=== Make a Bucket Publicly Visible +To allow read access to items in a bucket by unauthenticated users, select the "Make publicly visible" checkbox. + +image::bucket_nav_make_public.png["Make Bucket Public"] + +This setting will override any specific policies granting read access to the bucket. + +=== Allow Bundles in a Bucket to be Overwritten +To allow released bundles in a bucket to be overwritten, select the "Allow bundle overwrite" checkbox. + +image::bucket_nav_allow_bundle_overwrite.png["Allow Bundle Overwrite"] + +Currently, the only supported bundle type is a link:https://nifi.apache.org/docs/nifi-docs/html/developer-guide.html#nars[NiFi Archive (NAR)]. By default, buckets do not allow the re-release of released NARs. This setting explicitly allows the same version of a NAR to be uploaded to a bucket. + +For more information on bundles, see the <> section. + +[[bucket_policies]] +=== Bucket Policies +Bucket policies define user privileges on buckets/flows in the Registry and in NiFi. The available permissions are: + +* *All* - In the Registry, the assigned user is able to view and delete flows in the bucket. In NiFi, the selected user is able to import flows from the bucket and commit changes to flows in the bucket. + +* *Read* - In the Registry, the assigned user is able to view flows in the bucket. In NiFi, the selected user is able to import flows from the bucket. + +* *Write* - In NiFi, the assigned user is able to commit changes to flows in the bucket. + +* *Delete* - In the Registry, the assigned user is able to delete flows in the bucket. + +NOTE: Users would typically have Read permissions at a minimum. A user with Write permission would not commit changes to a flow if they were not able to import it initially. A user with Delete permission would not delete a flow if they could not view it. + +NOTE: If a user has a bucket policy and the group that the user is in also has a policy, all policies are used to determine access. For example, assume User1 is in Group1, User1 has READ privileges on Bucket1 and Group1 has READ privileges on Bucket2. In this scenario, User1 will have READ privileges on both Bucket1 and Bucket2. + +==== Create a Bucket Policy +1. Select the Manage button (image:iconManage.png["Manage Icon"]) in the row of the bucket. +2. Select the "New Policy" button. ++ +image::new_bucket_policy_create.png["Create New Bucket Policy"] +3. Select a user, check the desired permissions and select the "Apply" button: ++ +image::new_bucket_policy_user_permission.png["New Bucket Policy User and Permissions"] +4. The policy is added to the bucket: ++ +image::new_bucket_policy_added.png["New Bucket Policy Added"] + +==== Delete a Bucket Policy +1. Select the Manage button (image:iconManage.png["Manage Icon"]) in the row of the bucket. +2. Select the Delete button (image:iconDelete.png["Delete Icon"]) in the row of the policy. ++ +image::delete_bucket_policy.png["Delete Policy"] +3. From the Delete Policy dialog, select "Delete". ++ +image::delete_bucket_policy_dialog.png["Delete Policy Dialog"] + + +== Manage Users & Groups + +To manage users/groups, enter the Administration section of the Registry by clicking the Settings button (image:iconSettings.png["Settings Icon"]) on the top right of the UI. Select Users from the top menu to open the Users window. + +=== Sorting & Filtering Users/Groups +Users/groups can be sorted alphabetically by Name (ascending or descending) using the up/down arrows. + +image::users_sort_by_name.png["Users Sort By Name"] + +The Users/groups listed can be filtered by: + +* user name +* user ID +* group name +* group ID + +Here is an example of filtering by user name: + +image::users_filter_by_name.png["Users Filter By Name"] + +=== Add a User +1. Select the "Add User" button. ++ +image::add_user_button.png["Add User"] +2. Enter the desired username or appropriate Identity information. Select the "Add" button. ++ +image::add_user_dialog.png["New User Dialog"] + +NOTE: To quickly create multiple users, check "Keep this dialog open after adding user". + +=== Delete a User +1. Select the Delete button (image:iconDelete.png["Delete Icon"]) in the row of the user. ++ +image::delete_user_single.png["Delete Single User"] +2. From the Delete User dialog, select "Delete". ++ +image::delete_user_dialog.png["Delete User Dialog"] + +=== Delete Multiple Users +1. Select the checkboxes in the rows of the desired users to delete. ++ +image::check_multiple_users.png["Check Multiple Users"] +2. Select the "Actions" drop-down and click the "Delete" option. ++ +image::delete_multiple_users.png["Delete Multiple Users"] +3. From the Delete Users dialog, select "Delete". ++ +image::delete_users_groups_dialog.png["Delete Users Dialog"] + + +=== Edit a User Name +1. Select the Manage button (image:iconManage.png["Manage Icon"]) in the row of the user. ++ +image::manage_user.png["Manage User"] +2. Enter a new user name and select the "Save" button. ++ +image::user_nav_name_edit.png["Edit User Name"] + +WARNING: Some users cannot have their names edited. For example, those defined by LDAP. These users will be specially highlighted in the list. + +image::users_non_configurable.png["Non-configurable Users"] + +=== Special Privileges +Special privileges are additional permissions that allow a user to manage or access certain aspects of the Registry. The special privileges are: + +* *Can manage buckets* - Allow a user to manage all buckets in the registry, as well as provide the user access to all buckets from a connected system (e.g., NiFi). + +* *Can manage users* - Allow a user to manage all registry users and groups. + +* *Can manage policies* - Allow a user to grant all registry users read, write, and delete permission to a bucket. + +* *Can proxy user requests* - Allow a connected system (e.g., NiFi) to process requests of authorized users of that system. For example, if dev and prod NiFi clusters are connected to the same NiFi Registry instance, privileges can be set to allow the dev NiFi cluster to only update versioned flows while limiting the prod NiFi to only download flows. + +==== Grant Special Privileges to a User +1. Select the Manage button (image:iconManage.png["Manage Icon"]) in the row of the user. ++ +image::manage_user.png["Manage User"] +2. Check the desired privileges: ++ +image::user_special_privileges.png["User Special Privileges"] +3. Changes made to special privileges are automatically saved. + +== Manage Groups + +=== Add an Empty Group +1. With no users checked, select the "Actions" drop-down and click the "Create new group" option. ++ +image::create_new_group.png["Create New Group"] +2. Enter a name for the Group and select the "Create" button. ++ +image::create_new_group_dialog.png["Create New Group Dialog"] + +NOTE: To quickly create multiple empty groups, check "Keep this dialog open after creating group". + + +=== Add User to a Group +1. Select the Manage button (image:iconManage.png["Manage Icon"]) in the row of the user. +2. Select the "Add To Group" button. ++ +image::user_nav_add_to_group.png["Add User to Group"] +3. In the "Add User to Groups" dialog, select the group(s) to add the user to. Select the "Add" button when all desired groups have been selected. ++ +image::add_user_to_groups_dialog.png["Add User to Groups Dialog"] +4. The user is added to the group: ++ +image::group_added.png["Group Added"] + +NOTE: Groups cannot contain other groups. + +=== Create a New Group with Selected Users +1. Select the checkboxes in the rows of the desired users. From the "Actions" drop-down, click the "Create new group" option. ++ +image::select_users_create_new_group.png["Select Users for New Group"] +2. Enter a name for the Group and select the "Create" button. ++ +image::select_users_create_new_group_dialog.png["Create New Group Dialog"] +3. The new group is created with the selected users as members: ++ +image::select_users_new_group_added.png["New Group Added with Selected Users"] + +=== Remove a User from a Group +There are two ways to remove a user from a group. + +==== User Window +1. Select the Manage button (image:iconManage.png["Manage Icon"]) in the row of the user. +2. In the Membership section of the window, select the Remove button (image:iconDelete.png["Delete Icon"]) in the row of the group. ++ +image::remove_group_from_user.png["Remove Group From User"] + +==== Group Window +1. Select the Manage button (image:iconManage.png["Manage Icon"]) in the row of the group. The Members tab is selected by default. +2. In the Membership section of the window, select the Remove button (image:iconDelete.png["Delete Icon"]) in the row of the user. ++ +image::remove_user_from_group.png["Remove User From Group"] + +=== Other Group Level Actions + +Editing group names, deleting groups, adding policies to/deleting policies from groups and granting special privileges to groups follow similar procedures described earlier for corresponding user level actions. + +[[manage_bundles]] +== Manage Bundles + +Bundles can be managed through the REST API. + +=== Upload Bundle + +A bundle can be uploaded to a bucket by making a `POST` request to the following REST end-point: + + /nifi-registry-api/buckets//bundles/ + +Replace `bucketId` with the id of the bucket where the bundle is being uploaded to and `bundleType` with the type of bundle being uploaded. Currently, the only supported bundle type is a link:https://nifi.apache.org/docs/nifi-docs/html/developer-guide.html#nars[NiFi Archive (NAR)] which can be specified as `nifi-nar`. + +The `Content-Type` of the request is expected to be `multipart/form-data`. An example of using `curl` to upload `my-processors-1.0.0.nar` would be the following: + + curl -v -F file=@/path/to/my-processors-1.0.0.nar http://localhost:18080/nifi-registry-api/buckets/de8e08c9-592d-4e10-affe-b3752698f1d9/bundles/nifi-nar + +NOTE: In order to upload a NAR to NiFi Registry, it must contain the file _META-INF/docs/extension-manifest.xml_ which is produced by the NAR Maven plugin, starting with version 1.3.0. + +=== Download Bundle + +There are two ways to download a bundle. + +==== Bundle Coordinates + +A bundle can be downloaded by using the combination of the bucket name and bundle coordinates, where bundle coordinates are the group, artifact, and version of the bundle. + +To download a bundle by its coordinates, a `GET` request can be made to the following end-point: + + /nifi-registry-api/extension-repository/{bucketName}/{groupId}/{artifactId}/{version}/content + +The `Content-Type` of the response is `application/octet-stream`. + +An example of using `curl` to download `my-processors-1.0.0.nar` from the `Test` bucket would be the following: + + curl http://localhost:18080/nifi-registry-api/extension-repository/Test/com.test/my-processors/1.0.0/content > my-processors-1.0.0.nar + + +==== Bundle Id + +A bundle can be downloaded by using the combination of its unique id and version. The unique id is an id assigned to the bundle when the first version of the bundle is uploaded to NiFi Registry. This id is returned in the response of a successful upload. + +To download a bundle by its id and version, a `GET` request can be made to the following end-point: + + /nifi-registry-api/bundles/{bundleId}/versions/{version}/content + +The `Content-Type` of the response is `application/octet-stream`. + +An example of using `curl` to download `my-processors-1.0.0.nar` by id and version would be the following: + + curl http://localhost:18080/nifi-registry-api/bundles/3db78035-e3ba-4cbf-820e-022f292bd68c/versions/1.0.0/content > my-processors-1.0.0.nar + +=== Additional Actions + +For additional actions that can be performed related to bundles, please consult the link:rest-api.html[REST API documentation]. diff --git a/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/assembly/dependencies.xml b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/assembly/dependencies.xml new file mode 100644 index 0000000000..6f6279b41f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-docs/src/main/assembly/dependencies.xml @@ -0,0 +1,44 @@ + + + + resources + + zip + + false + + + ${project.build.directory}/generated-docs/ + /html/ + + + + + ./LICENSE + ./ + LICENSE + 0644 + true + + + ./NOTICE + ./ + NOTICE + 0644 + true + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/pom.xml b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/pom.xml new file mode 100644 index 0000000000..b475247d4d --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/pom.xml @@ -0,0 +1,30 @@ + + + + 4.0.0 + + org.apache.nifi.registry + nifi-registry-core + 1.14.0-SNAPSHOT + + + nifi-registry-flow-diff + jar + + + + org.apache.nifi.registry + nifi-registry-data-model + 1.14.0-SNAPSHOT + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/ComparableDataFlow.java b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/ComparableDataFlow.java new file mode 100644 index 0000000000..603b7cf0f4 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/ComparableDataFlow.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow.diff; + +import org.apache.nifi.registry.flow.VersionedProcessGroup; + +public interface ComparableDataFlow { + String getName(); + + VersionedProcessGroup getContents(); +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/ConciseEvolvingDifferenceDescriptor.java b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/ConciseEvolvingDifferenceDescriptor.java new file mode 100644 index 0000000000..8ac2da92ff --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/ConciseEvolvingDifferenceDescriptor.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow.diff; + +import java.util.Objects; + +import org.apache.nifi.registry.flow.ScheduledState; +import org.apache.nifi.registry.flow.VersionedComponent; +import org.apache.nifi.registry.flow.VersionedFlowCoordinates; + +/** + * Describes differences between flows as if Flow A is an 'earlier version' of the same flow than Flow B. + * This provides verbiage such as "Processor with ID 123 was added to flow." + */ +public class ConciseEvolvingDifferenceDescriptor implements DifferenceDescriptor { + + @Override + public String describeDifference(final DifferenceType type, final String flowAName, final String flowBName, final VersionedComponent componentA, + final VersionedComponent componentB, final String fieldName, final Object valueA, final Object valueB) { + + final String description; + switch (type) { + case COMPONENT_ADDED: + description = String.format("%s was added", componentB.getComponentType().getTypeName()); + break; + case COMPONENT_REMOVED: + description = String.format("%s was removed", componentA.getComponentType().getTypeName()); + break; + case SCHEDULED_STATE_CHANGED: + if (ScheduledState.DISABLED.equals(valueA)) { + description = String.format("%s was enabled", componentA.getComponentType().getTypeName()); + } else { + description = String.format("%s was disabled", componentA.getComponentType().getTypeName()); + } + break; + case PROPERTY_ADDED: + description = String.format("Property '%s' was added", fieldName); + break; + case PROPERTY_REMOVED: + description = String.format("Property '%s' was removed", fieldName); + break; + case PROPERTY_PARAMETERIZED: + description = String.format("Property '%s' was parameterized", fieldName); + break; + case PROPERTY_PARAMETERIZATION_REMOVED: + description = String.format("Property '%s' is no longer a parameter reference", fieldName); + break; + case VARIABLE_ADDED: + description = String.format("Variable '%s' was added", fieldName); + break; + case VARIABLE_REMOVED: + description = String.format("Variable '%s' was removed", fieldName); + break; + case POSITION_CHANGED: + description = "Position was changed"; + break; + case BENDPOINTS_CHANGED: + description = "Connection Bendpoints changed"; + break; + case VERSIONED_FLOW_COORDINATES_CHANGED: + if (valueA instanceof VersionedFlowCoordinates && valueB instanceof VersionedFlowCoordinates) { + final VersionedFlowCoordinates coordinatesA = (VersionedFlowCoordinates) valueA; + final VersionedFlowCoordinates coordinatesB = (VersionedFlowCoordinates) valueB; + + // If the two vary only by version, then use a more concise message. If anything else is different, then use a fully explanation. + if (Objects.equals(coordinatesA.getRegistryUrl(), coordinatesB.getRegistryUrl()) && Objects.equals(coordinatesA.getBucketId(), coordinatesB.getBucketId()) + && Objects.equals(coordinatesA.getFlowId(), coordinatesB.getFlowId()) && coordinatesA.getVersion() != coordinatesB.getVersion()) { + + description = String.format("Flow Version changed from %s to %s", coordinatesA.getVersion(), coordinatesB.getVersion()); + break; + } + } + + description = String.format("From '%s' to '%s'", valueA, valueB); + break; + default: + description = String.format("From '%s' to '%s'", valueA, valueB); + break; + } + + return description; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/DifferenceDescriptor.java b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/DifferenceDescriptor.java new file mode 100644 index 0000000000..56e65ef465 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/DifferenceDescriptor.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow.diff; + +import org.apache.nifi.registry.flow.VersionedComponent; + +public interface DifferenceDescriptor { + /** + * Describes a difference between two flows + * + * @param type the difference + * @param componentA the component in "Flow A" + * @param componentB the component in "Flow B" + * @param fieldName the name of the field that changed, or null if the field name does not apply for the difference type + * @param valueA the value being compared from "Flow A" + * @param valueB the value being compared from "Flow B" + * @return a human-readable description of how the flows differ + */ + String describeDifference(DifferenceType type, String flowAName, String flowBName, VersionedComponent componentA, VersionedComponent componentB, String fieldName, + Object valueA, Object valueB); +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/DifferenceType.java b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/DifferenceType.java new file mode 100644 index 0000000000..b4f29f7cec --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/DifferenceType.java @@ -0,0 +1,282 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow.diff; + +public enum DifferenceType { + /** + * The component does not exist in Flow A but exists in Flow B + */ + COMPONENT_ADDED("Component Added"), + + /** + * The component exists in Flow A but does not exist in Flow B + */ + COMPONENT_REMOVED("Component Removed"), + + /** + * The component has a different name in each of the flows + */ + NAME_CHANGED("Component Name Changed"), + + /** + * The component has a different type in each of the flows + */ + TYPE_CHANGED("Component Type Changed"), + + /** + * The component has a different bundle in each of the flows + */ + BUNDLE_CHANGED("Component Bundle Changed"), + + /** + * The component has a different penalty duration in each of the flows + */ + PENALTY_DURATION_CHANGED("Penalty Duration Changed"), + + /** + * The component has a different yield duration in each of the flows + */ + YIELD_DURATION_CHANGED("Yield Duration Changed"), + + /** + * The component has a different bulletin level in each of the flows + */ + BULLETIN_LEVEL_CHANGED("Bulletin Level Changed"), + + /** + * The component has a different set of Auto-Terminated Relationships in each of the flows + */ + AUTO_TERMINATED_RELATIONSHIPS_CHANGED("Auto-Terminated Relationships Changed"), + + /** + * The component has a different scheduling strategy in each of the flows + */ + SCHEDULING_STRATEGY_CHANGED("Scheduling Strategy Changed"), + + /** + * The component has a different maximum number of concurrent tasks in each of the flows + */ + CONCURRENT_TASKS_CHANGED("Concurrent Tasks Changed"), + + /** + * The component has a different run schedule in each of the flows + */ + RUN_SCHEDULE_CHANGED("Run Schedule Changed"), + + /** + * The component has a different scheduled state (enabled/disabled) in each of the flows + */ + SCHEDULED_STATE_CHANGED("Scheduled State Changed"), + + /** + * The component has a different execution mode in each of the flows + */ + EXECUTION_MODE_CHANGED("Execution Mode Changed"), + + /** + * The component has a different run duration in each of the flows + */ + RUN_DURATION_CHANGED("Run Duration Changed"), + + /** + * The component has a different value in each of the flows for a specific property + */ + PROPERTY_CHANGED("Property Value Changed"), + + /** + * Property does not exist in Flow A but does exist in Flow B + */ + PROPERTY_ADDED("Property Added"), + + /** + * Property exists in Flow A but does not exist in Flow B + */ + PROPERTY_REMOVED("Property Removed"), + + /** + * Property is unset or set to an explicit value in Flow A but set to (exactly) a parameter reference in Flow B. Note that if Flow A + * has a property set to "#{param1} abc" and it is changed to "#{param1} abc #{param2}" this would indicate a Difference Type of @{link #PROPERTY_CHANGED}, not + * PROPERTY_PARAMETERIZED + */ + PROPERTY_PARAMETERIZED("Property Parameterized"), + + /** + * Property is set to (exactly) a parameter reference in Flow A but either unset or set to an explicit value in Flow B. + */ + PROPERTY_PARAMETERIZATION_REMOVED("Property Parameterization Removed"), + + /** + * The component has a different value for the Annotation Data in each of the flows + */ + ANNOTATION_DATA_CHANGED("Annotation Data (Advanced UI Configuration) Changed"), + + /** + * The component has a different comment in each of the flows + */ + COMMENTS_CHANGED("Comments Changed"), + + /** + * The position of the component on the graph is different in each of the flows + */ + POSITION_CHANGED("Position Changed"), + + /** + * The stylistic configuration of the component is different in each of the flows + */ + STYLE_CHANGED("Style Changed"), + + /** + * The Relationships included in a connection is different in each of the flows + */ + SELECTED_RELATIONSHIPS_CHANGED("Selected Relationships Changed"), + + /** + * The Connection has a different set of Prioritizers in each of the flows + */ + PRIORITIZERS_CHANGED("Prioritizers Changed"), + + /** + * The Connection has a different value for the FlowFile Expiration in each of the flows + */ + FLOWFILE_EXPIRATION_CHANGED("FlowFile Expiration Changed"), + + /** + * The Connection has a different value for the Object Backpressure Threshold in each of the flows + */ + BACKPRESSURE_OBJECT_THRESHOLD_CHANGED("Backpressure Object Threshold Changed"), + + /** + * The Connection has a different value for the Data Size Backpressure Threshold in each of the flows + */ + BACKPRESSURE_DATA_SIZE_THRESHOLD_CHANGED("Backpressure Data Size Threshold Changed"), + + /** + * The Connection has a different value for the Load Balance Strategy in each of the flows + */ + LOAD_BALANCE_STRATEGY_CHANGED("Load-Balance Strategy Changed"), + + /** + * The Connection has a different value for the Partitioning Attribute in each of the flows + */ + PARTITIONING_ATTRIBUTE_CHANGED("Partitioning Attribute Changed"), + + /** + * The Connection has a different value for the Load Balancing Compression in each of the flows + */ + LOAD_BALANCE_COMPRESSION_CHANGED("Load-Balance Compression Changed"), + + /** + * The Connection has a different set of Bend Points in each of the flows + */ + BENDPOINTS_CHANGED("Connection Bend Points Changed"), + + /** + * The Connection has a difference Source in each of the flows + */ + SOURCE_CHANGED("Connection Source Changed"), + + /** + * The Connection has a difference Destination in each of the flows + */ + DESTINATION_CHANGED("Connection Destination Changed"), + + /** + * The value in the Label is different in each of the flows + */ + LABEL_VALUE_CHANGED("Label Text Changed"), + + /** + * The variable does not exist in Flow A but exists in Flow B + */ + VARIABLE_ADDED("Variable Added to Process Group"), + + /** + * The variable does not exist in Flow B but exists in Flow A + */ + VARIABLE_REMOVED("Variable Removed from Process Group"), + + /** + * The API of the Controller Service is different in each of the flows + */ + SERVICE_API_CHANGED("Controller Service API Changed"), + + /** + * The Remote Process Group has a different Transport Protocol in each of the flows + */ + RPG_TRANSPORT_PROTOCOL_CHANGED("Remote Process Group Transport Protocol Changed"), + + /** + * The Remote Process Group has a different Proxy Host in each of the flows + */ + RPG_PROXY_HOST_CHANGED("Remote Process Group Proxy Host Changed"), + + /** + * The Remote Process Group has a different Proxy Port in each of the flows + */ + RPG_PROXY_PORT_CHANGED("Remote Process Group Proxy Port Changed"), + + /** + * The Remote Process Group has a different Proxy User in each of the flows + */ + RPG_PROXY_USER_CHANGED("Remote Process Group Proxy User Changed"), + + /** + * The Remote Process Group has a different Network Interface chosen in each of the flows + */ + RPG_NETWORK_INTERFACE_CHANGED("Remote Process Group Network Interface Changed"), + + /** + * The Remote Process Group has a different Communications Timeout in each of the flows + */ + RPG_COMMS_TIMEOUT_CHANGED("Remote Process Group Communications Timeout Changed"), + + /** + * The Remote Input Port or Remote Output Port has a different Batch Size in each of the flows + */ + REMOTE_PORT_BATCH_SIZE_CHANGED("Remote Process Group Port's Batch Size Changed"), + + /** + * The Remote Input Port or Remote Output Port has a different value for the Compression flag in each of the flows + */ + REMOTE_PORT_COMPRESSION_CHANGED("Remote Process Group Port's Compression Flag Changed"), + + /** + * The Process Group points to a different Versioned Flow in each of the flows + */ + VERSIONED_FLOW_COORDINATES_CHANGED("Versioned Flow Coordinates Changed"), + + /** + * The Process Group's configured FlowFile Concurrency is different in each of the flows + */ + FLOWFILE_CONCURRENCY_CHANGED("FlowFile Concurrency Changed"), + + /** + * The Process Group's configured FlowFile Outbound Policy is different in each of the flows + */ + FLOWFILE_OUTBOUND_POLICY_CHANGED("FlowFile Outbound Policy Changed"); + + private final String description; + + DifferenceType(final String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/EvolvingDifferenceDescriptor.java b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/EvolvingDifferenceDescriptor.java new file mode 100644 index 0000000000..a6c5d74b0a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/EvolvingDifferenceDescriptor.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow.diff; + +import org.apache.nifi.registry.flow.ScheduledState; +import org.apache.nifi.registry.flow.VersionedComponent; + +/** + * Describes differences between flows as if Flow A is an 'earlier version' of the same flow than Flow B. + * This provides verbiage such as "Processor with ID 123 was added to flow." + */ +public class EvolvingDifferenceDescriptor implements DifferenceDescriptor { + + @Override + public String describeDifference(final DifferenceType type, final String flowAName, final String flowBName, final VersionedComponent componentA, + final VersionedComponent componentB, final String fieldName, final Object valueA, final Object valueB) { + + final String description; + switch (type) { + case COMPONENT_ADDED: + description = String.format("%s with ID %s was added to flow", componentB.getComponentType().getTypeName(), componentB.getIdentifier()); + break; + case COMPONENT_REMOVED: + description = String.format("%s with ID %s was removed from flow", componentA.getComponentType().getTypeName(), componentA.getIdentifier()); + break; + case SCHEDULED_STATE_CHANGED: + if (ScheduledState.DISABLED.equals(valueA)) { + description = String.format("%s was enabled", componentA.getComponentType().getTypeName()); + } else { + description = String.format("%s was disabled", componentA.getComponentType().getTypeName()); + } + break; + case PROPERTY_ADDED: + description = String.format("Property '%s' was added to %s with ID %s", fieldName, componentB.getComponentType().getTypeName(), componentB.getIdentifier()); + break; + case PROPERTY_REMOVED: + description = String.format("Property '%s' was removed from %s with ID %s", fieldName, componentA.getComponentType().getTypeName(), componentA.getIdentifier()); + break; + case PROPERTY_PARAMETERIZED: + description = String.format("Property '%s' was parameterized", fieldName); + break; + case PROPERTY_PARAMETERIZATION_REMOVED: + description = String.format("Property '%s' is no longer a parameter reference", fieldName); + break; + case VARIABLE_ADDED: + description = String.format("Variable '%s' was added to Process Group with ID %s", fieldName, componentB.getIdentifier()); + break; + case VARIABLE_REMOVED: + description = String.format("Variable '%s' was removed from Process Group with ID %s", fieldName, componentA.getIdentifier()); + break; + default: + description = String.format("%s for %s with ID %s from '%s' to '%s'", + type.getDescription(), componentA.getComponentType().getTypeName(), componentA.getIdentifier(), valueA, valueB); + break; + } + + return description; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/FlowComparator.java b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/FlowComparator.java new file mode 100644 index 0000000000..84ffb2c7fc --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/FlowComparator.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow.diff; + +import java.util.Set; + +import org.apache.nifi.registry.flow.VersionedControllerService; + +public interface FlowComparator { + FlowComparison compare(); + + /** + * Compares two versions of a Controller Service and returns the differences between them + * + * @param serviceA the first Controller Service + * @param serviceB the second Controller Service + * @return the differences between them + */ + Set compareControllerServices(VersionedControllerService serviceA, VersionedControllerService serviceB); +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/FlowComparison.java b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/FlowComparison.java new file mode 100644 index 0000000000..52b3f265d1 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/FlowComparison.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow.diff; + +import java.util.Set; + +public interface FlowComparison { + ComparableDataFlow getFlowA(); + + ComparableDataFlow getFlowB(); + + Set getDifferences(); +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/FlowDifference.java b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/FlowDifference.java new file mode 100644 index 0000000000..249bf42dc5 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/FlowDifference.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow.diff; + +import java.util.Optional; + +import org.apache.nifi.registry.flow.VersionedComponent; + +public interface FlowDifference { + DifferenceType getDifferenceType(); + + VersionedComponent getComponentA(); + + VersionedComponent getComponentB(); + + Optional getFieldName(); + + Object getValueA(); + + Object getValueB(); + + String getDescription(); +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardComparableDataFlow.java b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardComparableDataFlow.java new file mode 100644 index 0000000000..649dbc3226 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardComparableDataFlow.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow.diff; + +import org.apache.nifi.registry.flow.VersionedProcessGroup; + +public class StandardComparableDataFlow implements ComparableDataFlow { + private final String name; + private final VersionedProcessGroup contents; + + public StandardComparableDataFlow(final String name, final VersionedProcessGroup contents) { + this.name = name; + this.contents = contents; + } + + @Override + public String getName() { + return name; + } + + @Override + public VersionedProcessGroup getContents() { + return contents; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardFlowComparator.java b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardFlowComparator.java new file mode 100644 index 0000000000..6a0baad200 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardFlowComparator.java @@ -0,0 +1,463 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow.diff; + +import org.apache.nifi.registry.flow.VersionedComponent; +import org.apache.nifi.registry.flow.VersionedConnection; +import org.apache.nifi.registry.flow.VersionedControllerService; +import org.apache.nifi.registry.flow.VersionedFlowCoordinates; +import org.apache.nifi.registry.flow.VersionedFunnel; +import org.apache.nifi.registry.flow.VersionedLabel; +import org.apache.nifi.registry.flow.VersionedPort; +import org.apache.nifi.registry.flow.VersionedProcessGroup; +import org.apache.nifi.registry.flow.VersionedProcessor; +import org.apache.nifi.registry.flow.VersionedPropertyDescriptor; +import org.apache.nifi.registry.flow.VersionedRemoteGroupPort; +import org.apache.nifi.registry.flow.VersionedRemoteProcessGroup; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class StandardFlowComparator implements FlowComparator { + private static final String DEFAULT_LOAD_BALANCE_STRATEGY = "DO_NOT_LOAD_BALANCE"; + private static final String DEFAULT_PARTITIONING_ATTRIBUTE = ""; + private static final String DEFAULT_LOAD_BALANCE_COMPRESSION = "DO_NOT_COMPRESS"; + private static final String DEFAULT_FLOW_FILE_CONCURRENCY = "UNBOUNDED"; + private static final String DEFAULT_OUTBOUND_FLOW_FILE_POLICY = "STREAM_WHEN_AVAILABLE"; + + private static final Pattern PARAMETER_REFERENCE_PATTERN = Pattern.compile("#\\{[A-Za-z0-9\\-_. ]+}"); + + private final ComparableDataFlow flowA; + private final ComparableDataFlow flowB; + private final Set externallyAccessibleServiceIds; + private final DifferenceDescriptor differenceDescriptor; + + public StandardFlowComparator(final ComparableDataFlow flowA, final ComparableDataFlow flowB, + final Set externallyAccessibleServiceIds, final DifferenceDescriptor differenceDescriptor) { + this.flowA = flowA; + this.flowB = flowB; + this.externallyAccessibleServiceIds = externallyAccessibleServiceIds; + this.differenceDescriptor = differenceDescriptor; + } + + @Override + public FlowComparison compare() { + final VersionedProcessGroup groupA = flowA.getContents(); + final VersionedProcessGroup groupB = flowB.getContents(); + final Set differences = compare(groupA, groupB); + + return new StandardFlowComparison(flowA, flowB, differences); + } + + private Set compare(final VersionedProcessGroup groupA, final VersionedProcessGroup groupB) { + final Set differences = new HashSet<>(); + // Note that we do not compare the names, because when we import a Flow into NiFi, we may well give it a new name. + // Child Process Groups' names will still compare but the main group that is under Version Control will not + compare(groupA, groupB, differences, false); + return differences; + } + + + private Set compareComponents(final Set componentsA, final Set componentsB, final ComponentComparator comparator) { + final Map componentMapA = byId(componentsA == null ? Collections.emptySet() : componentsA); + final Map componentMapB = byId(componentsB == null ? Collections.emptySet() : componentsB); + + final Set differences = new HashSet<>(); + + componentMapA.forEach((key, componentA) -> { + final T componentB = componentMapB.get(key); + comparator.compare(componentA, componentB, differences); + }); + + componentMapB.forEach((key, componentB) -> { + final T componentA = componentMapA.get(key); + + // if component A is not null, it has already been compared above. If component A + // is null, then it is missing from Flow A but present in Flow B, so we will just call + // compare(), which will handle this for us. + if (componentA == null) { + comparator.compare(componentA, componentB, differences); + } + }); + + return differences; + } + + + private boolean compareComponents(final VersionedComponent componentA, final VersionedComponent componentB, final Set differences) { + return compareComponents(componentA, componentB, differences, true, true, true); + } + + private boolean compareComponents(final VersionedComponent componentA, final VersionedComponent componentB, final Set differences, + final boolean compareName, final boolean comparePos, final boolean compareComments) { + if (componentA == null) { + differences.add(difference(DifferenceType.COMPONENT_ADDED, componentA, componentB, componentA, componentB)); + return true; + } + + if (componentB == null) { + differences.add(difference(DifferenceType.COMPONENT_REMOVED, componentA, componentB, componentA, componentB)); + return true; + } + + if (compareComments) { + addIfDifferent(differences, DifferenceType.COMMENTS_CHANGED, componentA, componentB, VersionedComponent::getComments, false); + } + + if (compareName) { + addIfDifferent(differences, DifferenceType.NAME_CHANGED, componentA, componentB, VersionedComponent::getName); + } + + if (comparePos) { + addIfDifferent(differences, DifferenceType.POSITION_CHANGED, componentA, componentB, VersionedComponent::getPosition); + } + + return false; + } + + private void compare(final VersionedProcessor processorA, final VersionedProcessor processorB, final Set differences) { + if (compareComponents(processorA, processorB, differences)) { + return; + } + + addIfDifferent(differences, DifferenceType.ANNOTATION_DATA_CHANGED, processorA, processorB, VersionedProcessor::getAnnotationData); + addIfDifferent(differences, DifferenceType.AUTO_TERMINATED_RELATIONSHIPS_CHANGED, processorA, processorB, VersionedProcessor::getAutoTerminatedRelationships); + addIfDifferent(differences, DifferenceType.BULLETIN_LEVEL_CHANGED, processorA, processorB, VersionedProcessor::getBulletinLevel); + addIfDifferent(differences, DifferenceType.BUNDLE_CHANGED, processorA, processorB, VersionedProcessor::getBundle); + addIfDifferent(differences, DifferenceType.CONCURRENT_TASKS_CHANGED, processorA, processorB, VersionedProcessor::getConcurrentlySchedulableTaskCount); + addIfDifferent(differences, DifferenceType.EXECUTION_MODE_CHANGED, processorA, processorB, VersionedProcessor::getExecutionNode); + addIfDifferent(differences, DifferenceType.PENALTY_DURATION_CHANGED, processorA, processorB, VersionedProcessor::getPenaltyDuration); + addIfDifferent(differences, DifferenceType.RUN_DURATION_CHANGED, processorA, processorB, VersionedProcessor::getRunDurationMillis); + addIfDifferent(differences, DifferenceType.RUN_SCHEDULE_CHANGED, processorA, processorB, VersionedProcessor::getSchedulingPeriod); + addIfDifferent(differences, DifferenceType.SCHEDULING_STRATEGY_CHANGED, processorA, processorB, VersionedProcessor::getSchedulingStrategy); + addIfDifferent(differences, DifferenceType.SCHEDULED_STATE_CHANGED, processorA, processorB, VersionedProcessor::getScheduledState); + addIfDifferent(differences, DifferenceType.STYLE_CHANGED, processorA, processorB, VersionedProcessor::getStyle); + addIfDifferent(differences, DifferenceType.YIELD_DURATION_CHANGED, processorA, processorB, VersionedProcessor::getYieldDuration); + compareProperties(processorA, processorB, processorA.getProperties(), processorB.getProperties(), processorA.getPropertyDescriptors(), processorB.getPropertyDescriptors(), differences); + } + + @Override + public Set compareControllerServices(final VersionedControllerService serviceA, final VersionedControllerService serviceB) { + final Set differences = new HashSet<>(); + compare(serviceA, serviceB, differences); + return differences; + } + + private void compare(final VersionedControllerService serviceA, final VersionedControllerService serviceB, final Set differences) { + if (compareComponents(serviceA, serviceB, differences)) { + return; + } + + addIfDifferent(differences, DifferenceType.ANNOTATION_DATA_CHANGED, serviceA, serviceB, VersionedControllerService::getAnnotationData); + addIfDifferent(differences, DifferenceType.BUNDLE_CHANGED, serviceA, serviceB, VersionedControllerService::getBundle); + compareProperties(serviceA, serviceB, serviceA.getProperties(), serviceB.getProperties(), serviceA.getPropertyDescriptors(), serviceB.getPropertyDescriptors(), differences); + } + + + private void compareProperties(final VersionedComponent componentA, final VersionedComponent componentB, + final Map propertiesA, final Map propertiesB, + final Map descriptorsA, final Map descriptorsB, + final Set differences) { + + propertiesA.forEach((key, valueA) -> { + final String valueB = propertiesB.get(key); + + VersionedPropertyDescriptor descriptor = descriptorsA.get(key); + if (descriptor == null) { + descriptor = descriptorsB.get(key); + } + + final String displayName; + if (descriptor == null) { + displayName = key; + } else { + displayName = descriptor.getDisplayName() == null ? descriptor.getName() : descriptor.getDisplayName(); + } + + if (valueA == null && valueB != null) { + if (isParameterReference(valueB)) { + differences.add(difference(DifferenceType.PROPERTY_PARAMETERIZED, componentA, componentB, key, displayName, null, null)); + } else { + differences.add(difference(DifferenceType.PROPERTY_ADDED, componentA, componentB, key, displayName, valueA, valueB)); + } + } else if (valueA != null && valueB == null) { + if (isParameterReference(valueA)) { + differences.add(difference(DifferenceType.PROPERTY_PARAMETERIZATION_REMOVED, componentA, componentB, key, displayName, null, null)); + } else { + differences.add(difference(DifferenceType.PROPERTY_REMOVED, componentA, componentB, key, displayName, valueA, valueB)); + } + } else if (valueA != null && !valueA.equals(valueB)) { + // If the property in Flow A references a Controller Service that is not available in the flow + // and the property in Flow B references a Controller Service that is available in its environment + // but not part of the Versioned Flow, then we do not want to consider this to be a Flow Difference. + // This is typically the case when a flow is versioned in one instance, referencing an external Controller Service, + // and then imported into another NiFi instance. When imported, the property does not point to any existing Controller + // Service, and the user must then point the property an existing Controller Service. We don't want to consider the + // flow as having changed, since it is an environment-specific change (similar to how we handle variables). + if (descriptor != null && descriptor.getIdentifiesControllerService()) { + final boolean accessibleA = externallyAccessibleServiceIds.contains(valueA); + final boolean accessibleB = externallyAccessibleServiceIds.contains(valueB); + if (!accessibleA && accessibleB) { + return; + } + } + + final boolean aParameterized = isParameterReference(valueA); + final boolean bParameterized = isParameterReference(valueB); + if (aParameterized && !bParameterized) { + differences.add(difference(DifferenceType.PROPERTY_PARAMETERIZATION_REMOVED, componentA, componentB, key, displayName, null, null)); + } else if (!aParameterized && bParameterized) { + differences.add(difference(DifferenceType.PROPERTY_PARAMETERIZED, componentA, componentB, key, displayName, null, null)); + } else { + differences.add(difference(DifferenceType.PROPERTY_CHANGED, componentA, componentB, key, displayName, valueA, valueB)); + } + } + }); + + propertiesB.forEach((key, valueB) -> { + final String valueA = propertiesA.get(key); + + // If there are any properties for component B that do not exist for Component A, add those as differences as well. + if (valueA == null && valueB != null) { + final VersionedPropertyDescriptor descriptor = descriptorsB.get(key); + + final String displayName; + if (descriptor == null) { + displayName = key; + } else { + displayName = descriptor.getDisplayName() == null ? descriptor.getName() : descriptor.getDisplayName(); + } + + if (isParameterReference(valueB)) { + differences.add(difference(DifferenceType.PROPERTY_PARAMETERIZED, componentA, componentB, key, displayName, null, null)); + } else { + differences.add(difference(DifferenceType.PROPERTY_ADDED, componentA, componentB, key, displayName, null, valueB)); + } + } + }); + } + + + private boolean isParameterReference(final String propertyValue) { + return PARAMETER_REFERENCE_PATTERN.matcher(propertyValue).matches(); + } + + private void compare(final VersionedFunnel funnelA, final VersionedFunnel funnelB, final Set differences) { + compareComponents(funnelA, funnelB, differences); + } + + private void compare(final VersionedLabel labelA, final VersionedLabel labelB, final Set differences) { + if (compareComponents(labelA, labelB, differences)) { + return; + } + + addIfDifferent(differences, DifferenceType.LABEL_VALUE_CHANGED, labelA, labelB, VersionedLabel::getLabel); + addIfDifferent(differences, DifferenceType.POSITION_CHANGED, labelA, labelB, VersionedLabel::getHeight); + addIfDifferent(differences, DifferenceType.POSITION_CHANGED, labelA, labelB, VersionedLabel::getWidth); + addIfDifferent(differences, DifferenceType.STYLE_CHANGED, labelA, labelB, VersionedLabel::getStyle); + } + + private void compare(final VersionedPort portA, final VersionedPort portB, final Set differences) { + if (compareComponents(portA, portB, differences)) { + return; + } + + if (portA != null && portA.isAllowRemoteAccess() && portB != null && portB.isAllowRemoteAccess()) { + addIfDifferent(differences, DifferenceType.CONCURRENT_TASKS_CHANGED, portA, portB, VersionedPort::getConcurrentlySchedulableTaskCount); + } + } + + private void compare(final VersionedRemoteProcessGroup rpgA, final VersionedRemoteProcessGroup rpgB, final Set differences) { + if (compareComponents(rpgA, rpgB, differences, false, true, false)) { // do not compare comments for RPG because they come from remote system, not our local flow + return; + } + + addIfDifferent(differences, DifferenceType.RPG_COMMS_TIMEOUT_CHANGED, rpgA, rpgB, VersionedRemoteProcessGroup::getCommunicationsTimeout); + addIfDifferent(differences, DifferenceType.RPG_NETWORK_INTERFACE_CHANGED, rpgA, rpgB, VersionedRemoteProcessGroup::getLocalNetworkInterface); + addIfDifferent(differences, DifferenceType.RPG_PROXY_HOST_CHANGED, rpgA, rpgB, VersionedRemoteProcessGroup::getProxyHost); + addIfDifferent(differences, DifferenceType.RPG_PROXY_PORT_CHANGED, rpgA, rpgB, VersionedRemoteProcessGroup::getProxyPort); + addIfDifferent(differences, DifferenceType.RPG_PROXY_USER_CHANGED, rpgA, rpgB, VersionedRemoteProcessGroup::getProxyUser); + addIfDifferent(differences, DifferenceType.RPG_TRANSPORT_PROTOCOL_CHANGED, rpgA, rpgB, VersionedRemoteProcessGroup::getTransportProtocol); + addIfDifferent(differences, DifferenceType.YIELD_DURATION_CHANGED, rpgA, rpgB, VersionedRemoteProcessGroup::getYieldDuration); + + differences.addAll(compareComponents(rpgA.getInputPorts(), rpgB.getInputPorts(), this::compare)); + differences.addAll(compareComponents(rpgA.getOutputPorts(), rpgB.getOutputPorts(), this::compare)); + } + + private void compare(final VersionedRemoteGroupPort portA, final VersionedRemoteGroupPort portB, final Set differences) { + if (compareComponents(portA, portB, differences)) { + return; + } + + addIfDifferent(differences, DifferenceType.REMOTE_PORT_BATCH_SIZE_CHANGED, portA, portB, VersionedRemoteGroupPort::getBatchSize); + addIfDifferent(differences, DifferenceType.REMOTE_PORT_COMPRESSION_CHANGED, portA, portB, VersionedRemoteGroupPort::isUseCompression); + addIfDifferent(differences, DifferenceType.CONCURRENT_TASKS_CHANGED, portA, portB, VersionedRemoteGroupPort::getConcurrentlySchedulableTaskCount); + } + + + private void compare(final VersionedProcessGroup groupA, final VersionedProcessGroup groupB, final Set differences, final boolean compareNamePos) { + if (compareComponents(groupA, groupB, differences, compareNamePos, compareNamePos, true)) { + return; + } + + if (groupA == null) { + differences.add(difference(DifferenceType.COMPONENT_ADDED, groupA, groupB, groupA, groupB)); + return; + } + + if (groupB == null) { + differences.add(difference(DifferenceType.COMPONENT_REMOVED, groupA, groupB, groupA, groupB)); + return; + } + + addIfDifferent(differences, DifferenceType.VERSIONED_FLOW_COORDINATES_CHANGED, groupA, groupB, VersionedProcessGroup::getVersionedFlowCoordinates); + addIfDifferent(differences, DifferenceType.FLOWFILE_CONCURRENCY_CHANGED, groupA, groupB, VersionedProcessGroup::getFlowFileConcurrency, + true, DEFAULT_FLOW_FILE_CONCURRENCY); + addIfDifferent(differences, DifferenceType.FLOWFILE_OUTBOUND_POLICY_CHANGED, groupA, groupB, VersionedProcessGroup::getFlowFileOutboundPolicy, + true, DEFAULT_OUTBOUND_FLOW_FILE_POLICY); + + final VersionedFlowCoordinates groupACoordinates = groupA.getVersionedFlowCoordinates(); + final VersionedFlowCoordinates groupBCoordinates = groupB.getVersionedFlowCoordinates(); + + if ((groupACoordinates == null && groupBCoordinates == null) + || (groupACoordinates != null && groupBCoordinates != null && !groupACoordinates.equals(groupBCoordinates)) ) { + differences.addAll(compareComponents(groupA.getConnections(), groupB.getConnections(), this::compare)); + differences.addAll(compareComponents(groupA.getProcessors(), groupB.getProcessors(), this::compare)); + differences.addAll(compareComponents(groupA.getControllerServices(), groupB.getControllerServices(), this::compare)); + differences.addAll(compareComponents(groupA.getFunnels(), groupB.getFunnels(), this::compare)); + differences.addAll(compareComponents(groupA.getInputPorts(), groupB.getInputPorts(), this::compare)); + differences.addAll(compareComponents(groupA.getLabels(), groupB.getLabels(), this::compare)); + differences.addAll(compareComponents(groupA.getOutputPorts(), groupB.getOutputPorts(), this::compare)); + differences.addAll(compareComponents(groupA.getProcessGroups(), groupB.getProcessGroups(), (a, b, diffs) -> compare(a, b, diffs, true))); + differences.addAll(compareComponents(groupA.getRemoteProcessGroups(), groupB.getRemoteProcessGroups(), this::compare)); + } + } + + + private void compare(final VersionedConnection connectionA, final VersionedConnection connectionB, final Set differences) { + if (compareComponents(connectionA, connectionB, differences)) { + return; + } + + addIfDifferent(differences, DifferenceType.BACKPRESSURE_DATA_SIZE_THRESHOLD_CHANGED, connectionA, connectionB, VersionedConnection::getBackPressureDataSizeThreshold); + addIfDifferent(differences, DifferenceType.BACKPRESSURE_OBJECT_THRESHOLD_CHANGED, connectionA, connectionB, VersionedConnection::getBackPressureObjectThreshold); + addIfDifferent(differences, DifferenceType.BENDPOINTS_CHANGED, connectionA, connectionB, VersionedConnection::getBends); + addIfDifferent(differences, DifferenceType.DESTINATION_CHANGED, connectionA, connectionB, VersionedConnection::getDestination); + addIfDifferent(differences, DifferenceType.FLOWFILE_EXPIRATION_CHANGED, connectionA, connectionB, VersionedConnection::getFlowFileExpiration); + addIfDifferent(differences, DifferenceType.PRIORITIZERS_CHANGED, connectionA, connectionB, VersionedConnection::getPrioritizers); + addIfDifferent(differences, DifferenceType.SELECTED_RELATIONSHIPS_CHANGED, connectionA, connectionB, VersionedConnection::getSelectedRelationships); + addIfDifferent(differences, DifferenceType.SOURCE_CHANGED, connectionA, connectionB, c -> c.getSource().getId()); + + addIfDifferent(differences, DifferenceType.LOAD_BALANCE_STRATEGY_CHANGED, connectionA, connectionB, + conn -> conn.getLoadBalanceStrategy() == null ? DEFAULT_LOAD_BALANCE_STRATEGY : conn.getLoadBalanceStrategy()); + + addIfDifferent(differences, DifferenceType.PARTITIONING_ATTRIBUTE_CHANGED, connectionA, connectionB, + conn -> conn.getPartitioningAttribute() == null ? DEFAULT_PARTITIONING_ATTRIBUTE : conn.getPartitioningAttribute()); + + addIfDifferent(differences, DifferenceType.LOAD_BALANCE_COMPRESSION_CHANGED, connectionA, connectionB, + conn -> conn.getLoadBalanceCompression() == null ? DEFAULT_LOAD_BALANCE_COMPRESSION : conn.getLoadBalanceCompression()); + } + + + private Map byId(final Set components) { + return components.stream().collect(Collectors.toMap(VersionedComponent::getIdentifier, Function.identity())); + } + + private void addIfDifferent(final Set differences, final DifferenceType type, final T componentA, final T componentB, + final Function transform) { + + addIfDifferent(differences, type, componentA, componentB, transform, true); + } + + private void addIfDifferent(final Set differences, final DifferenceType type, final T componentA, final T componentB, + final Function transform, final boolean differentiateNullAndEmptyString) { + addIfDifferent(differences, type, componentA, componentB, transform, differentiateNullAndEmptyString, null); + } + + private void addIfDifferent(final Set differences, final DifferenceType type, final T componentA, final T componentB, + final Function transform, final boolean differentiateNullAndEmptyString, final Object defaultValue) { + + Object valueA = transform.apply(componentA); + if (valueA == null) { + valueA = defaultValue; + } + + Object valueB = transform.apply(componentB); + if (valueB == null) { + valueB = defaultValue; + } + + if (Objects.equals(valueA, valueB)) { + return; + } + + // We don't want to disambiguate between an empty collection and null. + if ((valueA == null || valueA instanceof Collection) && (valueB == null || valueB instanceof Collection) && isEmpty((Collection) valueA) && isEmpty((Collection) valueB)) { + return; + } + + if (!differentiateNullAndEmptyString && isEmptyString(valueA) && isEmptyString(valueB)) { + return; + } + + differences.add(difference(type, componentA, componentB, valueA, valueB)); + } + + private boolean isEmpty(final Collection collection) { + return collection == null || collection.isEmpty(); + } + + private boolean isEmptyString(final Object potentialString) { + if (potentialString == null) { + return true; + } + + if (potentialString instanceof String) { + final String string = (String) potentialString; + return string.isEmpty(); + } else { + return false; + } + } + + private FlowDifference difference(final DifferenceType type, final VersionedComponent componentA, final VersionedComponent componentB, + final Object valueA, final Object valueB) { + + final String description = differenceDescriptor.describeDifference(type, flowA.getName(), flowB.getName(), componentA, componentB, null, valueA, valueB); + return new StandardFlowDifference(type, componentA, componentB, valueA, valueB, description); + } + + private FlowDifference difference(final DifferenceType type, final VersionedComponent componentA, final VersionedComponent componentB, final String fieldName, final String prettyPrintFieldName, + final Object valueA, final Object valueB) { + + final String description = differenceDescriptor.describeDifference(type, flowA.getName(), flowB.getName(), componentA, componentB, prettyPrintFieldName, valueA, valueB); + return new StandardFlowDifference(type, componentA, componentB, fieldName, valueA, valueB, description); + } + + + private static interface ComponentComparator { + void compare(T componentA, T componentB, Set differences); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardFlowComparison.java b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardFlowComparison.java new file mode 100644 index 0000000000..32459940a9 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardFlowComparison.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow.diff; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public class StandardFlowComparison implements FlowComparison { + + private final ComparableDataFlow flowA; + private final ComparableDataFlow flowB; + private final Set differences; + + public StandardFlowComparison(final ComparableDataFlow flowA, final ComparableDataFlow flowB) { + this.flowA = flowA; + this.flowB = flowB; + this.differences = new HashSet<>(); + } + + public StandardFlowComparison(final ComparableDataFlow flowA, final ComparableDataFlow flowB, final Set differences) { + this.flowA = flowA; + this.flowB = flowB; + this.differences = differences; + } + + @Override + public ComparableDataFlow getFlowA() { + return flowA; + } + + @Override + public ComparableDataFlow getFlowB() { + return flowB; + } + + @Override + public Set getDifferences() { + return Collections.unmodifiableSet(differences); + } + + public void addDifference(final FlowDifference difference) { + this.differences.add(difference); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardFlowDifference.java b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardFlowDifference.java new file mode 100644 index 0000000000..054ee5f42a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardFlowDifference.java @@ -0,0 +1,119 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow.diff; + +import java.util.Objects; +import java.util.Optional; + +import org.apache.nifi.registry.flow.VersionedComponent; + +public class StandardFlowDifference implements FlowDifference { + private final DifferenceType type; + private final VersionedComponent componentA; + private final VersionedComponent componentB; + private final Optional fieldName; + private final Object valueA; + private final Object valueB; + private final String description; + + public StandardFlowDifference(final DifferenceType type, final VersionedComponent componentA, final VersionedComponent componentB, final Object valueA, final Object valueB, + final String description) { + this(type, componentA, componentB, null, valueA, valueB, description); + } + + public StandardFlowDifference(final DifferenceType type, final VersionedComponent componentA, final VersionedComponent componentB, final String fieldName, + final Object valueA, final Object valueB, final String description) { + this.type = type; + this.componentA = componentA; + this.componentB = componentB; + this.fieldName = Optional.ofNullable(fieldName); + this.valueA = valueA; + this.valueB = valueB; + this.description = description; + } + + @Override + public DifferenceType getDifferenceType() { + return type; + } + + @Override + public VersionedComponent getComponentA() { + return componentA; + } + + @Override + public VersionedComponent getComponentB() { + return componentB; + } + + @Override + public Optional getFieldName() { + return fieldName; + } + + @Override + public Object getValueA() { + return valueA; + } + + @Override + public Object getValueB() { + return valueB; + } + + @Override + public String getDescription() { + return description; + } + + @Override + public String toString() { + return description; + } + + @Override + public int hashCode() { + return 31 + 17 * (componentA == null ? 0 : componentA.getIdentifier().hashCode()) + + 17 * (componentB == null ? 0 : componentB.getIdentifier().hashCode()) + + Objects.hash(description, type, valueA, valueB); + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (obj == this) { + return true; + } + if (!(obj instanceof StandardFlowDifference)) { + return false; + } + final StandardFlowDifference other = (StandardFlowDifference) obj; + final String componentAId = componentA == null ? null : componentA.getIdentifier(); + final String otherComponentAId = other.componentA == null ? null : other.componentA.getIdentifier(); + + final String componentBId = componentB == null ? null : componentB.getIdentifier(); + final String otherComponentBId = other.componentB == null ? null : other.componentB.getIdentifier(); + + return Objects.equals(componentAId, otherComponentAId) && Objects.equals(componentBId, otherComponentBId) + && Objects.equals(description, other.description) && Objects.equals(type, other.type) + && Objects.equals(valueA, other.valueA) && Objects.equals(valueB, other.valueB); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StaticDifferenceDescriptor.java b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StaticDifferenceDescriptor.java new file mode 100644 index 0000000000..86688040d8 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StaticDifferenceDescriptor.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow.diff; + +import java.util.Objects; + +import org.apache.nifi.registry.flow.ScheduledState; +import org.apache.nifi.registry.flow.VersionedComponent; +import org.apache.nifi.registry.flow.VersionedFlowCoordinates; + +/** + * Describes differences between flows as if the flows are two disparate flows that are being + * compared to one another. This provides verbiage such as "Processor with ID 123 exists in Flow A but not in Flow B." + */ +public class StaticDifferenceDescriptor implements DifferenceDescriptor { + + @Override + public String describeDifference(final DifferenceType type, final String flowAName, final String flowBName, final VersionedComponent componentA, + final VersionedComponent componentB, final String fieldName, final Object valueA, final Object valueB) { + + final String description; + switch (type) { + case COMPONENT_ADDED: + description = String.format("%s with ID %s exists in %s but not in %s", + componentB.getComponentType().getTypeName(), componentB.getIdentifier(), flowBName, flowAName); + break; + case COMPONENT_REMOVED: + description = String.format("%s with ID %s exists in %s but not in %s", + componentA.getComponentType().getTypeName(), componentA.getIdentifier(), flowAName, flowBName); + break; + case PROPERTY_ADDED: + description = String.format("Property '%s' exists for %s with ID %s in %s but not in %s", + fieldName, componentB.getComponentType().getTypeName(), componentB.getIdentifier(), flowBName, flowAName); + break; + case PROPERTY_REMOVED: + description = String.format("Property '%s' exists for %s with ID %s in %s but not in %s", + fieldName, componentA.getComponentType().getTypeName(), componentA.getIdentifier(), flowAName, flowBName); + break; + case PROPERTY_PARAMETERIZED: + description = String.format("Property '%s' is a parameter reference in %s but not in %s", fieldName, flowAName, flowBName); + break; + case PROPERTY_PARAMETERIZATION_REMOVED: + description = String.format("Property '%s' is a parameter reference in %s but not in %s", fieldName, flowBName, flowAName); + break; + case SCHEDULED_STATE_CHANGED: + if (ScheduledState.DISABLED.equals(valueA)) { + description = String.format("%s is disabled in %s but enabled in %s", componentA.getComponentType().getTypeName(), flowAName, flowBName); + } else { + description = String.format("%s is enabled in %s but disabled in %s", componentA.getComponentType().getTypeName(), flowAName, flowBName); + } + break; + case VARIABLE_ADDED: + description = String.format("Variable '%s' exists for Process Group with ID %s in %s but not in %s", + fieldName, componentB.getIdentifier(), flowBName, flowAName); + break; + case VARIABLE_REMOVED: + description = String.format("Variable '%s' exists for Process Group with ID %s in %s but not in %s", + fieldName, componentA.getIdentifier(), flowAName, flowBName); + break; + case VERSIONED_FLOW_COORDINATES_CHANGED: + if (valueA instanceof VersionedFlowCoordinates && valueB instanceof VersionedFlowCoordinates) { + final VersionedFlowCoordinates coordinatesA = (VersionedFlowCoordinates) valueA; + final VersionedFlowCoordinates coordinatesB = (VersionedFlowCoordinates) valueB; + + // If the two vary only by version, then use a more concise message. If anything else is different, then use a fully explanation. + if (Objects.equals(coordinatesA.getRegistryUrl(), coordinatesB.getRegistryUrl()) && Objects.equals(coordinatesA.getBucketId(), coordinatesB.getBucketId()) + && Objects.equals(coordinatesA.getFlowId(), coordinatesB.getFlowId()) && coordinatesA.getVersion() != coordinatesB.getVersion()) { + + description = String.format("Flow Version is %s in %s but %s in %s", coordinatesA.getVersion(), flowAName, coordinatesB.getVersion(), flowBName); + break; + } + } + + description = String.format("%s for %s with ID %s; flow '%s' has value %s; flow '%s' has value %s", + type.getDescription(), componentA.getComponentType().getTypeName(), componentA.getIdentifier(), + flowAName, valueA, flowBName, valueB); + break; + default: + description = String.format("%s for %s with ID %s; flow '%s' has value %s; flow '%s' has value %s", + type.getDescription(), componentA.getComponentType().getTypeName(), componentA.getIdentifier(), + flowAName, valueA, flowBName, valueB); + break; + } + + return description; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/pom.xml b/nifi-registry/nifi-registry-core/nifi-registry-framework/pom.xml new file mode 100644 index 0000000000..f1fcad626a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/pom.xml @@ -0,0 +1,430 @@ + + + + + org.apache.nifi.registry + nifi-registry-core + 1.14.0-SNAPSHOT + + 4.0.0 + nifi-registry-framework + jar + + + + + src/main/resources + + + src/main/xsd + + + + + org.codehaus.mojo + jaxb2-maven-plugin + + + aliases + + xjc + + + + src/main/xsd/aliases.xsd + + org.apache.nifi.registry.url.aliaser.generated + false + + + + providers + + xjc + + + + src/main/xsd/providers.xsd + + org.apache.nifi.registry.provider.generated + false + + + + authorizers + + xjc + + + + src/main/xsd/authorizers.xsd + + org.apache.nifi.registry.security.authorization.generated + false + + + + authorizations + + xjc + + + + src/main/xsd/authorizations.xsd + + org.apache.nifi.registry.security.authorization.file.generated + false + + + + tenants + + xjc + + + + src/main/xsd/tenants.xsd + + org.apache.nifi.registry.security.authorization.file.tenants.generated + false + + + + identity-providers + + xjc + + + + src/main/xsd/identity-providers.xsd + + org.apache.nifi.registry.security.authentication.generated + false + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + **/generated/*.java + + + + org.codehaus.gmavenplus + gmavenplus-plugin + 1.5 + + + + addTestSources + testCompile + + + + + + org.apache.rat + apache-rat-plugin + + + src/test/resources/serialization/json/no-version.snapshot + src/test/resources/serialization/json/non-integer-version.snapshot + src/test/resources/serialization/ver1.snapshot + src/test/resources/serialization/ver2.snapshot + src/test/resources/serialization/ver3.snapshot + src/test/resources/serialization/ver9999.snapshot + src/test/resources/extensions/ConsumeKafkaRecord_1_0.json + + + + + + + + + org.apache.nifi.registry + nifi-registry-data-model + 1.14.0-SNAPSHOT + + + org.apache.nifi.registry + nifi-registry-properties + 1.14.0-SNAPSHOT + + + org.apache.nifi.registry + nifi-registry-utils + 1.14.0-SNAPSHOT + provided + + + org.apache.nifi.registry + nifi-registry-provider-api + provided + + + org.apache.nifi.registry + nifi-registry-security-api + provided + + + org.apache.nifi.registry + nifi-registry-security-utils + 1.14.0-SNAPSHOT + + + org.apache.nifi.registry + nifi-registry-bundle-utils + 1.14.0-SNAPSHOT + + + org.apache.nifi.registry + nifi-registry-flow-diff + 1.14.0-SNAPSHOT + + + javax.servlet + javax.servlet-api + 3.1.0 + provided + + + org.springframework.boot + spring-boot-starter-security + ${spring.boot.version} + + + org.springframework.security + spring-security-ldap + ${spring.security.version} + + + org.springframework.security + spring-security-core + + + org.springframework + spring-beans + + + org.springframework + spring-context + + + org.springframework + spring-core + + + org.springframework + spring-tx + + + commons-logging + commons-logging + + + + + org.bouncycastle + bcprov-jdk15on + ${bouncycastle.version} + + + + org.bouncycastle + bcpg-jdk15on + ${bouncycastle.version} + + + commons-io + commons-io + + + org.glassfish + javax.el + + + + org.springframework.boot + spring-boot-starter-data-jpa + ${spring.boot.version} + + + org.apache.tomcat + tomcat-jdbc + + + org.springframework.data + spring-data-jpa + + + org.hibernate + hibernate-entitymanager + + + org.hibernate + hibernate-core + + + + javax.xml.bind + jaxb-api + + + + + org.springframework.boot + spring-boot-starter-validation + ${spring.boot.version} + + + org.flywaydb + flyway-core + ${flyway.version} + + + com.h2database + h2 + ${h2.version} + + + org.eclipse.jgit + org.eclipse.jgit + ${jgit.version} + + + org.eclipse.jgit + org.eclipse.jgit.gpg.bc + ${jgit.version} + + + org.eclipse.jgit + org.eclipse.jgit.ssh.apache + ${jgit.version} + + + commons-codec + commons-codec + 1.12 + + + com.jcraft + jsch + 0.1.54 + + + org.yaml + snakeyaml + 1.28 + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + com.fasterxml.jackson.module + jackson-module-jaxb-annotations + ${jackson.version} + + + + org.apache.nifi.registry + nifi-registry-test + 1.14.0-SNAPSHOT + test + + + org.flywaydb.flyway-test-extensions + flyway-spring-test + ${flyway.tests.version} + test + + + org.flywaydb + flyway-core + + + org.springframework + spring-test + + + org.springframework + spring-context + + + org.springframework + spring-jdbc + + + + + org.mockito + mockito-core + test + + + org.spockframework + spock-core + test + + + org.codehaus.groovy + groovy-test + test + + + cglib + cglib-nodep + 2.2.2 + test + + + org.apache.directory.server + apacheds-all + 2.0.0-M24 + test + + + + + + + jigsaw + + (1.8,) + + + + jakarta.xml.bind + jakarta.xml.bind-api + + + org.glassfish.jaxb + jaxb-runtime + + + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/CustomFlywayConfiguration.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/CustomFlywayConfiguration.java new file mode 100644 index 0000000000..7f9ef7a777 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/CustomFlywayConfiguration.java @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db; + +import org.flywaydb.core.api.FlywayException; +import org.flywaydb.core.api.configuration.FluentConfiguration; +import org.flywaydb.core.internal.jdbc.DatabaseType; +import org.flywaydb.core.internal.jdbc.JdbcUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.flyway.FlywayConfigurationCustomizer; +import org.springframework.context.annotation.Configuration; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; + +@Configuration +public class CustomFlywayConfiguration implements FlywayConfigurationCustomizer { + + private static final Logger LOGGER = LoggerFactory.getLogger(CustomFlywayConfiguration.class); + + private static final String LOCATION_COMMON = "classpath:db/migration/common"; + + private static final String LOCATION_DEFAULT = "classpath:db/migration/default"; + private static final String[] LOCATIONS_DEFAULT = {LOCATION_COMMON, LOCATION_DEFAULT}; + + private static final String LOCATION_MYSQL = "classpath:db/migration/mysql"; + private static final String[] LOCATIONS_MYSQL = {LOCATION_COMMON, LOCATION_MYSQL}; + + private static final String LOCATION_POSTGRES = "classpath:db/migration/postgres"; + private static final String[] LOCATIONS_POSTGRES = {LOCATION_COMMON, LOCATION_POSTGRES}; + + private static final String LEGACY_FLYWAY_SCHEMA_TABLE = "schema_version"; + + @Override + public void customize(final FluentConfiguration configuration) { + final DatabaseType databaseType = getDatabaseType(configuration.getDataSource()); + LOGGER.info("Determined database type is {}", new Object[] {databaseType.name()}); + + switch (databaseType) { + case MYSQL: + LOGGER.info("Setting migration locations to {}", new Object[] {LOCATIONS_MYSQL}); + configuration.locations(LOCATIONS_MYSQL); + break; + case POSTGRESQL: + LOGGER.info("Setting migration locations to {}", new Object[] {LOCATIONS_POSTGRES}); + configuration.locations(LOCATIONS_POSTGRES); + break; + default: + LOGGER.info("Setting migration locations to {}", new Object[] {LOCATIONS_DEFAULT}); + configuration.locations(LOCATIONS_DEFAULT); + break; + } + + // At some point Flyway changed their default table name: https://github.com/flyway/flyway/issues/1848 + // So we need to determine if we are upgrading from an existing nifi registry that is using the older + // name, and if so then continue using that name, otherwise use the new default name + if (isLegacyFlywaySchemaTable(configuration.getDataSource())) { + LOGGER.info("Using legacy Flyway configuration table - {}", LEGACY_FLYWAY_SCHEMA_TABLE); + configuration.table(LEGACY_FLYWAY_SCHEMA_TABLE); + } else { + LOGGER.info("Using default Flyway configuration table"); + } + } + + /** + * Determines the database type from the given data source. + * + * @param dataSource the data source + * @return the database type + */ + private DatabaseType getDatabaseType(final DataSource dataSource) { + try (final Connection connection = dataSource.getConnection()) { + return DatabaseType.fromJdbcConnection(connection); + } catch (SQLException e) { + LOGGER.error(e.getMessage(), e); + throw new FlywayException("Unable to obtain connection from Flyway DataSource", e); + } + } + + /** + * Determines if the legacy flyway schema table exists. + * + * @param dataSource the data source + * @return true if the legacy schema tables exists, false otherwise + */ + private boolean isLegacyFlywaySchemaTable(final DataSource dataSource) { + try (final Connection connection = dataSource.getConnection()) { + final DatabaseMetaData databaseMetaData = JdbcUtils.getDatabaseMetaData(connection); + + try (final ResultSet resultSet = databaseMetaData.getTables(null, null, null, null)) { + while (resultSet.next()) { + final String table = resultSet.getString(3); + LOGGER.trace("Found table {}", table); + if (LEGACY_FLYWAY_SCHEMA_TABLE.equals(table)) { + return true; + } + } + } + + return false; + } catch (SQLException e) { + LOGGER.error(e.getMessage(), e); + throw new FlywayException("Unable to obtain connection from Flyway DataSource", e); + } + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/CustomFlywayMigrationStrategy.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/CustomFlywayMigrationStrategy.java new file mode 100644 index 0000000000..13954c6a4f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/CustomFlywayMigrationStrategy.java @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.db.migration.BucketEntityV1; +import org.apache.nifi.registry.db.migration.FlowEntityV1; +import org.apache.nifi.registry.db.migration.FlowSnapshotEntityV1; +import org.apache.nifi.registry.db.migration.LegacyDataSourceFactory; +import org.apache.nifi.registry.db.migration.LegacyDatabaseService; +import org.apache.nifi.registry.db.migration.LegacyEntityMapper; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.apache.nifi.registry.service.MetadataService; +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.FlywayException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.flyway.FlywayMigrationStrategy; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; + +import javax.sql.DataSource; +import java.io.File; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +/** + * Custom Flyway migration strategy that lets us perform data migration from the original database used in the + * 0.1.0 release, to the new database. The data migration will be triggered when it is determined that new database + * is brand new AND the legacy DB properties are specified. If the primary database already contains the 'BUCKET' table, + * or if the legacy database properties are not specified, then no data migration is performed. + */ +@Component +public class CustomFlywayMigrationStrategy implements FlywayMigrationStrategy { + + private static final Logger LOGGER = LoggerFactory.getLogger(CustomFlywayMigrationStrategy.class); + + private NiFiRegistryProperties properties; + + @Autowired + public CustomFlywayMigrationStrategy(final NiFiRegistryProperties properties) { + this.properties = properties; + } + + @Override + public void migrate(Flyway flyway) { + final boolean newDatabase = isNewDatabase(flyway.getConfiguration().getDataSource()); + if (newDatabase) { + LOGGER.info("First time initializing database..."); + } else { + LOGGER.info("Found existing database..."); + } + + boolean existingLegacyDatabase = false; + if (!StringUtils.isBlank(properties.getLegacyDatabaseDirectory())) { + LOGGER.info("Found legacy database properties..."); + + final File legacyDatabaseFile = new File(properties.getLegacyDatabaseDirectory(), "nifi-registry.mv.db"); + if (legacyDatabaseFile.exists()) { + LOGGER.info("Found legacy database file..."); + existingLegacyDatabase = true; + } else { + LOGGER.info("Did not find legacy database file..."); + existingLegacyDatabase = false; + } + } + + // If newDatabase is true, then we need to run the Flyway migration first to create all the tables, then the data migration + // If newDatabase is false, then we need to run the Flyway migration to run any schema updates, but no data migration + + flyway.migrate(); + + if (newDatabase && existingLegacyDatabase) { + final LegacyDataSourceFactory legacyDataSourceFactory = new LegacyDataSourceFactory(properties); + final DataSource legacyDataSource = legacyDataSourceFactory.getDataSource(); + final DataSource primaryDataSource = flyway.getConfiguration().getDataSource(); + migrateData(legacyDataSource, primaryDataSource); + } + } + + /** + * Determines if the database represented by this data source is being initialized for the first time based on + * whether or not the table named 'BUCKET' or 'bucket' already exists. + * + * @param dataSource the data source + * @return true if the database has never been initialized before, false otherwise + */ + private boolean isNewDatabase(final DataSource dataSource) { + try (final Connection connection = dataSource.getConnection(); + final ResultSet rsUpper = connection.getMetaData().getTables(null, null, "BUCKET", null); + final ResultSet rsLower = connection.getMetaData().getTables(null, null, "bucket", null)) { + return !rsUpper.next() && !rsLower.next(); + } catch (SQLException e) { + LOGGER.error(e.getMessage(), e); + throw new FlywayException("Unable to obtain connection from Flyway DataSource", e); + } + } + + /** + * Transfers all data from the source to the destination. + * + * @param source the legacy H2 DataSource + * @param dest the new destination DataSource + */ + private void migrateData(final DataSource source, final DataSource dest) { + final LegacyDatabaseService legacyDatabaseService = new LegacyDatabaseService(source); + + final JdbcTemplate destJdbcTemplate = new JdbcTemplate(dest); + final MetadataService destMetadataService = new DatabaseMetadataService(destJdbcTemplate); + + LOGGER.info("Migrating data from legacy database to new new database..."); + + // Migrate buckets + final List sourceBuckets = legacyDatabaseService.getAllBuckets(); + LOGGER.info("Migrating {} buckets..", new Object[]{sourceBuckets.size()}); + + sourceBuckets.stream() + .map(b -> LegacyEntityMapper.createBucketEntity(b)) + .forEach(b -> destMetadataService.createBucket(b)); + + // Migrate flows + final List sourceFlows = legacyDatabaseService.getAllFlows(); + LOGGER.info("Migrating {} flows..", new Object[]{sourceFlows.size()}); + + sourceFlows.stream() + .map(f -> LegacyEntityMapper.createFlowEntity(f)) + .forEach(f -> destMetadataService.createFlow(f)); + + // Migrate flow snapshots + final List sourceSnapshots = legacyDatabaseService.getAllFlowSnapshots(); + LOGGER.info("Migrating {} flow snapshots..", new Object[]{sourceSnapshots.size()}); + + sourceSnapshots.stream() + .map(fs -> LegacyEntityMapper.createFlowSnapshotEntity(fs)) + .forEach(fs -> destMetadataService.createFlowSnapshot(fs)); + + LOGGER.info("Data migration complete!"); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/DataSourceFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/DataSourceFactory.java new file mode 100644 index 0000000000..29c132e293 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/DataSourceFactory.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db; + +import com.zaxxer.hikari.HikariDataSource; +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +import javax.sql.DataSource; + +/** + * Overriding Spring Boot's normal automatic creation of a DataSource in order to use the properties + * from NiFiRegistryProperties rather than the standard application.properties/yaml. + */ +@Configuration +public class DataSourceFactory { + + private static final Logger LOGGER = LoggerFactory.getLogger(DataSourceFactory.class); + + private final NiFiRegistryProperties properties; + + private DataSource dataSource; + + @Autowired + public DataSourceFactory(final NiFiRegistryProperties properties) { + this.properties = properties; + } + + @Bean + @Primary + public DataSource getDataSource() { + if (dataSource == null) { + dataSource = createDataSource(); + } + + return dataSource; + } + + private DataSource createDataSource() { + final String databaseUrl = properties.getDatabaseUrl(); + if (StringUtils.isBlank(databaseUrl)) { + throw new IllegalStateException(NiFiRegistryProperties.DATABASE_URL + " is required"); + } + + final String databaseDriver = properties.getDatabaseDriverClassName(); + if (StringUtils.isBlank(databaseDriver)) { + throw new IllegalStateException(NiFiRegistryProperties.DATABASE_DRIVER_CLASS_NAME + " is required"); + } + + final String databaseUsername = properties.getDatabaseUsername(); + if (StringUtils.isBlank(databaseUsername)) { + throw new IllegalStateException(NiFiRegistryProperties.DATABASE_USERNAME + " is required"); + } + + String databasePassword = properties.getDatabasePassword(); + if (StringUtils.isBlank(databasePassword)) { + throw new IllegalStateException(NiFiRegistryProperties.DATABASE_PASSWORD + " is required"); + } + + final DataSource dataSource = DataSourceBuilder + .create() + .url(databaseUrl) + .driverClassName(databaseDriver) + .username(databaseUsername) + .password(databasePassword) + .build(); + + if (dataSource instanceof HikariDataSource) { + LOGGER.info("Setting maximum pool size on HikariDataSource to {}", new Object[]{properties.getDatabaseMaxConnections()}); + ((HikariDataSource)dataSource).setMaximumPoolSize(properties.getDatabaseMaxConnections()); + } + + return dataSource; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/DatabaseKeyService.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/DatabaseKeyService.java new file mode 100644 index 0000000000..b2daf2db77 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/DatabaseKeyService.java @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db; + +import org.apache.nifi.registry.db.entity.KeyEntity; +import org.apache.nifi.registry.db.mapper.KeyEntityRowMapper; +import org.apache.nifi.registry.security.key.Key; +import org.apache.nifi.registry.security.key.KeyService; +import org.apache.nifi.registry.service.mapper.KeyMappings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; + +import java.util.UUID; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +@Service +public class DatabaseKeyService implements KeyService { + + private static final Logger logger = LoggerFactory.getLogger(DatabaseKeyService.class); + + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + private final Lock readLock = lock.readLock(); + private final Lock writeLock = lock.writeLock(); + + private JdbcTemplate jdbcTemplate; + + @Autowired + public DatabaseKeyService(final JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public Key getKey(String id) { + if (id == null) { + throw new IllegalArgumentException("Id cannot be null"); + } + + Key key = null; + readLock.lock(); + try { + final String sql = "SELECT * FROM SIGNING_KEY WHERE id = ?"; + + KeyEntity keyEntity; + try { + keyEntity = jdbcTemplate.queryForObject(sql, new KeyEntityRowMapper(), id); + } catch (EmptyResultDataAccessException e) { + keyEntity = null; + } + + if (keyEntity != null) { + key = KeyMappings.map(keyEntity); + } else { + logger.debug("No signing key found with id='" + id + "'"); + } + } finally { + readLock.unlock(); + } + return key; + } + + @Override + public Key getOrCreateKey(String tenantIdentity) { + if (tenantIdentity == null) { + throw new IllegalArgumentException("Identity cannot be null"); + } + + Key key; + writeLock.lock(); + try { + final String selectSql = "SELECT * FROM SIGNING_KEY WHERE tenant_identity = ?"; + + KeyEntity existingKeyEntity; + try { + existingKeyEntity = jdbcTemplate.queryForObject(selectSql, new KeyEntityRowMapper(), tenantIdentity); + } catch (EmptyResultDataAccessException e) { + existingKeyEntity = null; + } + + if (existingKeyEntity == null) { + logger.debug("No key found with identity='" + tenantIdentity + "'. Creating new key."); + + final KeyEntity newKeyEntity = new KeyEntity(); + newKeyEntity.setId(UUID.randomUUID().toString()); + newKeyEntity.setTenantIdentity(tenantIdentity); + newKeyEntity.setKeyValue(UUID.randomUUID().toString()); + + final String insertSql = "INSERT INTO SIGNING_KEY (ID, TENANT_IDENTITY, KEY_VALUE) VALUES (?, ?, ?)"; + jdbcTemplate.update(insertSql, newKeyEntity.getId(), newKeyEntity.getTenantIdentity(), newKeyEntity.getKeyValue()); + + key = KeyMappings.map(newKeyEntity); + } else { + key = KeyMappings.map(existingKeyEntity); + } + } finally { + writeLock.unlock(); + } + return key; + } + + @Override + public void deleteKey(String tenantIdentity) { + if (tenantIdentity == null) { + throw new IllegalArgumentException("Identity cannot be null"); + } + + writeLock.lock(); + try { + logger.debug("Deleting key with identity='" + tenantIdentity + "'."); + final String deleteSql = "DELETE FROM SIGNING_KEY WHERE tenant_identity = ?"; + jdbcTemplate.update(deleteSql, tenantIdentity); + } finally { + writeLock.unlock(); + } + + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/DatabaseMetadataService.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/DatabaseMetadataService.java new file mode 100644 index 0000000000..ed5a2fbef7 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/DatabaseMetadataService.java @@ -0,0 +1,1163 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.db.entity.BucketEntity; +import org.apache.nifi.registry.db.entity.BucketItemEntity; +import org.apache.nifi.registry.db.entity.BucketItemEntityType; +import org.apache.nifi.registry.db.entity.BundleEntity; +import org.apache.nifi.registry.db.entity.BundleVersionDependencyEntity; +import org.apache.nifi.registry.db.entity.BundleVersionEntity; +import org.apache.nifi.registry.db.entity.ExtensionAdditionalDetailsEntity; +import org.apache.nifi.registry.db.entity.ExtensionEntity; +import org.apache.nifi.registry.db.entity.ExtensionProvidedServiceApiEntity; +import org.apache.nifi.registry.db.entity.ExtensionRestrictionEntity; +import org.apache.nifi.registry.db.entity.FlowEntity; +import org.apache.nifi.registry.db.entity.FlowSnapshotEntity; +import org.apache.nifi.registry.db.entity.TagCountEntity; +import org.apache.nifi.registry.db.mapper.BucketEntityRowMapper; +import org.apache.nifi.registry.db.mapper.BucketItemEntityRowMapper; +import org.apache.nifi.registry.db.mapper.BundleEntityRowMapper; +import org.apache.nifi.registry.db.mapper.BundleVersionDependencyEntityRowMapper; +import org.apache.nifi.registry.db.mapper.BundleVersionEntityRowMapper; +import org.apache.nifi.registry.db.mapper.ExtensionEntityRowMapper; +import org.apache.nifi.registry.db.mapper.FlowEntityRowMapper; +import org.apache.nifi.registry.db.mapper.FlowSnapshotEntityRowMapper; +import org.apache.nifi.registry.db.mapper.TagCountEntityMapper; +import org.apache.nifi.registry.extension.bundle.BundleFilterParams; +import org.apache.nifi.registry.extension.bundle.BundleType; +import org.apache.nifi.registry.extension.bundle.BundleVersionFilterParams; +import org.apache.nifi.registry.extension.component.ExtensionFilterParams; +import org.apache.nifi.registry.extension.component.manifest.ExtensionType; +import org.apache.nifi.registry.extension.component.manifest.ProvidedServiceAPI; +import org.apache.nifi.registry.service.MetadataService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +@Repository +public class DatabaseMetadataService implements MetadataService { + + private final JdbcTemplate jdbcTemplate; + + @Autowired + public DatabaseMetadataService(final JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + //----------------- Buckets --------------------------------- + + @Override + public BucketEntity createBucket(final BucketEntity b) { + final String sql = "INSERT INTO BUCKET (ID, NAME, DESCRIPTION, CREATED, ALLOW_EXTENSION_BUNDLE_REDEPLOY, ALLOW_PUBLIC_READ) VALUES (?, ?, ?, ?, ?, ?)"; + jdbcTemplate.update(sql, + b.getId(), + b.getName(), + b.getDescription(), + b.getCreated(), + b.isAllowExtensionBundleRedeploy() ? 1 : 0, + b.isAllowPublicRead() ? 1 : 0); + return b; + } + + @Override + public BucketEntity getBucketById(final String bucketIdentifier) { + final String sql = "SELECT * FROM BUCKET WHERE id = ?"; + try { + return jdbcTemplate.queryForObject(sql, new BucketEntityRowMapper(), bucketIdentifier); + } catch (EmptyResultDataAccessException e) { + return null; + } + } + + @Override + public List getBucketsByName(final String name) { + final String sql = "SELECT * FROM BUCKET WHERE name = ? ORDER BY name ASC"; + return jdbcTemplate.query(sql, new Object[] {name} , new BucketEntityRowMapper()); + } + + @Override + public BucketEntity updateBucket(final BucketEntity bucket) { + final String sql = "UPDATE BUCKET SET " + + "name = ?, " + + "description = ?, " + + "allow_extension_bundle_redeploy = ?, " + + "allow_public_read = ? " + + "WHERE id = ?"; + + jdbcTemplate.update(sql, + bucket.getName(), + bucket.getDescription(), + bucket.isAllowExtensionBundleRedeploy() ? 1 : 0, + bucket.isAllowPublicRead() ? 1 : 0, + bucket.getId()); + + return bucket; + } + + @Override + public void deleteBucket(final BucketEntity bucket) { + // NOTE: Cascading deletes will delete from all child tables + final String sql = "DELETE FROM BUCKET WHERE id = ?"; + jdbcTemplate.update(sql, bucket.getId()); + } + + @Override + public List getBuckets(final Set bucketIds) { + if (bucketIds == null || bucketIds.isEmpty()) { + return Collections.emptyList(); + } + + final StringBuilder sqlBuilder = new StringBuilder("SELECT * FROM BUCKET WHERE "); + addIdentifiersInClause(sqlBuilder, "id", bucketIds); + sqlBuilder.append("ORDER BY name ASC"); + + return jdbcTemplate.query(sqlBuilder.toString(), bucketIds.toArray(), new BucketEntityRowMapper()); + } + + @Override + public List getAllBuckets() { + final String sql = "SELECT * FROM BUCKET ORDER BY name ASC"; + return jdbcTemplate.query(sql, new BucketEntityRowMapper()); + } + + //----------------- BucketItems --------------------------------- + + private static final String BASE_BUCKET_ITEMS_SQL = + "SELECT " + + "item.id as ID, " + + "item.name as NAME, " + + "item.description as DESCRIPTION, " + + "item.created as CREATED, " + + "item.modified as MODIFIED, " + + "item.item_type as ITEM_TYPE, " + + "b.id as BUCKET_ID, " + + "b.name as BUCKET_NAME ," + + "eb.bundle_type as BUNDLE_TYPE, " + + "eb.group_id as BUNDLE_GROUP_ID, " + + "eb.artifact_id as BUNDLE_ARTIFACT_ID " + + "FROM BUCKET_ITEM item " + + "INNER JOIN BUCKET b ON item.bucket_id = b.id " + + "LEFT JOIN BUNDLE eb ON item.id = eb.id "; + + @Override + public List getBucketItems(final String bucketIdentifier) { + final String sql = BASE_BUCKET_ITEMS_SQL + " WHERE item.bucket_id = ?"; + final List items = jdbcTemplate.query(sql, new Object[] { bucketIdentifier }, new BucketItemEntityRowMapper()); + return getItemsWithCounts(items); + } + + @Override + public List getBucketItems(final Set bucketIds) { + if (bucketIds == null || bucketIds.isEmpty()) { + return Collections.emptyList(); + } + + final StringBuilder sqlBuilder = new StringBuilder(BASE_BUCKET_ITEMS_SQL + " WHERE item.bucket_id IN ("); + for (int i=0; i < bucketIds.size(); i++) { + if (i > 0) { + sqlBuilder.append(", "); + } + sqlBuilder.append("?"); + } + sqlBuilder.append(")"); + + final List items = jdbcTemplate.query(sqlBuilder.toString(), bucketIds.toArray(), new BucketItemEntityRowMapper()); + return getItemsWithCounts(items); + } + + private List getItemsWithCounts(final Iterable items) { + final Map snapshotCounts = getFlowSnapshotCounts(); + final Map extensionBundleVersionCounts = getExtensionBundleVersionCounts(); + + final List itemWithCounts = new ArrayList<>(); + for (final BucketItemEntity item : items) { + if (item.getType() == BucketItemEntityType.FLOW) { + final Long snapshotCount = snapshotCounts.get(item.getId()); + if (snapshotCount != null) { + final FlowEntity flowEntity = (FlowEntity) item; + flowEntity.setSnapshotCount(snapshotCount); + } + } else if (item.getType() == BucketItemEntityType.BUNDLE) { + final Long versionCount = extensionBundleVersionCounts.get(item.getId()); + if (versionCount != null) { + final BundleEntity bundleEntity = (BundleEntity) item; + bundleEntity.setVersionCount(versionCount); + } + } + + itemWithCounts.add(item); + } + + return itemWithCounts; + } + + private Map getFlowSnapshotCounts() { + final String sql = "SELECT flow_id, count(*) FROM FLOW_SNAPSHOT GROUP BY flow_id"; + + final Map results = new HashMap<>(); + jdbcTemplate.query(sql, (rs) -> { + results.put(rs.getString(1), rs.getLong(2)); + }); + return results; + } + + private Long getFlowSnapshotCount(final String flowIdentifier) { + final String sql = "SELECT count(*) FROM FLOW_SNAPSHOT WHERE flow_id = ?"; + + return jdbcTemplate.queryForObject(sql, new Object[] {flowIdentifier}, (rs, num) -> { + return rs.getLong(1); + }); + } + + private Map getExtensionBundleVersionCounts() { + final String sql = "SELECT bundle_id, count(*) FROM BUNDLE_VERSION GROUP BY bundle_id"; + + final Map results = new HashMap<>(); + jdbcTemplate.query(sql, (rs) -> { + results.put(rs.getString(1), rs.getLong(2)); + }); + return results; + } + + private Long getExtensionBundleVersionCount(final String extensionBundleIdentifier) { + final String sql = "SELECT count(*) FROM BUNDLE_VERSION WHERE bundle_id = ?"; + + return jdbcTemplate.queryForObject(sql, new Object[] {extensionBundleIdentifier}, (rs, num) -> { + return rs.getLong(1); + }); + } + + //----------------- Flows --------------------------------- + + @Override + public FlowEntity createFlow(final FlowEntity flow) { + final String itemSql = "INSERT INTO BUCKET_ITEM (ID, NAME, DESCRIPTION, CREATED, MODIFIED, ITEM_TYPE, BUCKET_ID) VALUES (?, ?, ?, ?, ?, ?, ?)"; + + jdbcTemplate.update(itemSql, + flow.getId(), + flow.getName(), + flow.getDescription(), + flow.getCreated(), + flow.getModified(), + flow.getType().toString(), + flow.getBucketId()); + + final String flowSql = "INSERT INTO FLOW (ID) VALUES (?)"; + + jdbcTemplate.update(flowSql, flow.getId()); + + return flow; + } + + @Override + public FlowEntity getFlowById(final String flowIdentifier) { + final String sql = "SELECT * FROM FLOW f, BUCKET_ITEM item WHERE f.id = ? AND item.id = f.id"; + try { + return jdbcTemplate.queryForObject(sql, new FlowEntityRowMapper(), flowIdentifier); + } catch (EmptyResultDataAccessException e) { + return null; + } + } + + @Override + public FlowEntity getFlowByIdWithSnapshotCounts(final String flowIdentifier) { + final FlowEntity flowEntity = getFlowById(flowIdentifier); + if (flowEntity == null) { + return flowEntity; + } + + final Long snapshotCount = getFlowSnapshotCount(flowIdentifier); + if (snapshotCount != null) { + flowEntity.setSnapshotCount(snapshotCount); + } + + return flowEntity; + } + + @Override + public List getFlowsByName(final String name) { + final String sql = "SELECT * FROM FLOW f, BUCKET_ITEM item WHERE item.name = ? AND item.id = f.id"; + return jdbcTemplate.query(sql, new Object[] {name}, new FlowEntityRowMapper()); + } + + @Override + public List getFlowsByName(final String bucketIdentifier, final String name) { + final String sql = "SELECT * FROM FLOW f, BUCKET_ITEM item WHERE item.name = ? AND item.id = f.id AND item.bucket_id = ?"; + return jdbcTemplate.query(sql, new Object[] {name, bucketIdentifier}, new FlowEntityRowMapper()); + } + + @Override + public List getFlowsByBucket(final String bucketIdentifier) { + final String sql = "SELECT * FROM FLOW f, BUCKET_ITEM item WHERE item.bucket_id = ? AND item.id = f.id"; + final List flows = jdbcTemplate.query(sql, new Object[] {bucketIdentifier}, new FlowEntityRowMapper()); + + final Map snapshotCounts = getFlowSnapshotCounts(); + for (final FlowEntity flowEntity : flows) { + final Long snapshotCount = snapshotCounts.get(flowEntity.getId()); + if (snapshotCount != null) { + flowEntity.setSnapshotCount(snapshotCount); + } + } + + return flows; + } + + @Override + public FlowEntity updateFlow(final FlowEntity flow) { + flow.setModified(new Date()); + + final String sql = "UPDATE BUCKET_ITEM SET name = ?, description = ?, modified = ? WHERE id = ?"; + jdbcTemplate.update(sql, flow.getName(), flow.getDescription(), flow.getModified(), flow.getId()); + return flow; + } + + @Override + public void deleteFlow(final FlowEntity flow) { + // NOTE: Cascading deletes will delete from child tables + final String itemDeleteSql = "DELETE FROM BUCKET_ITEM WHERE id = ?"; + jdbcTemplate.update(itemDeleteSql, flow.getId()); + } + + //----------------- Flow Snapshots --------------------------------- + + @Override + public FlowSnapshotEntity createFlowSnapshot(final FlowSnapshotEntity flowSnapshot) { + final String sql = "INSERT INTO FLOW_SNAPSHOT (FLOW_ID, VERSION, CREATED, CREATED_BY, COMMENTS) VALUES (?, ?, ?, ?, ?)"; + + jdbcTemplate.update(sql, + flowSnapshot.getFlowId(), + flowSnapshot.getVersion(), + flowSnapshot.getCreated(), + flowSnapshot.getCreatedBy(), + flowSnapshot.getComments()); + + return flowSnapshot; + } + + @Override + public FlowSnapshotEntity getFlowSnapshot(final String flowIdentifier, final Integer version) { + final String sql = + "SELECT " + + "fs.flow_id, " + + "fs.version, " + + "fs.created, " + + "fs.created_by, " + + "fs.comments " + + "FROM " + + "FLOW_SNAPSHOT fs, " + + "FLOW f, " + + "BUCKET_ITEM item " + + "WHERE " + + "item.id = f.id AND " + + "f.id = ? AND " + + "f.id = fs.flow_id AND " + + "fs.version = ?"; + + try { + return jdbcTemplate.queryForObject(sql, new FlowSnapshotEntityRowMapper(), + flowIdentifier, version); + } catch (EmptyResultDataAccessException e) { + return null; + } + } + + @Override + public FlowSnapshotEntity getLatestSnapshot(final String flowIdentifier) { + final String sql = "SELECT * FROM FLOW_SNAPSHOT WHERE flow_id = ? ORDER BY version DESC LIMIT 1"; + + try { + return jdbcTemplate.queryForObject(sql, new FlowSnapshotEntityRowMapper(), flowIdentifier); + } catch (EmptyResultDataAccessException e) { + return null; + } + } + + @Override + public List getSnapshots(final String flowIdentifier) { + final String sql = + "SELECT " + + "fs.flow_id, " + + "fs.version, " + + "fs.created, " + + "fs.created_by, " + + "fs.comments " + + "FROM " + + "FLOW_SNAPSHOT fs, " + + "FLOW f, " + + "BUCKET_ITEM item " + + "WHERE " + + "item.id = f.id AND " + + "f.id = ? AND " + + "f.id = fs.flow_id"; + + final Object[] args = new Object[] { flowIdentifier }; + return jdbcTemplate.query(sql, args, new FlowSnapshotEntityRowMapper()); + } + + @Override + public void deleteFlowSnapshot(final FlowSnapshotEntity flowSnapshot) { + final String sql = "DELETE FROM FLOW_SNAPSHOT WHERE flow_id = ? AND version = ?"; + jdbcTemplate.update(sql, flowSnapshot.getFlowId(), flowSnapshot.getVersion()); + } + + //----------------- Extension Bundles --------------------------------- + + @Override + public BundleEntity createBundle(final BundleEntity extensionBundle) { + final String itemSql = + "INSERT INTO BUCKET_ITEM (" + + "ID, " + + "NAME, " + + "DESCRIPTION, " + + "CREATED, " + + "MODIFIED, " + + "ITEM_TYPE, " + + "BUCKET_ID) " + + "VALUES (?, ?, ?, ?, ?, ?, ?)"; + + jdbcTemplate.update(itemSql, + extensionBundle.getId(), + extensionBundle.getName(), + extensionBundle.getDescription(), + extensionBundle.getCreated(), + extensionBundle.getModified(), + extensionBundle.getType().name(), + extensionBundle.getBucketId()); + + final String bundleSql = + "INSERT INTO BUNDLE (" + + "ID, " + + "BUCKET_ID, " + + "BUNDLE_TYPE, " + + "GROUP_ID, " + + "ARTIFACT_ID) " + + "VALUES (?, ?, ?, ?, ?)"; + + jdbcTemplate.update(bundleSql, + extensionBundle.getId(), + extensionBundle.getBucketId(), + extensionBundle.getBundleType().name(), + extensionBundle.getGroupId(), + extensionBundle.getArtifactId()); + + return extensionBundle; + } + + private static final String BASE_BUNDLE_SQL = + "SELECT " + + "item.id as ID," + + "item.name as NAME, " + + "item.description as DESCRIPTION, " + + "item.created as CREATED, " + + "item.modified as MODIFIED, " + + "eb.bundle_type as BUNDLE_TYPE, " + + "eb.group_id as GROUP_ID, " + + "eb.artifact_id as ARTIFACT_ID, " + + "b.id as BUCKET_ID, " + + "b.name as BUCKET_NAME " + + "FROM " + + "BUNDLE eb, " + + "BUCKET_ITEM item," + + "BUCKET b " + + "WHERE " + + "eb.id = item.id AND " + + "item.bucket_id = b.id"; + + @Override + public BundleEntity getBundle(final String extensionBundleId) { + final StringBuilder sqlBuilder = new StringBuilder(BASE_BUNDLE_SQL).append(" AND eb.id = ?"); + try { + final BundleEntity entity = jdbcTemplate.queryForObject(sqlBuilder.toString(), new BundleEntityRowMapper(), extensionBundleId); + + final Long versionCount = getExtensionBundleVersionCount(extensionBundleId); + if (versionCount != null) { + entity.setVersionCount(versionCount); + } + + return entity; + } catch (EmptyResultDataAccessException e) { + return null; + } + } + + @Override + public BundleEntity getBundle(final String bucketId, final String groupId, final String artifactId) { + final StringBuilder sqlBuilder = new StringBuilder(BASE_BUNDLE_SQL) + .append(" AND eb.bucket_id = ? ") + .append("AND eb.group_id = ? ") + .append("AND eb.artifact_id = ? "); + + try { + final BundleEntity entity = jdbcTemplate.queryForObject(sqlBuilder.toString(), new BundleEntityRowMapper(), bucketId, groupId, artifactId); + + final Long versionCount = getExtensionBundleVersionCount(entity.getId()); + if (versionCount != null) { + entity.setVersionCount(versionCount); + } + + return entity; + } catch (EmptyResultDataAccessException e) { + return null; + } + } + + @Override + public List getBundles(final Set bucketIds, final BundleFilterParams filterParams) { + if (bucketIds == null || bucketIds.isEmpty()) { + return Collections.emptyList(); + } + + final List args = new ArrayList<>(); + + final StringBuilder sqlBuilder = new StringBuilder( + "SELECT " + + "item.id as ID, " + + "item.name as NAME, " + + "item.description as DESCRIPTION, " + + "item.created as CREATED, " + + "item.modified as MODIFIED, " + + "item.item_type as ITEM_TYPE, " + + "b.id as BUCKET_ID, " + + "b.name as BUCKET_NAME ," + + "eb.bundle_type as BUNDLE_TYPE, " + + "eb.group_id as GROUP_ID, " + + "eb.artifact_id as ARTIFACT_ID " + + "FROM " + + "BUNDLE eb, " + + "BUCKET_ITEM item," + + "BUCKET b " + + "WHERE " + + "item.id = eb.id AND " + + "b.id = item.bucket_id"); + + if (filterParams != null) { + final String bucketName = filterParams.getBucketName(); + if (!StringUtils.isBlank(bucketName)) { + sqlBuilder.append(" AND b.name LIKE ? "); + args.add(bucketName); + } + + final String groupId = filterParams.getGroupId(); + if (!StringUtils.isBlank(groupId)) { + sqlBuilder.append(" AND eb.group_id LIKE ? "); + args.add(groupId); + } + + final String artifactId = filterParams.getArtifactId(); + if (!StringUtils.isBlank(artifactId)) { + sqlBuilder.append(" AND eb.artifact_id LIKE ? "); + args.add(artifactId); + } + } + + sqlBuilder.append(" AND "); + addIdentifiersInClause(sqlBuilder, "item.bucket_id", bucketIds); + sqlBuilder.append("ORDER BY eb.group_id ASC, eb.artifact_id ASC"); + + args.addAll(bucketIds); + + final List bundleEntities = jdbcTemplate.query(sqlBuilder.toString(), args.toArray(), new BundleEntityRowMapper()); + return populateVersionCounts(bundleEntities); + } + + @Override + public List getBundlesByBucket(final String bucketId) { + final StringBuilder sqlBuilder = new StringBuilder(BASE_BUNDLE_SQL) + .append(" AND b.id = ?") + .append(" ORDER BY eb.group_id ASC, eb.artifact_id ASC"); + + final List bundles = jdbcTemplate.query(sqlBuilder.toString(), new Object[]{bucketId}, new BundleEntityRowMapper()); + return populateVersionCounts(bundles); + } + + @Override + public List getBundlesByBucketAndGroup(String bucketId, String groupId) { + final StringBuilder sqlBuilder = new StringBuilder(BASE_BUNDLE_SQL) + .append(" AND b.id = ?") + .append(" AND eb.group_id = ?") + .append(" ORDER BY eb.group_id ASC, eb.artifact_id ASC"); + + final List bundles = jdbcTemplate.query(sqlBuilder.toString(), new Object[]{bucketId, groupId}, new BundleEntityRowMapper()); + return populateVersionCounts(bundles); + } + + private List populateVersionCounts(final List bundles) { + if (!bundles.isEmpty()) { + final Map versionCounts = getExtensionBundleVersionCounts(); + for (final BundleEntity entity : bundles) { + final Long versionCount = versionCounts.get(entity.getId()); + if (versionCount != null) { + entity.setVersionCount(versionCount); + } + } + } + + return bundles; + } + + @Override + public void deleteBundle(final BundleEntity extensionBundle) { + deleteBundle(extensionBundle.getId()); + } + + @Override + public void deleteBundle(final String extensionBundleId) { + // NOTE: All of the foreign key constraints for extension related tables are set to cascade on delete + final String itemDeleteSql = "DELETE FROM BUCKET_ITEM WHERE id = ?"; + jdbcTemplate.update(itemDeleteSql, extensionBundleId); + } + + //----------------- Extension Bundle Versions --------------------------------- + + @Override + public BundleVersionEntity createBundleVersion(final BundleVersionEntity extensionBundleVersion) { + final String sql = + "INSERT INTO BUNDLE_VERSION (" + + "ID, " + + "BUNDLE_ID, " + + "VERSION, " + + "CREATED, " + + "CREATED_BY, " + + "DESCRIPTION, " + + "SHA_256_HEX, " + + "SHA_256_SUPPLIED," + + "CONTENT_SIZE, " + + "SYSTEM_API_VERSION, " + + "BUILD_TOOL, " + + "BUILD_FLAGS, " + + "BUILD_BRANCH, " + + "BUILD_TAG, " + + "BUILD_REVISION, " + + "BUILT, " + + "BUILT_BY" + + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + + jdbcTemplate.update(sql, + extensionBundleVersion.getId(), + extensionBundleVersion.getBundleId(), + extensionBundleVersion.getVersion(), + extensionBundleVersion.getCreated(), + extensionBundleVersion.getCreatedBy(), + extensionBundleVersion.getDescription(), + extensionBundleVersion.getSha256Hex(), + extensionBundleVersion.getSha256Supplied() ? 1 : 0, + extensionBundleVersion.getContentSize(), + extensionBundleVersion.getSystemApiVersion(), + extensionBundleVersion.getBuildTool(), + extensionBundleVersion.getBuildFlags(), + extensionBundleVersion.getBuildBranch(), + extensionBundleVersion.getBuildTag(), + extensionBundleVersion.getBuildRevision(), + extensionBundleVersion.getBuilt(), + extensionBundleVersion.getBuiltBy()); + + return extensionBundleVersion; + } + + private static final String BASE_EXTENSION_BUNDLE_VERSION_SQL = + "SELECT " + + "ebv.id AS ID," + + "ebv.bundle_id AS BUNDLE_ID, " + + "ebv.version AS VERSION, " + + "ebv.created AS CREATED, " + + "ebv.created_by AS CREATED_BY, " + + "ebv.description AS DESCRIPTION, " + + "ebv.sha_256_hex AS SHA_256_HEX, " + + "ebv.sha_256_supplied AS SHA_256_SUPPLIED ," + + "ebv.content_size AS CONTENT_SIZE, " + + "ebv.system_api_version AS SYSTEM_API_VERSION, " + + "ebv.build_tool AS BUILD_TOOL, " + + "ebv.build_flags AS BUILD_FLAGS, " + + "ebv.build_branch AS BUILD_BRANCH, " + + "ebv.build_tag AS BUILD_TAG, " + + "ebv.build_revision AS BUILD_REVISION, " + + "ebv.built AS BUILT, " + + "ebv.built_by AS BUILT_BY, " + + "eb.bucket_id AS BUCKET_ID, " + + "eb.group_id AS GROUP_ID, " + + "eb.artifact_id AS ARTIFACT_ID " + + "FROM BUNDLE eb, BUNDLE_VERSION ebv " + + "WHERE eb.id = ebv.bundle_id "; + + @Override + public BundleVersionEntity getBundleVersion(final String extensionBundleId, final String version) { + final String sql = BASE_EXTENSION_BUNDLE_VERSION_SQL + + " AND ebv.bundle_id = ? AND ebv.version = ?"; + try { + return jdbcTemplate.queryForObject(sql, new BundleVersionEntityRowMapper(), extensionBundleId, version); + } catch (EmptyResultDataAccessException e) { + return null; + } + } + + @Override + public BundleVersionEntity getBundleVersion(final String bucketId, final String groupId, final String artifactId, final String version) { + final String sql = BASE_EXTENSION_BUNDLE_VERSION_SQL + + "AND eb.bucket_id = ? " + + "AND eb.group_id = ? " + + "AND eb.artifact_id = ? " + + "AND ebv.version = ?"; + + try { + return jdbcTemplate.queryForObject(sql, new BundleVersionEntityRowMapper(), bucketId, groupId, artifactId, version); + } catch (EmptyResultDataAccessException e) { + return null; + } + } + + @Override + public List getBundleVersions(final Set bucketIdentifiers, final BundleVersionFilterParams filterParams) { + if (bucketIdentifiers == null || bucketIdentifiers.isEmpty()) { + return Collections.emptyList(); + } + + final List args = new ArrayList<>(); + final StringBuilder sqlBuilder = new StringBuilder(BASE_EXTENSION_BUNDLE_VERSION_SQL); + + if (filterParams != null) { + final String groupId = filterParams.getGroupId(); + if (!StringUtils.isBlank(groupId)) { + sqlBuilder.append(" AND eb.group_id LIKE ? "); + args.add(groupId); + } + + final String artifactId = filterParams.getArtifactId(); + if (!StringUtils.isBlank(artifactId)) { + sqlBuilder.append(" AND eb.artifact_id LIKE ? "); + args.add(artifactId); + } + + final String version = filterParams.getVersion(); + if (!StringUtils.isBlank(version)) { + sqlBuilder.append(" AND ebv.version LIKE ? "); + args.add(version); + } + } + + sqlBuilder.append(" AND "); + addIdentifiersInClause(sqlBuilder, "eb.bucket_id", bucketIdentifiers); + args.addAll(bucketIdentifiers); + + final List bundleVersionEntities = jdbcTemplate.query( + sqlBuilder.toString(), args.toArray(), new BundleVersionEntityRowMapper()); + + return bundleVersionEntities; + } + + private void addIdentifiersInClause(StringBuilder sqlBuilder, String idFieldName, Set identifiers) { + sqlBuilder.append(idFieldName).append(" IN ("); + for (int i = 0; i < identifiers.size(); i++) { + if (i > 0) { + sqlBuilder.append(", "); + } + sqlBuilder.append("?"); + } + sqlBuilder.append(") "); + } + + @Override + public List getBundleVersions(final String extensionBundleId) { + final String sql = BASE_EXTENSION_BUNDLE_VERSION_SQL + " AND ebv.bundle_id = ?"; + return jdbcTemplate.query(sql, new Object[]{extensionBundleId}, new BundleVersionEntityRowMapper()); + } + + @Override + public List getBundleVersions(final String bucketId, final String groupId, final String artifactId) { + final String sql = BASE_EXTENSION_BUNDLE_VERSION_SQL + + "AND eb.bucket_id = ? " + + "AND eb.group_id = ? " + + "AND eb.artifact_id = ? "; + + final Object[] args = {bucketId, groupId, artifactId}; + return jdbcTemplate.query(sql, args, new BundleVersionEntityRowMapper()); + } + + @Override + public List getBundleVersionsGlobal(final String groupId, final String artifactId, final String version) { + final String sql = BASE_EXTENSION_BUNDLE_VERSION_SQL + + "AND eb.group_id = ? " + + "AND eb.artifact_id = ? " + + "AND ebv.version = ?"; + + final Object[] args = {groupId, artifactId, version}; + return jdbcTemplate.query(sql, args, new BundleVersionEntityRowMapper()); + } + + @Override + public void deleteBundleVersion(final BundleVersionEntity extensionBundleVersion) { + deleteBundleVersion(extensionBundleVersion.getId()); + } + + @Override + public void deleteBundleVersion(final String extensionBundleVersionId) { + // NOTE: All of the foreign key constraints for extension related tables are set to cascade on delete + final String sql = "DELETE FROM BUNDLE_VERSION WHERE id = ?"; + jdbcTemplate.update(sql, extensionBundleVersionId); + } + + //------------ Extension Bundle Version Dependencies ------------ + + @Override + public BundleVersionDependencyEntity createDependency(final BundleVersionDependencyEntity dependencyEntity) { + final String dependencySql = + "INSERT INTO BUNDLE_VERSION_DEPENDENCY (" + + "ID, " + + "BUNDLE_VERSION_ID, " + + "GROUP_ID, " + + "ARTIFACT_ID, " + + "VERSION " + + ") VALUES (?, ?, ?, ?, ?)"; + + jdbcTemplate.update(dependencySql, + dependencyEntity.getId(), + dependencyEntity.getExtensionBundleVersionId(), + dependencyEntity.getGroupId(), + dependencyEntity.getArtifactId(), + dependencyEntity.getVersion()); + + return dependencyEntity; + } + + @Override + public List getDependenciesForBundleVersion(final String extensionBundleVersionId) { + final String sql = "SELECT * FROM BUNDLE_VERSION_DEPENDENCY WHERE bundle_version_id = ?"; + final Object[] args = {extensionBundleVersionId}; + return jdbcTemplate.query(sql, args, new BundleVersionDependencyEntityRowMapper()); + } + + + //----------------- Extensions --------------------------------- + + private static String BASE_EXTENSION_SQL = + "SELECT " + + "e.id AS ID, " + + "e.bundle_version_id AS BUNDLE_VERSION_ID, " + + "e.name AS NAME, " + + "e.display_name AS DISPLAY_NAME, " + + "e.type AS TYPE, " + + "e.content AS CONTENT," + + "e.has_additional_details AS HAS_ADDITIONAL_DETAILS, " + + "eb.id AS BUNDLE_ID, " + + "eb.group_id AS GROUP_ID, " + + "eb.artifact_id AS ARTIFACT_ID, " + + "eb.bundle_type AS BUNDLE_TYPE, " + + "ebv.version AS VERSION, " + + "ebv.system_api_version AS SYSTEM_API_VERSION, " + + "b.id AS BUCKET_ID, " + + "b.name as BUCKET_NAME " + + "FROM " + + "EXTENSION e, " + + "BUNDLE_VERSION ebv, " + + "BUNDLE eb," + + "BUCKET b " + + "WHERE " + + "e.bundle_version_id = ebv.id AND " + + "ebv.bundle_id = eb.id AND " + + "eb.bucket_id = b.id "; + + @Override + public ExtensionEntity createExtension(final ExtensionEntity extension) { + final String insertExtensionSql = + "INSERT INTO EXTENSION (" + + "ID, " + + "BUNDLE_VERSION_ID, " + + "NAME, " + + "DISPLAY_NAME, " + + "TYPE, " + + "CONTENT, " + + "ADDITIONAL_DETAILS, " + + "HAS_ADDITIONAL_DETAILS " + + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; + + jdbcTemplate.update(insertExtensionSql, + extension.getId(), + extension.getBundleVersionId(), + extension.getName(), + extension.getDisplayName(), + extension.getExtensionType().name(), + extension.getContent(), + extension.getAdditionalDetails(), + extension.getAdditionalDetails() != null ? 1 : 0 + ); + + // insert tags... + final String insertTagSql = "INSERT INTO EXTENSION_TAG (EXTENSION_ID, TAG) VALUES (?, ?);"; + + final Set tags = extension.getTags(); + if (tags != null) { + for (final String tag : tags) { + if (tag != null) { + final String normalizedTag = tag.trim().toLowerCase(); + if (!normalizedTag.isEmpty()) { + jdbcTemplate.update(insertTagSql, extension.getId(), normalizedTag); + } + } + } + } + + // insert provided service APIs... + final Set providedServiceApis = extension.getProvidedServiceApis(); + if (providedServiceApis != null) { + providedServiceApis.forEach(p -> createProvidedServiceApi(p)); + } + + // insert restrictions... + final Set restrictions = extension.getRestrictions(); + if (restrictions != null) { + restrictions.forEach(r -> createRestriction(r)); + } + + return extension; + } + + @Override + public ExtensionEntity getExtensionById(final String id) { + final String selectSql = BASE_EXTENSION_SQL + " AND e.id = ?"; + try { + return jdbcTemplate.queryForObject(selectSql, new ExtensionEntityRowMapper(), id); + } catch (EmptyResultDataAccessException e) { + return null; + } + } + + @Override + public ExtensionEntity getExtensionByName(final String bundleVersionId, final String name) { + final String selectSql = BASE_EXTENSION_SQL + " AND e.bundle_version_id = ? AND e.name = ?"; + try { + return jdbcTemplate.queryForObject(selectSql, new ExtensionEntityRowMapper(), bundleVersionId, name); + } catch (EmptyResultDataAccessException e) { + return null; + } + } + + @Override + public ExtensionAdditionalDetailsEntity getExtensionAdditionalDetails(final String bundleVersionId, final String name) { + final String selectSql = "SELECT id, additional_details FROM EXTENSION WHERE bundle_version_id = ? AND name = ?"; + try { + final Object[] args = {bundleVersionId, name}; + return jdbcTemplate.queryForObject(selectSql, args, (rs, i) -> { + final ExtensionAdditionalDetailsEntity entity = new ExtensionAdditionalDetailsEntity(); + entity.setExtensionId(rs.getString("ID")); + entity.setAdditionalDetails(Optional.ofNullable(rs.getString("ADDITIONAL_DETAILS"))); + return entity; + }); + } catch (EmptyResultDataAccessException e) { + return null; + } + } + + @Override + public List getExtensions(final Set bucketIdentifiers, final ExtensionFilterParams filterParams) { + if (bucketIdentifiers == null || bucketIdentifiers.isEmpty()) { + return Collections.emptyList(); + } + + final List args = new ArrayList<>(); + + final StringBuilder sqlBuilder = new StringBuilder(BASE_EXTENSION_SQL); + sqlBuilder.append(" AND "); + addIdentifiersInClause(sqlBuilder, "eb.bucket_id", bucketIdentifiers); + args.addAll(bucketIdentifiers); + + if (filterParams != null) { + final BundleType bundleType = filterParams.getBundleType(); + if (bundleType != null) { + sqlBuilder.append(" AND eb.bundle_type = ?"); + args.add(bundleType.name()); + } + + final ExtensionType extensionType = filterParams.getExtensionType(); + if (extensionType != null) { + sqlBuilder.append(" AND e.type = ?"); + args.add(extensionType.name()); + } + + final Collection tags = filterParams.getTags(); + if (tags != null && !tags.isEmpty()) { + sqlBuilder.append(" AND e.id IN (") + .append(" SELECT et.extension_id FROM EXTENSION_TAG et WHERE "); + + boolean first = true; + for (final String tag : tags) { + if (!first) { + sqlBuilder.append(" OR "); + } + sqlBuilder.append(" et.tag = ? "); + args.add(tag.trim().toLowerCase()); + first = false; + } + + sqlBuilder.append(")"); + } + } + + sqlBuilder.append(" ORDER BY e.name ASC"); + return jdbcTemplate.query(sqlBuilder.toString(), args.toArray(), new ExtensionEntityRowMapper()); + } + + @Override + public List getExtensionsByProvidedServiceApi(final Set bucketIdentifiers, final ProvidedServiceAPI providedServiceAPI) { + if (bucketIdentifiers == null || bucketIdentifiers.isEmpty()) { + return Collections.emptyList(); + } + + final List args = new ArrayList<>(); + + final StringBuilder sqlBuilder = new StringBuilder(BASE_EXTENSION_SQL); + sqlBuilder.append(" AND "); + addIdentifiersInClause(sqlBuilder, "eb.bucket_id", bucketIdentifiers); + args.addAll(bucketIdentifiers); + + sqlBuilder.append(" AND e.id IN (") + .append(" SELECT ep.extension_id FROM EXTENSION_PROVIDED_SERVICE_API ep") + .append(" WHERE ep.class_name = ? ") + .append(" AND ep.group_id = ? ") + .append(" AND ep.artifact_id = ? ") + .append(" AND ep.version = ?") + .append(")") + .toString(); + + args.add(providedServiceAPI.getClassName()); + args.add(providedServiceAPI.getGroupId()); + args.add(providedServiceAPI.getArtifactId()); + args.add(providedServiceAPI.getVersion()); + + return jdbcTemplate.query(sqlBuilder.toString(), args.toArray(), new ExtensionEntityRowMapper()); + } + + @Override + public List getExtensionsByBundleVersionId(final String bundleVersionId) { + final String selectSql = BASE_EXTENSION_SQL + " AND e.bundle_version_id = ?"; + final Object[] args = { bundleVersionId }; + return jdbcTemplate.query(selectSql, args, new ExtensionEntityRowMapper()); + } + + @Override + public List getAllExtensionTags() { + final String selectSql = + "SELECT tag as TAG, count(*) as COUNT " + + "FROM EXTENSION_TAG " + + "GROUP BY tag " + + "ORDER BY tag ASC"; + + return jdbcTemplate.query(selectSql, new TagCountEntityMapper()); + } + + @Override + public void deleteExtension(final ExtensionEntity extension) { + // NOTE: All of the foreign key constraints for extension related tables are set to cascade on delete + final String deleteSql = "DELETE FROM EXTENSION WHERE id = ?"; + jdbcTemplate.update(deleteSql, extension.getId()); + } + + //----------------- Extension Provided Service APIs -------------------- + + private ExtensionProvidedServiceApiEntity createProvidedServiceApi(final ExtensionProvidedServiceApiEntity providedServiceApi) { + final String sql = + "INSERT INTO EXTENSION_PROVIDED_SERVICE_API (" + + "ID, " + + "EXTENSION_ID, " + + "CLASS_NAME, " + + "GROUP_ID, " + + "ARTIFACT_ID, " + + "VERSION) " + + "VALUES (?, ?, ?, ?, ?, ?)"; + + jdbcTemplate.update(sql, + providedServiceApi.getId(), + providedServiceApi.getExtensionId(), + providedServiceApi.getClassName(), + providedServiceApi.getGroupId(), + providedServiceApi.getArtifactId(), + providedServiceApi.getVersion() + ); + + return providedServiceApi; + } + + //----------------- Extension Restrictions -------------------- + + private ExtensionRestrictionEntity createRestriction(final ExtensionRestrictionEntity restriction) { + final String sql = + "INSERT INTO EXTENSION_RESTRICTION (" + + "ID, " + + "EXTENSION_ID, " + + "REQUIRED_PERMISSION, " + + "EXPLANATION) " + + "VALUES (?, ?, ?, ?)"; + + jdbcTemplate.update(sql, + restriction.getId(), + restriction.getExtensionId(), + restriction.getRequiredPermission(), + restriction.getExplanation()); + + return restriction; + } + + //----------------- Fields --------------------------------- + + @Override + public Set getBucketFields() { + final Set fields = new LinkedHashSet<>(); + fields.add("ID"); + fields.add("NAME"); + fields.add("DESCRIPTION"); + fields.add("CREATED"); + return fields; + } + + @Override + public Set getBucketItemFields() { + final Set fields = new LinkedHashSet<>(); + fields.add("ID"); + fields.add("NAME"); + fields.add("DESCRIPTION"); + fields.add("CREATED"); + fields.add("MODIFIED"); + fields.add("ITEM_TYPE"); + fields.add("BUCKET_ID"); + return fields; + } + + @Override + public Set getFlowFields() { + final Set fields = new LinkedHashSet<>(); + fields.add("ID"); + fields.add("NAME"); + fields.add("DESCRIPTION"); + fields.add("CREATED"); + fields.add("MODIFIED"); + fields.add("ITEM_TYPE"); + fields.add("BUCKET_ID"); + return fields; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BucketEntity.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BucketEntity.java new file mode 100644 index 0000000000..b6bf12b544 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BucketEntity.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.entity; + +import java.util.Date; +import java.util.Objects; + +public class BucketEntity { + + private String id; + + private String name; + + private String description; + + private Date created; + + private boolean allowExtensionBundleRedeploy; + + private boolean allowPublicRead; + + + 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; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public boolean isAllowExtensionBundleRedeploy() { + return allowExtensionBundleRedeploy; + } + + public void setAllowExtensionBundleRedeploy(final boolean allowExtensionBundleRedeploy) { + this.allowExtensionBundleRedeploy = allowExtensionBundleRedeploy; + } + + public boolean isAllowPublicRead() { + return allowPublicRead; + } + + public void setAllowPublicRead(boolean allowPublicRead) { + this.allowPublicRead = allowPublicRead; + } + + @Override + public int hashCode() { + return Objects.hashCode(this.id); + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + + final BucketEntity other = (BucketEntity) obj; + return Objects.equals(this.id, other.id); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BucketItemEntity.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BucketItemEntity.java new file mode 100644 index 0000000000..cdfa963372 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BucketItemEntity.java @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.entity; + +import java.util.Date; +import java.util.Objects; + +public class BucketItemEntity { + + private String id; + + private String name; + + private String description; + + private Date created; + + private Date modified; + + // NOTE: sub-classes should ensure that the type is set appropriately by overriding the getter/setter + private BucketItemEntityType type; + + private String bucketId; + + private String bucketName; + + + 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; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Date getModified() { + return modified; + } + + public void setModified(Date modified) { + this.modified = modified; + } + + public BucketItemEntityType getType() { + return type; + } + + public void setType(BucketItemEntityType type) { + this.type = type; + } + + public String getBucketId() { + return bucketId; + } + + public void setBucketId(String bucketId) { + this.bucketId = bucketId; + } + + public String getBucketName() { + return bucketName; + } + + public void setBucketName(String bucketName) { + this.bucketName = bucketName; + } + + @Override + public int hashCode() { + return Objects.hashCode(this.id); + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + + final BucketItemEntity other = (BucketItemEntity) obj; + return Objects.equals(this.id, other.id); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BucketItemEntityType.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BucketItemEntityType.java new file mode 100644 index 0000000000..cc883b976c --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BucketItemEntityType.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.entity; + +/** + * Possible types of BucketItemEntity. + */ +public enum BucketItemEntityType { + + FLOW(Values.FLOW), + + BUNDLE(Values.BUNDLE); + + + private final String value; + + BucketItemEntityType(final String value) { + this.value = value; + } + + @Override + public String toString() { + return value; + } + + // need these constants to reference from @DiscriminatorValue + public static class Values { + public static final String FLOW = "FLOW"; + public static final String BUNDLE = "BUNDLE"; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BundleEntity.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BundleEntity.java new file mode 100644 index 0000000000..1e3af7cc47 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BundleEntity.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.entity; + +import org.apache.nifi.registry.extension.bundle.BundleType; + +public class BundleEntity extends BucketItemEntity { + + private String groupId; + private String artifactId; + private BundleType bundleType; + + private long versionCount; + + public BundleEntity() { + setType(BucketItemEntityType.BUNDLE); + } + + public BundleType getBundleType() { + return bundleType; + } + + public void setBundleType(BundleType bundleType) { + this.bundleType = bundleType; + } + + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + public String getArtifactId() { + return artifactId; + } + + public void setArtifactId(String artifactId) { + this.artifactId = artifactId; + } + + public long getVersionCount() { + return versionCount; + } + + public void setVersionCount(long versionCount) { + this.versionCount = versionCount; + } + + @Override + public void setType(BucketItemEntityType type) { + if (BucketItemEntityType.BUNDLE != type) { + throw new IllegalStateException("Must set type to " + BucketItemEntityType.Values.BUNDLE); + } + super.setType(type); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BundleVersionDependencyEntity.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BundleVersionDependencyEntity.java new file mode 100644 index 0000000000..64502c4f04 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BundleVersionDependencyEntity.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.entity; + +public class BundleVersionDependencyEntity { + + // Database id for this specific dependency + private String id; + + // Foreign key to the extension bundle version this dependency goes with + private String extensionBundleVersionId; + + // The bundle coordinates for this dependency + private String groupId; + private String artifactId; + private String version; + + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getExtensionBundleVersionId() { + return extensionBundleVersionId; + } + + public void setExtensionBundleVersionId(String extensionBundleVersionId) { + this.extensionBundleVersionId = extensionBundleVersionId; + } + + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + public String getArtifactId() { + return artifactId; + } + + public void setArtifactId(String artifactId) { + this.artifactId = artifactId; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BundleVersionEntity.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BundleVersionEntity.java new file mode 100644 index 0000000000..35a23a21b2 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BundleVersionEntity.java @@ -0,0 +1,228 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.entity; + +import java.util.Date; + +public class BundleVersionEntity { + + // Database id for this specific version of an extension bundle + private String id; + + // Foreign key to the extension bundle this version goes with + private String bundleId; + + // The bucket id where the bundle is located + private String bucketId; + + // The group and artifact id for the bundle this version belongs to + private String groupId; + private String artifactId; + + // The version of this bundle + private String version; + + // General info about this version of the bundle + private Date created; + private String createdBy; + private String description; + + // The hex representation of the SHA-256 digest for the binary content of this version + private String sha256Hex; + + // Indicates whether the SHA-256 was supplied by the client, which means it matched the server's calculation, or was not supplied by the client + private boolean sha256Supplied; + + // The size of binary content in bytes + private long contentSize; + + // The version of the system API that the bundle was built against (i.e. nifi-api) + private String systemApiVersion; + + // Build information + private String buildTool; + private String buildFlags; + + private String buildBranch; + private String buildTag; + private String buildRevision; + + private Date built; + private String builtBy; + + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getBundleId() { + return bundleId; + } + + public void setBundleId(String bundleId) { + this.bundleId = bundleId; + } + + public String getBucketId() { + return bucketId; + } + + public void setBucketId(String bucketId) { + this.bucketId = bucketId; + } + + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + public String getArtifactId() { + return artifactId; + } + + public void setArtifactId(String artifactId) { + this.artifactId = artifactId; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getSha256Hex() { + return sha256Hex; + } + + public void setSha256Hex(String sha256Hex) { + this.sha256Hex = sha256Hex; + } + + public boolean getSha256Supplied() { + return sha256Supplied; + } + + public void setSha256Supplied(boolean sha256Supplied) { + this.sha256Supplied = sha256Supplied; + } + + public long getContentSize() { + return contentSize; + } + + public void setContentSize(long contentSize) { + this.contentSize = contentSize; + } + + public String getSystemApiVersion() { + return systemApiVersion; + } + + public void setSystemApiVersion(String systemApiVersion) { + this.systemApiVersion = systemApiVersion; + } + + public String getBuildTool() { + return buildTool; + } + + public void setBuildTool(String buildTool) { + this.buildTool = buildTool; + } + + public String getBuildFlags() { + return buildFlags; + } + + public void setBuildFlags(String buildFlags) { + this.buildFlags = buildFlags; + } + + public String getBuildBranch() { + return buildBranch; + } + + public void setBuildBranch(String buildBranch) { + this.buildBranch = buildBranch; + } + + public String getBuildTag() { + return buildTag; + } + + public void setBuildTag(String buildTag) { + this.buildTag = buildTag; + } + + public String getBuildRevision() { + return buildRevision; + } + + public void setBuildRevision(String buildRevision) { + this.buildRevision = buildRevision; + } + + public Date getBuilt() { + return built; + } + + public void setBuilt(Date built) { + this.built = built; + } + + public String getBuiltBy() { + return builtBy; + } + + public void setBuiltBy(String builtBy) { + this.builtBy = builtBy; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionAdditionalDetailsEntity.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionAdditionalDetailsEntity.java new file mode 100644 index 0000000000..ce1509b4b6 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionAdditionalDetailsEntity.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.entity; + +import java.util.Optional; + +public class ExtensionAdditionalDetailsEntity { + + private String extensionId; + + private Optional additionalDetails; + + public String getExtensionId() { + return extensionId; + } + + public void setExtensionId(String extensionId) { + this.extensionId = extensionId; + } + + public Optional getAdditionalDetails() { + return additionalDetails; + } + + public void setAdditionalDetails(Optional additionalDetails) { + this.additionalDetails = additionalDetails; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionEntity.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionEntity.java new file mode 100644 index 0000000000..4c465a9cd3 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionEntity.java @@ -0,0 +1,210 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.entity; + +import org.apache.nifi.registry.extension.bundle.BundleType; +import org.apache.nifi.registry.extension.component.manifest.ExtensionType; + +import java.util.Set; + +public class ExtensionEntity { + + private String id; + private String bundleVersionId; + private String name; + private String displayName; + private ExtensionType extensionType; + + // serialized content of Extension + private String content; + + // populated during creation if provided, but typically won't be populated on retrieval + private String additionalDetails; + + // read-only to let consumers know there are additional details that have not be returned, but can be retrieved later + private boolean hasAdditionalDetails; + + // populated during creation to insert into child tables, but won't be populated on retrieval b/c the + // content field contains all of this info and will be deserialized into the full extension + private Set tags; + private Set providedServiceApis; + private Set restrictions; + + // read-only - populated on retrieval only by joining with additional tables + private String bucketId; + private String bucketName; + private String bundleId; + private String groupId; + private String artifactId; + private String version; + private String systemApiVersion; + private BundleType bundleType; + + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getBundleVersionId() { + return bundleVersionId; + } + + public void setBundleVersionId(String bundleVersionId) { + this.bundleVersionId = bundleVersionId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public ExtensionType getExtensionType() { + return extensionType; + } + + public void setExtensionType(ExtensionType extensionType) { + this.extensionType = extensionType; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getAdditionalDetails() { + return additionalDetails; + } + + public void setAdditionalDetails(String additionalDetails) { + this.additionalDetails = additionalDetails; + } + + public boolean getHasAdditionalDetails() { + return hasAdditionalDetails; + } + + public void setHasAdditionalDetails(boolean hasAdditionalDetails) { + this.hasAdditionalDetails = hasAdditionalDetails; + } + + public Set getTags() { + return tags; + } + + public void setTags(Set tags) { + this.tags = tags; + } + + public Set getProvidedServiceApis() { + return providedServiceApis; + } + + public void setProvidedServiceApis(Set providedServiceApis) { + this.providedServiceApis = providedServiceApis; + } + + public Set getRestrictions() { + return restrictions; + } + + public void setRestrictions(Set restrictions) { + this.restrictions = restrictions; + } + + + public String getBucketId() { + return bucketId; + } + + public void setBucketId(String bucketId) { + this.bucketId = bucketId; + } + + public String getBucketName() { + return bucketName; + } + + public void setBucketName(String bucketName) { + this.bucketName = bucketName; + } + + public String getBundleId() { + return bundleId; + } + + public void setBundleId(String bundleId) { + this.bundleId = bundleId; + } + + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + public String getArtifactId() { + return artifactId; + } + + public void setArtifactId(String artifactId) { + this.artifactId = artifactId; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getSystemApiVersion() { + return systemApiVersion; + } + + public void setSystemApiVersion(String systemApiVersion) { + this.systemApiVersion = systemApiVersion; + } + + public BundleType getBundleType() { + return bundleType; + } + + public void setBundleType(BundleType bundleType) { + this.bundleType = bundleType; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionProvidedServiceApiEntity.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionProvidedServiceApiEntity.java new file mode 100644 index 0000000000..f08595057a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionProvidedServiceApiEntity.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.entity; + +import java.util.Objects; + +public class ExtensionProvidedServiceApiEntity { + + private String id; + + private String extensionId; + + private String className; + + private String groupId; + + private String artifactId; + + private String version; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getExtensionId() { + return extensionId; + } + + public void setExtensionId(String extensionId) { + this.extensionId = extensionId; + } + + public String getClassName() { + return className; + } + + public void setClassName(String className) { + this.className = className; + } + + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + public String getArtifactId() { + return artifactId; + } + + public void setArtifactId(String artifactId) { + this.artifactId = artifactId; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ExtensionProvidedServiceApiEntity entity = (ExtensionProvidedServiceApiEntity) o; + return Objects.equals(id, entity.id) + && Objects.equals(extensionId, entity.extensionId) + && Objects.equals(className, entity.className) + && Objects.equals(groupId, entity.groupId) + && Objects.equals(artifactId, entity.artifactId) + && Objects.equals(version, entity.version); + } + + @Override + public int hashCode() { + return Objects.hash(id, extensionId, className, groupId, artifactId, version); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionRestrictionEntity.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionRestrictionEntity.java new file mode 100644 index 0000000000..26a30398d5 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionRestrictionEntity.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.entity; + +import java.util.Objects; + +public class ExtensionRestrictionEntity { + + private String id; + + private String extensionId; + + private String requiredPermission; + + private String explanation; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getExtensionId() { + return extensionId; + } + + public void setExtensionId(String extensionId) { + this.extensionId = extensionId; + } + + public String getRequiredPermission() { + return requiredPermission; + } + + public void setRequiredPermission(String requiredPermission) { + this.requiredPermission = requiredPermission; + } + + public String getExplanation() { + return explanation; + } + + public void setExplanation(String explanation) { + this.explanation = explanation; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ExtensionRestrictionEntity that = (ExtensionRestrictionEntity) o; + return Objects.equals(id, that.id) + && Objects.equals(extensionId, that.extensionId) + && Objects.equals(requiredPermission, that.requiredPermission) + && Objects.equals(explanation, that.explanation); + } + + @Override + public int hashCode() { + return Objects.hash(id, extensionId, requiredPermission, explanation); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionTagEntity.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionTagEntity.java new file mode 100644 index 0000000000..16124423ad --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionTagEntity.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.entity; + +public class ExtensionTagEntity { + + private String extensionId; + + private String tag; + + public String getExtensionId() { + return extensionId; + } + + public void setExtensionId(String extensionId) { + this.extensionId = extensionId; + } + + public String getTag() { + return tag; + } + + public void setTag(String tag) { + this.tag = tag; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/FlowEntity.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/FlowEntity.java new file mode 100644 index 0000000000..b978168fbb --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/FlowEntity.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.entity; + +public class FlowEntity extends BucketItemEntity { + + private long snapshotCount; + + public FlowEntity() { + setType(BucketItemEntityType.FLOW); + } + + public long getSnapshotCount() { + return snapshotCount; + } + + public void setSnapshotCount(long snapshotCount) { + this.snapshotCount = snapshotCount; + } + + @Override + public void setType(BucketItemEntityType type) { + if (BucketItemEntityType.FLOW != type) { + throw new IllegalStateException("Must set type to FLOW"); + } + super.setType(type); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/FlowSnapshotEntity.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/FlowSnapshotEntity.java new file mode 100644 index 0000000000..3143a6e3b6 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/FlowSnapshotEntity.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.entity; + +import java.util.Date; +import java.util.Objects; + +public class FlowSnapshotEntity { + + private String flowId; + + private Integer version; + + private Date created; + + private String createdBy; + + private String comments; + + public String getFlowId() { + return flowId; + } + + public void setFlowId(String flowId) { + this.flowId = flowId; + } + + public Integer getVersion() { + return version; + } + + public void setVersion(Integer version) { + this.version = version; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public String getComments() { + return comments; + } + + public void setComments(String comments) { + this.comments = comments; + } + + @Override + public int hashCode() { + return Objects.hash(this.flowId, this.version); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + + if (!(obj instanceof FlowSnapshotEntity)) { + return false; + } + + final FlowSnapshotEntity other = (FlowSnapshotEntity) obj; + return Objects.equals(this.flowId, other.flowId) && Objects.equals(this.version, other.version); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/KeyEntity.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/KeyEntity.java new file mode 100644 index 0000000000..494867fd7f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/KeyEntity.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.entity; + +public class KeyEntity { + + private String id; + + private String tenantIdentity; + + private String keyValue; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getTenantIdentity() { + return tenantIdentity; + } + + public void setTenantIdentity(String tenantIdentity) { + this.tenantIdentity = tenantIdentity; + } + + public String getKeyValue() { + return keyValue; + } + + public void setKeyValue(String keyValue) { + this.keyValue = keyValue; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/TagCountEntity.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/TagCountEntity.java new file mode 100644 index 0000000000..3de37f94ca --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/TagCountEntity.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.entity; + +public class TagCountEntity { + + private String tag; + + private int count; + + public String getTag() { + return tag; + } + + public void setTag(String tag) { + this.tag = tag; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BucketEntityRowMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BucketEntityRowMapper.java new file mode 100644 index 0000000000..5bda0a8252 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BucketEntityRowMapper.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.mapper; + +import org.apache.nifi.registry.db.entity.BucketEntity; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.lang.Nullable; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class BucketEntityRowMapper implements RowMapper { + + @Nullable + @Override + public BucketEntity mapRow(ResultSet rs, int rowNum) throws SQLException { + final BucketEntity b = new BucketEntity(); + b.setId(rs.getString("ID")); + b.setName(rs.getString("NAME")); + b.setDescription(rs.getString("DESCRIPTION")); + b.setCreated(rs.getTimestamp("CREATED")); + b.setAllowExtensionBundleRedeploy(rs.getInt("ALLOW_EXTENSION_BUNDLE_REDEPLOY") == 0 ? false : true); + b.setAllowPublicRead(rs.getInt("ALLOW_PUBLIC_READ") == 0 ? false : true); + return b; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BucketItemEntityRowMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BucketItemEntityRowMapper.java new file mode 100644 index 0000000000..6db8e4146f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BucketItemEntityRowMapper.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.mapper; + +import org.apache.nifi.registry.db.entity.BucketItemEntity; +import org.apache.nifi.registry.db.entity.BucketItemEntityType; +import org.apache.nifi.registry.db.entity.BundleEntity; +import org.apache.nifi.registry.db.entity.FlowEntity; +import org.apache.nifi.registry.extension.bundle.BundleType; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.lang.Nullable; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class BucketItemEntityRowMapper implements RowMapper { + + @Nullable + @Override + public BucketItemEntity mapRow(ResultSet rs, int rowNum) throws SQLException { + final BucketItemEntityType type = BucketItemEntityType.valueOf(rs.getString("ITEM_TYPE")); + + // Create the appropriate type of sub-class, eventually populate specific data for each type + final BucketItemEntity item; + switch (type) { + case FLOW: + item = new FlowEntity(); + break; + case BUNDLE: + final BundleEntity bundleEntity = new BundleEntity(); + bundleEntity.setBundleType(BundleType.valueOf(rs.getString("BUNDLE_TYPE"))); + bundleEntity.setGroupId(rs.getString("BUNDLE_GROUP_ID")); + bundleEntity.setArtifactId(rs.getString("BUNDLE_ARTIFACT_ID")); + item = bundleEntity; + break; + default: + // should never happen + item = new BucketItemEntity(); + break; + } + + // populate fields common to all bucket items + item.setId(rs.getString("ID")); + item.setName(rs.getString("NAME")); + item.setDescription(rs.getString("DESCRIPTION")); + item.setCreated(rs.getTimestamp("CREATED")); + item.setModified(rs.getTimestamp("MODIFIED")); + item.setBucketId(rs.getString("BUCKET_ID")); + item.setBucketName(rs.getString("BUCKET_NAME")); + item.setType(type); + return item; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BundleEntityRowMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BundleEntityRowMapper.java new file mode 100644 index 0000000000..48de7a8074 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BundleEntityRowMapper.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.mapper; + +import org.apache.nifi.registry.db.entity.BucketItemEntityType; +import org.apache.nifi.registry.db.entity.BundleEntity; +import org.apache.nifi.registry.extension.bundle.BundleType; +import org.springframework.jdbc.core.RowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class BundleEntityRowMapper implements RowMapper { + + @Override + public BundleEntity mapRow(final ResultSet rs, final int i) throws SQLException { + final BundleEntity entity = new BundleEntity(); + + // BucketItemEntity fields + entity.setId(rs.getString("ID")); + entity.setName(rs.getString("NAME")); + entity.setDescription(rs.getString("DESCRIPTION")); + entity.setCreated(rs.getTimestamp("CREATED")); + entity.setModified(rs.getTimestamp("MODIFIED")); + entity.setBucketId(rs.getString("BUCKET_ID")); + entity.setBucketName(rs.getString("BUCKET_NAME")); + entity.setType(BucketItemEntityType.BUNDLE); + + // BundleEntity fields + entity.setBundleType(BundleType.valueOf(rs.getString("BUNDLE_TYPE"))); + entity.setGroupId(rs.getString("GROUP_ID")); + entity.setArtifactId(rs.getString("ARTIFACT_ID")); + + return entity; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BundleVersionDependencyEntityRowMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BundleVersionDependencyEntityRowMapper.java new file mode 100644 index 0000000000..21b6d3f73a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BundleVersionDependencyEntityRowMapper.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.mapper; + +import org.apache.nifi.registry.db.entity.BundleVersionDependencyEntity; +import org.springframework.jdbc.core.RowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class BundleVersionDependencyEntityRowMapper implements RowMapper { + + @Override + public BundleVersionDependencyEntity mapRow(final ResultSet rs, final int i) throws SQLException { + final BundleVersionDependencyEntity entity = new BundleVersionDependencyEntity(); + entity.setId(rs.getString("ID")); + entity.setExtensionBundleVersionId(rs.getString("BUNDLE_VERSION_ID")); + entity.setGroupId(rs.getString("GROUP_ID")); + entity.setArtifactId(rs.getString("ARTIFACT_ID")); + entity.setVersion(rs.getString("VERSION")); + return entity; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BundleVersionEntityRowMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BundleVersionEntityRowMapper.java new file mode 100644 index 0000000000..ed57cb814f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BundleVersionEntityRowMapper.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.mapper; + +import org.apache.nifi.registry.db.entity.BundleVersionEntity; +import org.springframework.jdbc.core.RowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class BundleVersionEntityRowMapper implements RowMapper { + + @Override + public BundleVersionEntity mapRow(final ResultSet rs, final int i) throws SQLException { + final BundleVersionEntity entity = new BundleVersionEntity(); + entity.setId(rs.getString("ID")); + entity.setBundleId(rs.getString("BUNDLE_ID")); + entity.setBucketId(rs.getString("BUCKET_ID")); + entity.setGroupId(rs.getString("GROUP_ID")); + entity.setArtifactId(rs.getString("ARTIFACT_ID")); + entity.setVersion(rs.getString("VERSION")); + entity.setSha256Hex(rs.getString("SHA_256_HEX")); + entity.setSha256Supplied(rs.getInt("SHA_256_SUPPLIED") == 1); + entity.setContentSize(rs.getLong("CONTENT_SIZE")); + entity.setSystemApiVersion(rs.getString("SYSTEM_API_VERSION")); + + entity.setBuildTool(rs.getString("BUILD_TOOL")); + entity.setBuildFlags(rs.getString("BUILD_FLAGS")); + entity.setBuildBranch(rs.getString("BUILD_BRANCH")); + entity.setBuildTag(rs.getString("BUILD_TAG")); + entity.setBuildRevision(rs.getString("BUILD_REVISION")); + entity.setBuilt(rs.getTimestamp("BUILT")); + entity.setBuiltBy(rs.getString("BUILT_BY")); + + entity.setCreated(rs.getTimestamp("CREATED")); + entity.setCreatedBy(rs.getString("CREATED_BY")); + entity.setDescription(rs.getString("DESCRIPTION")); + + return entity; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/ExtensionEntityRowMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/ExtensionEntityRowMapper.java new file mode 100644 index 0000000000..473445ab11 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/ExtensionEntityRowMapper.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.mapper; + +import org.apache.nifi.registry.db.entity.ExtensionEntity; +import org.apache.nifi.registry.extension.bundle.BundleType; +import org.apache.nifi.registry.extension.component.manifest.ExtensionType; +import org.springframework.jdbc.core.RowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class ExtensionEntityRowMapper implements RowMapper { + + @Override + public ExtensionEntity mapRow(ResultSet rs, int i) throws SQLException { + final ExtensionEntity entity = new ExtensionEntity(); + + // fields from extension table... + entity.setId(rs.getString("ID")); + entity.setBundleVersionId(rs.getString("BUNDLE_VERSION_ID")); + entity.setName(rs.getString("NAME")); + entity.setDisplayName(rs.getString("DISPLAY_NAME")); + entity.setExtensionType(ExtensionType.valueOf(rs.getString("TYPE"))); + entity.setContent(rs.getString("CONTENT")); + entity.setHasAdditionalDetails(rs.getInt("HAS_ADDITIONAL_DETAILS") == 1 ? true : false); + + // fields from joined tables that we know will be there... + entity.setBucketId(rs.getString("BUCKET_ID")); + entity.setBucketName(rs.getString("BUCKET_NAME")); + entity.setBundleId(rs.getString("BUNDLE_ID")); + entity.setGroupId(rs.getString("GROUP_ID")); + entity.setArtifactId(rs.getString("ARTIFACT_ID")); + entity.setVersion(rs.getString("VERSION")); + entity.setSystemApiVersion(rs.getString("SYSTEM_API_VERSION")); + entity.setBundleType(BundleType.valueOf(rs.getString("BUNDLE_TYPE"))); + + return entity; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/FlowEntityRowMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/FlowEntityRowMapper.java new file mode 100644 index 0000000000..acaf343175 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/FlowEntityRowMapper.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.mapper; + +import org.apache.nifi.registry.db.entity.BucketItemEntityType; +import org.apache.nifi.registry.db.entity.FlowEntity; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.lang.Nullable; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class FlowEntityRowMapper implements RowMapper { + + @Nullable + @Override + public FlowEntity mapRow(ResultSet rs, int rowNum) throws SQLException { + final FlowEntity flowEntity = new FlowEntity(); + flowEntity.setId(rs.getString("ID")); + flowEntity.setName(rs.getString("NAME")); + flowEntity.setDescription(rs.getString("DESCRIPTION")); + flowEntity.setCreated(rs.getTimestamp("CREATED")); + flowEntity.setModified(rs.getTimestamp("MODIFIED")); + flowEntity.setBucketId(rs.getString("BUCKET_ID")); + flowEntity.setType(BucketItemEntityType.FLOW); + return flowEntity; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/FlowSnapshotEntityRowMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/FlowSnapshotEntityRowMapper.java new file mode 100644 index 0000000000..07a59b347a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/FlowSnapshotEntityRowMapper.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.mapper; + +import org.apache.nifi.registry.db.entity.FlowSnapshotEntity; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.lang.Nullable; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class FlowSnapshotEntityRowMapper implements RowMapper { + + @Nullable + @Override + public FlowSnapshotEntity mapRow(ResultSet rs, int rowNum) throws SQLException { + final FlowSnapshotEntity entity = new FlowSnapshotEntity(); + entity.setFlowId(rs.getString("FLOW_ID")); + entity.setVersion(rs.getInt("VERSION")); + entity.setCreated(rs.getTimestamp("CREATED")); + entity.setCreatedBy(rs.getString("CREATED_BY")); + entity.setComments(rs.getString("COMMENTS")); + return entity; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/KeyEntityRowMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/KeyEntityRowMapper.java new file mode 100644 index 0000000000..6e190a5744 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/KeyEntityRowMapper.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.mapper; + +import org.apache.nifi.registry.db.entity.KeyEntity; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.lang.Nullable; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class KeyEntityRowMapper implements RowMapper { + + @Nullable + @Override + public KeyEntity mapRow(ResultSet rs, int rowNum) throws SQLException { + final KeyEntity keyEntity = new KeyEntity(); + keyEntity.setId(rs.getString("ID")); + keyEntity.setTenantIdentity(rs.getString("TENANT_IDENTITY")); + keyEntity.setKeyValue(rs.getString("KEY_VALUE")); + return keyEntity; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/TagCountEntityMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/TagCountEntityMapper.java new file mode 100644 index 0000000000..8b63e6c919 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/TagCountEntityMapper.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.mapper; + +import org.apache.nifi.registry.db.entity.TagCountEntity; +import org.springframework.jdbc.core.RowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class TagCountEntityMapper implements RowMapper { + + @Override + public TagCountEntity mapRow(ResultSet rs, int rowNum) throws SQLException { + final TagCountEntity entity = new TagCountEntity(); + entity.setTag(rs.getString("TAG")); + entity.setCount(rs.getInt("COUNT")); + return entity; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/BucketEntityV1.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/BucketEntityV1.java new file mode 100644 index 0000000000..94000a5e9c --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/BucketEntityV1.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.migration; + +import java.util.Date; +import java.util.Objects; + +/** + * Bucket DB entity from the original database schema in 0.1.0, used for migration purposes. + */ +public class BucketEntityV1 { + + private String id; + + private String name; + + private String description; + + private Date created; + + + 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; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @Override + public int hashCode() { + return Objects.hashCode(this.id); + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + + final BucketEntityV1 other = (BucketEntityV1) obj; + return Objects.equals(this.id, other.id); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/FlowEntityV1.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/FlowEntityV1.java new file mode 100644 index 0000000000..961c3bd9b6 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/FlowEntityV1.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.migration; + +import java.util.Date; +import java.util.Objects; + +/** + * Flow DB entity from the original database schema in 0.1.0, used for migration purposes. + */ +public class FlowEntityV1 { + + private String id; + + private String name; + + private String description; + + private Date created; + + private Date modified; + + private String bucketId; + + + 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; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Date getModified() { + return modified; + } + + public void setModified(Date modified) { + this.modified = modified; + } + + public String getBucketId() { + return bucketId; + } + + public void setBucketId(String bucketId) { + this.bucketId = bucketId; + } + + @Override + public int hashCode() { + return Objects.hashCode(this.id); + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + + final FlowEntityV1 other = (FlowEntityV1) obj; + return Objects.equals(this.id, other.id); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/FlowSnapshotEntityV1.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/FlowSnapshotEntityV1.java new file mode 100644 index 0000000000..ec6b9a556d --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/FlowSnapshotEntityV1.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.migration; + +import java.util.Date; +import java.util.Objects; + +/** + * FlowSnapshot DB entity from the original database schema in 0.1.0, used for migration purposes. + */ +public class FlowSnapshotEntityV1 { + + private String flowId; + + private Integer version; + + private Date created; + + private String createdBy; + + private String comments; + + public String getFlowId() { + return flowId; + } + + public void setFlowId(String flowId) { + this.flowId = flowId; + } + + public Integer getVersion() { + return version; + } + + public void setVersion(Integer version) { + this.version = version; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public String getComments() { + return comments; + } + + public void setComments(String comments) { + this.comments = comments; + } + + @Override + public int hashCode() { + return Objects.hash(this.flowId, this.version); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + + if (!(obj instanceof FlowSnapshotEntityV1)) { + return false; + } + + final FlowSnapshotEntityV1 other = (FlowSnapshotEntityV1) obj; + return Objects.equals(this.flowId, other.flowId) && Objects.equals(this.version, other.version); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/LegacyDataSourceFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/LegacyDataSourceFactory.java new file mode 100644 index 0000000000..72d3acfccd --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/LegacyDataSourceFactory.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.migration; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.h2.jdbcx.JdbcConnectionPool; + +import javax.sql.DataSource; +import java.io.File; + +/** + * NOTE: This DataSource factory was used in the original 0.1.0 release and remains to migrate data from the old database. + * This class is intentionally not a Spring bean, and will be used manually in the custom Flyway migration. + */ +public class LegacyDataSourceFactory { + + private static final String DB_USERNAME_PASSWORD = "nifireg"; + private static final int MAX_CONNECTIONS = 5; + + // database file name + private static final String DATABASE_FILE_NAME = "nifi-registry"; + + private final NiFiRegistryProperties properties; + + private JdbcConnectionPool connectionPool; + + public LegacyDataSourceFactory(final NiFiRegistryProperties properties) { + this.properties = properties; + } + + public DataSource getDataSource() { + if (connectionPool == null) { + final String databaseUrl = getDatabaseUrl(properties); + connectionPool = JdbcConnectionPool.create(databaseUrl, DB_USERNAME_PASSWORD, DB_USERNAME_PASSWORD); + connectionPool.setMaxConnections(MAX_CONNECTIONS); + } + + return connectionPool; + } + + public static String getDatabaseUrl(final NiFiRegistryProperties properties) { + // locate the repository directory + final String repositoryDirectoryPath = properties.getLegacyDatabaseDirectory(); + + // ensure the repository directory is specified + if (repositoryDirectoryPath == null) { + throw new NullPointerException("Database directory must be specified."); + } + + // create a handle to the repository directory + final File repositoryDirectory = new File(repositoryDirectoryPath); + + // get a handle to the database file + final File databaseFile = new File(repositoryDirectory, DATABASE_FILE_NAME); + + // format the database url + String databaseUrl = "jdbc:h2:" + databaseFile + ";AUTOCOMMIT=OFF;DB_CLOSE_ON_EXIT=FALSE;LOCK_MODE=3"; + String databaseUrlAppend = properties.getLegacyDatabaseUrlAppend(); + if (StringUtils.isNotBlank(databaseUrlAppend)) { + databaseUrl += databaseUrlAppend; + } + + return databaseUrl; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/LegacyDatabaseService.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/LegacyDatabaseService.java new file mode 100644 index 0000000000..533fadd010 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/LegacyDatabaseService.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.migration; + +import org.springframework.jdbc.core.JdbcTemplate; + +import javax.sql.DataSource; +import java.util.List; + +/** + * Service used to load data from original database used in the 0.1.0 release. + */ +public class LegacyDatabaseService { + + private final JdbcTemplate jdbcTemplate; + + public LegacyDatabaseService(final DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + public List getAllBuckets() { + final String sql = "SELECT * FROM bucket ORDER BY name ASC"; + + return jdbcTemplate.query(sql, (rs, i) -> { + final BucketEntityV1 b = new BucketEntityV1(); + b.setId(rs.getString("ID")); + b.setName(rs.getString("NAME")); + b.setDescription(rs.getString("DESCRIPTION")); + b.setCreated(rs.getTimestamp("CREATED")); + return b; + }); + } + + public List getAllFlows() { + final String sql = "SELECT * FROM flow f, bucket_item item WHERE item.id = f.id"; + + return jdbcTemplate.query(sql, (rs, i) -> { + final FlowEntityV1 flowEntity = new FlowEntityV1(); + flowEntity.setId(rs.getString("ID")); + flowEntity.setName(rs.getString("NAME")); + flowEntity.setDescription(rs.getString("DESCRIPTION")); + flowEntity.setCreated(rs.getTimestamp("CREATED")); + flowEntity.setModified(rs.getTimestamp("MODIFIED")); + flowEntity.setBucketId(rs.getString("BUCKET_ID")); + return flowEntity; + }); + } + + public List getAllFlowSnapshots() { + final String sql = "SELECT * FROM flow_snapshot fs"; + + return jdbcTemplate.query(sql, (rs, i) -> { + final FlowSnapshotEntityV1 fs = new FlowSnapshotEntityV1(); + fs.setFlowId(rs.getString("FLOW_ID")); + fs.setVersion(rs.getInt("VERSION")); + fs.setCreated(rs.getTimestamp("CREATED")); + fs.setCreatedBy(rs.getString("CREATED_BY")); + fs.setComments(rs.getString("COMMENTS")); + return fs; + }); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/LegacyEntityMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/LegacyEntityMapper.java new file mode 100644 index 0000000000..bf82aaefe9 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/LegacyEntityMapper.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.migration; + +import org.apache.nifi.registry.db.entity.BucketEntity; +import org.apache.nifi.registry.db.entity.BucketItemEntityType; +import org.apache.nifi.registry.db.entity.FlowEntity; +import org.apache.nifi.registry.db.entity.FlowSnapshotEntity; + +/** + * Utility methods to map legacy DB entities to current DB entities. + * + * The initial implementations of these mappings will be almost a direct translation, but if future changes are made + * to the original tables these methods will handle the translation from old entity to new entity. + */ +public class LegacyEntityMapper { + + public static BucketEntity createBucketEntity(final BucketEntityV1 bucketEntityV1) { + final BucketEntity bucketEntity = new BucketEntity(); + bucketEntity.setId(bucketEntityV1.getId()); + bucketEntity.setName(bucketEntityV1.getName()); + bucketEntity.setDescription(bucketEntityV1.getDescription()); + bucketEntity.setCreated(bucketEntityV1.getCreated()); + return bucketEntity; + } + + public static FlowEntity createFlowEntity(final FlowEntityV1 flowEntityV1) { + final FlowEntity flowEntity = new FlowEntity(); + flowEntity.setId(flowEntityV1.getId()); + flowEntity.setName(flowEntityV1.getName()); + flowEntity.setDescription(flowEntityV1.getDescription()); + flowEntity.setCreated(flowEntityV1.getCreated()); + flowEntity.setModified(flowEntityV1.getModified()); + flowEntity.setBucketId(flowEntityV1.getBucketId()); + flowEntity.setType(BucketItemEntityType.FLOW); + return flowEntity; + } + + public static FlowSnapshotEntity createFlowSnapshotEntity(final FlowSnapshotEntityV1 flowSnapshotEntityV1) { + final FlowSnapshotEntity flowSnapshotEntity = new FlowSnapshotEntity(); + flowSnapshotEntity.setFlowId(flowSnapshotEntityV1.getFlowId()); + flowSnapshotEntity.setVersion(flowSnapshotEntityV1.getVersion()); + flowSnapshotEntity.setComments(flowSnapshotEntityV1.getComments()); + flowSnapshotEntity.setCreated(flowSnapshotEntityV1.getCreated()); + flowSnapshotEntity.setCreatedBy(flowSnapshotEntityV1.getCreatedBy()); + return flowSnapshotEntity; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/EventFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/EventFactory.java new file mode 100644 index 0000000000..42912ab405 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/EventFactory.java @@ -0,0 +1,186 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.event; + +import org.apache.nifi.registry.authorization.User; +import org.apache.nifi.registry.authorization.UserGroup; +import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.extension.bundle.Bundle; +import org.apache.nifi.registry.extension.bundle.BundleVersion; +import org.apache.nifi.registry.flow.VersionedFlow; +import org.apache.nifi.registry.flow.VersionedFlowSnapshot; +import org.apache.nifi.registry.hook.Event; +import org.apache.nifi.registry.hook.EventFieldName; +import org.apache.nifi.registry.hook.EventType; +import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils; + +/** + * Factory to create Events from domain objects. + */ +public class EventFactory { + + public static Event bucketCreated(final Bucket bucket) { + return new StandardEvent.Builder() + .eventType(EventType.CREATE_BUCKET) + .addField(EventFieldName.BUCKET_ID, bucket.getIdentifier()) + .addField(EventFieldName.USER, NiFiUserUtils.getNiFiUserIdentity()) + .build(); + } + + public static Event bucketUpdated(final Bucket bucket) { + return new StandardEvent.Builder() + .eventType(EventType.UPDATE_BUCKET) + .addField(EventFieldName.BUCKET_ID, bucket.getIdentifier()) + .addField(EventFieldName.USER, NiFiUserUtils.getNiFiUserIdentity()) + .build(); + } + + public static Event bucketDeleted(final Bucket bucket) { + return new StandardEvent.Builder() + .eventType(EventType.DELETE_BUCKET) + .addField(EventFieldName.BUCKET_ID, bucket.getIdentifier()) + .addField(EventFieldName.USER, NiFiUserUtils.getNiFiUserIdentity()) + .build(); + } + + public static Event flowCreated(final VersionedFlow versionedFlow) { + return new StandardEvent.Builder() + .eventType(EventType.CREATE_FLOW) + .addField(EventFieldName.BUCKET_ID, versionedFlow.getBucketIdentifier()) + .addField(EventFieldName.FLOW_ID, versionedFlow.getIdentifier()) + .addField(EventFieldName.USER, NiFiUserUtils.getNiFiUserIdentity()) + .build(); + } + + public static Event flowUpdated(final VersionedFlow versionedFlow) { + return new StandardEvent.Builder() + .eventType(EventType.UPDATE_FLOW) + .addField(EventFieldName.BUCKET_ID, versionedFlow.getBucketIdentifier()) + .addField(EventFieldName.FLOW_ID, versionedFlow.getIdentifier()) + .addField(EventFieldName.USER, NiFiUserUtils.getNiFiUserIdentity()) + .build(); + } + + public static Event flowDeleted(final VersionedFlow versionedFlow) { + return new StandardEvent.Builder() + .eventType(EventType.DELETE_FLOW) + .addField(EventFieldName.BUCKET_ID, versionedFlow.getBucketIdentifier()) + .addField(EventFieldName.FLOW_ID, versionedFlow.getIdentifier()) + .addField(EventFieldName.USER, NiFiUserUtils.getNiFiUserIdentity()) + .build(); + } + + public static Event flowVersionCreated(final VersionedFlowSnapshot versionedFlowSnapshot) { + final String versionComments = versionedFlowSnapshot.getSnapshotMetadata().getComments() == null + ? "" : versionedFlowSnapshot.getSnapshotMetadata().getComments(); + + return new StandardEvent.Builder() + .eventType(EventType.CREATE_FLOW_VERSION) + .addField(EventFieldName.BUCKET_ID, versionedFlowSnapshot.getSnapshotMetadata().getBucketIdentifier()) + .addField(EventFieldName.FLOW_ID, versionedFlowSnapshot.getSnapshotMetadata().getFlowIdentifier()) + .addField(EventFieldName.VERSION, String.valueOf(versionedFlowSnapshot.getSnapshotMetadata().getVersion())) + .addField(EventFieldName.USER, versionedFlowSnapshot.getSnapshotMetadata().getAuthor()) + .addField(EventFieldName.COMMENT, versionComments) + .build(); + } + + public static Event extensionBundleCreated(final Bundle bundle) { + return new StandardEvent.Builder() + .eventType(EventType.CREATE_EXTENSION_BUNDLE) + .addField(EventFieldName.BUCKET_ID, bundle.getBucketIdentifier()) + .addField(EventFieldName.EXTENSION_BUNDLE_ID, bundle.getIdentifier()) + .addField(EventFieldName.USER, NiFiUserUtils.getNiFiUserIdentity()) + .build(); + } + + public static Event extensionBundleDeleted(final Bundle bundle) { + return new StandardEvent.Builder() + .eventType(EventType.DELETE_EXTENSION_BUNDLE) + .addField(EventFieldName.BUCKET_ID, bundle.getBucketIdentifier()) + .addField(EventFieldName.EXTENSION_BUNDLE_ID, bundle.getIdentifier()) + .addField(EventFieldName.USER, NiFiUserUtils.getNiFiUserIdentity()) + .build(); + } + + public static Event extensionBundleVersionCreated(final BundleVersion bundleVersion) { + return new StandardEvent.Builder() + .eventType(EventType.CREATE_EXTENSION_BUNDLE_VERSION) + .addField(EventFieldName.BUCKET_ID, bundleVersion.getVersionMetadata().getBucketId()) + .addField(EventFieldName.EXTENSION_BUNDLE_ID, bundleVersion.getVersionMetadata().getBundleId()) + .addField(EventFieldName.VERSION, String.valueOf(bundleVersion.getVersionMetadata().getVersion())) + .addField(EventFieldName.USER, NiFiUserUtils.getNiFiUserIdentity()) + .build(); + } + + public static Event extensionBundleVersionDeleted(final BundleVersion bundleVersion) { + return new StandardEvent.Builder() + .eventType(EventType.DELETE_EXTENSION_BUNDLE_VERSION) + .addField(EventFieldName.BUCKET_ID, bundleVersion.getVersionMetadata().getBucketId()) + .addField(EventFieldName.EXTENSION_BUNDLE_ID, bundleVersion.getVersionMetadata().getBundleId()) + .addField(EventFieldName.VERSION, String.valueOf(bundleVersion.getVersionMetadata().getVersion())) + .addField(EventFieldName.USER, NiFiUserUtils.getNiFiUserIdentity()) + .build(); + } + + public static Event userCreated(final User user) { + return new StandardEvent.Builder() + .eventType(EventType.CREATE_USER) + .addField(EventFieldName.USER_ID, user.getIdentifier()) + .addField(EventFieldName.USER_IDENTITY, user.getIdentity()) + .build(); + } + + public static Event userUpdated(final User user) { + return new StandardEvent.Builder() + .eventType(EventType.UPDATE_USER) + .addField(EventFieldName.USER_ID, user.getIdentifier()) + .addField(EventFieldName.USER_IDENTITY, user.getIdentity()) + .build(); + } + + public static Event userDeleted(final User user) { + return new StandardEvent.Builder() + .eventType(EventType.DELETE_USER) + .addField(EventFieldName.USER_ID, user.getIdentifier()) + .addField(EventFieldName.USER_IDENTITY, user.getIdentity()) + .build(); + } + + public static Event userGroupCreated(final UserGroup userGroup) { + return new StandardEvent.Builder() + .eventType(EventType.CREATE_USER_GROUP) + .addField(EventFieldName.USER_GROUP_ID, userGroup.getIdentifier()) + .addField(EventFieldName.USER_GROUP_IDENTITY, userGroup.getIdentity()) + .build(); + } + + public static Event userGroupUpdated(final UserGroup userGroup) { + return new StandardEvent.Builder() + .eventType(EventType.UPDATE_USER_GROUP) + .addField(EventFieldName.USER_GROUP_ID, userGroup.getIdentifier()) + .addField(EventFieldName.USER_GROUP_IDENTITY, userGroup.getIdentity()) + .build(); + } + + public static Event userGroupDeleted(final UserGroup userGroup) { + return new StandardEvent.Builder() + .eventType(EventType.DELETE_USER_GROUP) + .addField(EventFieldName.USER_GROUP_ID, userGroup.getIdentifier()) + .addField(EventFieldName.USER_GROUP_IDENTITY, userGroup.getIdentity()) + .build(); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/EventService.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/EventService.java new file mode 100644 index 0000000000..8a114933d1 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/EventService.java @@ -0,0 +1,115 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.event; + +import org.apache.nifi.registry.hook.Event; +import org.apache.nifi.registry.hook.EventHookProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * Service used for publishing events and passing events to the hook providers. + */ +@Service +public class EventService implements DisposableBean { + + private static final Logger LOGGER = LoggerFactory.getLogger(EventService.class); + + // Should only be a few events in the queue at a time, but setting a capacity just so it isn't unbounded + static final int EVENT_QUEUE_SIZE = 10_000; + + private final BlockingQueue eventQueue; + private final ExecutorService scheduledExecutorService; + private final List eventHookProviders; + + @Autowired + public EventService(final List eventHookProviders) { + this.eventQueue = new LinkedBlockingQueue<>(EVENT_QUEUE_SIZE); + this.scheduledExecutorService = Executors.newSingleThreadExecutor(); + this.eventHookProviders = new ArrayList<>(eventHookProviders); + } + + @PostConstruct + public void postConstruct() { + LOGGER.info("Starting event consumer..."); + + this.scheduledExecutorService.execute(() -> { + while (!Thread.interrupted()) { + try { + final Event event = eventQueue.poll(1000, TimeUnit.MILLISECONDS); + if (event == null) { + continue; + } + + // event was available so notify each provider, contain errors per-provider + for(final EventHookProvider provider : eventHookProviders) { + try { + if (event.getEventType() == null + || (event.getEventType() != null && provider.shouldHandle(event.getEventType()))) { + provider.handle(event); + } + } catch (Exception e) { + LOGGER.error("Error handling event hook", e); + } + } + } catch (InterruptedException e) { + LOGGER.warn("Interrupted while polling event queue"); + return; + } + } + }); + + LOGGER.info("Event consumer started!"); + } + + @Override + public void destroy() throws Exception { + LOGGER.info("Shutting down event consumer..."); + this.scheduledExecutorService.shutdownNow(); + LOGGER.info("Event consumer shutdown!"); + } + + public void publish(final Event event) { + if (event == null) { + return; + } + + try { + event.validate(); + + final boolean queued = eventQueue.offer(event); + if (!queued) { + LOGGER.error("Unable to queue event because queue is full"); + } + } catch (IllegalStateException e) { + LOGGER.error("Invalid event due to: " + e.getMessage(), e); + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/StandardEvent.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/StandardEvent.java new file mode 100644 index 0000000000..26ca93c606 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/StandardEvent.java @@ -0,0 +1,147 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.event; + +import org.apache.commons.lang3.Validate; +import org.apache.nifi.registry.hook.Event; +import org.apache.nifi.registry.hook.EventField; +import org.apache.nifi.registry.hook.EventFieldName; +import org.apache.nifi.registry.hook.EventType; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Standard implementation of Event. + */ +public class StandardEvent implements Event { + + private final EventType eventType; + + private final List eventFields; + + private StandardEvent(final Builder builder) { + this.eventType = builder.eventType; + this.eventFields = Collections.unmodifiableList(builder.eventFields == null + ? Collections.emptyList() : new ArrayList<>(builder.eventFields)); + Validate.notNull(this.eventType); + } + + @Override + public EventType getEventType() { + return eventType; + } + + @Override + public List getFields() { + return eventFields; + } + + @Override + public EventField getField(final EventFieldName fieldName) { + if (fieldName == null) { + return null; + } + + return eventFields.stream().filter(e -> fieldName.equals(e.getName())).findFirst().orElse(null); + } + + @Override + public void validate() throws IllegalStateException { + final int numProvidedFields = eventFields.size(); + final int numRequiredFields = eventType.getFieldNames().size(); + + if (numProvidedFields != numRequiredFields) { + throw new IllegalStateException(numRequiredFields + " fields were required, but only " + numProvidedFields + " were provided"); + } + + for (int i=0; i < numRequiredFields; i++) { + final EventFieldName required = eventType.getFieldNames().get(i); + final EventFieldName provided = eventFields.get(i).getName(); + if (!required.equals(provided)) { + throw new IllegalStateException("Expected " + required.name() + ", but found " + provided.name()); + } + } + } + + /** + * Builder for Events. + */ + public static class Builder { + + private EventType eventType; + private List eventFields = new ArrayList<>(); + + public Builder eventType(final EventType eventType) { + this.eventType = eventType; + return this; + } + + public Builder addField(final EventFieldName name, final String value) { + this.eventFields.add(new StandardEventField(name, value)); + return this; + } + + public Builder addField(final EventField arg) { + if (arg != null) { + this.eventFields.add(arg); + } + return this; + } + + public Builder addFields(final Collection fields) { + if (fields != null) { + this.eventFields.addAll(fields); + } + return this; + } + + public Builder clearFields() { + this.eventFields.clear(); + return this; + } + + public Event build() { + return new StandardEvent(this); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + StandardEvent that = (StandardEvent) o; + return eventType == that.eventType && Objects.equals(eventFields, that.eventFields); + } + + @Override + public int hashCode() { + return Objects.hash(eventType, eventFields); + } + + @Override + public String toString() { + return "StandardEvent{eventType=" + eventType + ", eventFields=" + eventFields + '}'; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/StandardEventField.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/StandardEventField.java new file mode 100644 index 0000000000..978d6fe116 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/StandardEventField.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.event; + +import org.apache.commons.lang3.Validate; +import org.apache.nifi.registry.hook.EventField; +import org.apache.nifi.registry.hook.EventFieldName; + +import java.util.Objects; + +/** + * Standard implementation of EventField. + */ +public class StandardEventField implements EventField { + + private final EventFieldName name; + + private final String value; + + public StandardEventField(final EventFieldName name, final String value) { + this.name = name; + this.value = value; + Validate.notNull(this.name); + Validate.notNull(this.value); + } + + @Override + public EventFieldName getName() { + return name; + } + + @Override + public String getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + StandardEventField that = (StandardEventField) o; + return name == that.name && Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(name, value); + } + + @Override + public String toString() { + return "StandardEventField{name=" + name + ", value='" + value + '\'' + '}'; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/exception/AdministrationException.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/exception/AdministrationException.java new file mode 100644 index 0000000000..8f9180ce0f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/exception/AdministrationException.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.exception; + +/** + * + */ +public class AdministrationException extends RuntimeException { + + public AdministrationException(Throwable cause) { + super(cause); + } + + public AdministrationException(String message, Throwable cause) { + super(message, cause); + } + + public AdministrationException(String message) { + super(message); + } + + public AdministrationException() { + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/exception/ResourceNotFoundException.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/exception/ResourceNotFoundException.java new file mode 100644 index 0000000000..a83e9e224a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/exception/ResourceNotFoundException.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.exception; + +/** + * An exception that is thrown when an entity is not found. + */ +public class ResourceNotFoundException extends RuntimeException { + + public ResourceNotFoundException(String message) { + super(message); + } + + public ResourceNotFoundException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionClassLoader.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionClassLoader.java new file mode 100644 index 0000000000..1411f296bc --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionClassLoader.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension; + +import java.net.URL; +import java.net.URLClassLoader; + +/** + * Extend URLClassLoader to keep track of the root directory. + */ +public class ExtensionClassLoader extends URLClassLoader { + + private final String rootDir; + + public ExtensionClassLoader(final String rootDir, final URL[] urls, final ClassLoader parent) { + super(urls, parent); + this.rootDir = rootDir; + } + + public String getRootDir() { + return rootDir; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionCloseable.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionCloseable.java new file mode 100644 index 0000000000..0735f2c95e --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionCloseable.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension; + +import java.io.Closeable; +import java.io.IOException; + +public class ExtensionCloseable implements Closeable { + private final ClassLoader toSet; + + private ExtensionCloseable(ClassLoader toSet) { + this.toSet = toSet; + } + + public static ExtensionCloseable withComponentClassLoader(final ExtensionManager manager, final Class componentClass) { + + final ClassLoader current = Thread.currentThread().getContextClassLoader(); + final ExtensionCloseable closeable = new ExtensionCloseable(current); + + ClassLoader componentClassLoader = manager.getExtensionClassLoader(componentClass.getName()); + if (componentClassLoader == null) { + componentClassLoader = componentClass.getClassLoader(); + } + + Thread.currentThread().setContextClassLoader(componentClassLoader); + return closeable; + } + + public static ExtensionCloseable withClassLoader(final ClassLoader componentClassLoader) { + final ClassLoader current = Thread.currentThread().getContextClassLoader(); + final ExtensionCloseable closeable = new ExtensionCloseable(current); + Thread.currentThread().setContextClassLoader(componentClassLoader); + return closeable; + } + + @Override + public void close() throws IOException { + if (toSet != null) { + Thread.currentThread().setContextClassLoader(toSet); + } + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionManager.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionManager.java new file mode 100644 index 0000000000..edb3350c18 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionManager.java @@ -0,0 +1,201 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.hook.EventHookProvider; +import org.apache.nifi.registry.security.authentication.IdentityProvider; +import org.apache.nifi.registry.security.authorization.AccessPolicyProvider; +import org.apache.nifi.registry.security.authorization.Authorizer; +import org.apache.nifi.registry.security.authorization.UserGroupProvider; +import org.apache.nifi.registry.flow.FlowPersistenceProvider; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +@Component +public class ExtensionManager { + + static final Logger LOGGER = LoggerFactory.getLogger(ExtensionManager.class); + + private static final List EXTENSION_CLASSES; + static { + final List classes = new ArrayList<>(); + classes.add(FlowPersistenceProvider.class); + classes.add(UserGroupProvider.class); + classes.add(AccessPolicyProvider.class); + classes.add(Authorizer.class); + classes.add(IdentityProvider.class); + classes.add(EventHookProvider.class); + classes.add(BundlePersistenceProvider.class); + EXTENSION_CLASSES = Collections.unmodifiableList(classes); + } + + private final NiFiRegistryProperties properties; + private final Map classLoaderMap = new HashMap<>(); + private final AtomicBoolean loaded = new AtomicBoolean(false); + + @Autowired + public ExtensionManager(final NiFiRegistryProperties properties) { + this.properties = properties; + } + + @PostConstruct + public synchronized void discoverExtensions() { + if (!loaded.get()) { + // get the list of class loaders to consider + final List classLoaders = getClassLoaders(); + + // for each class loader, attempt to load each extension class using the ServiceLoader + for (final ExtensionClassLoader extensionClassLoader : classLoaders) { + for (final Class extensionClass : EXTENSION_CLASSES) { + loadExtensions(extensionClass, extensionClassLoader); + } + } + + loaded.set(true); + } + } + + public ExtensionClassLoader getExtensionClassLoader(final String canonicalClassName) { + if (StringUtils.isBlank(canonicalClassName)) { + throw new IllegalArgumentException("Class name can not be null"); + } + + return classLoaderMap.get(canonicalClassName); + } + + /** + * Loads implementations of the given extension class from the given class loader. + * + * @param extensionClass the extension/service class + * @param extensionClassLoader the class loader to search + */ + private void loadExtensions(final Class extensionClass, final ExtensionClassLoader extensionClassLoader) { + final ServiceLoader serviceLoader = ServiceLoader.load(extensionClass, extensionClassLoader); + for (final Object o : serviceLoader) { + final String extensionClassName = o.getClass().getCanonicalName(); + if (classLoaderMap.containsKey(extensionClassName)) { + final String currDir = extensionClassLoader.getRootDir(); + final String existingDir = classLoaderMap.get(extensionClassName).getRootDir(); + LOGGER.warn("Skipping {} from {} which was already found in {}", new Object[]{extensionClassName, currDir, existingDir}); + } else { + classLoaderMap.put(o.getClass().getCanonicalName(), extensionClassLoader); + } + } + } + + /** + * Gets all of the class loaders to consider for loading extensions. + * + * Includes the class loader of the web-app running the framework, plus a class loader for each additional + * directory specified in nifi-registry.properties. + * + * @return a list of extension class loaders + */ + private List getClassLoaders() { + final List classLoaders = new ArrayList<>(); + + // start with the class loader that loaded ExtensionManager, should be WebAppClassLoader for API WAR + final ExtensionClassLoader frameworkClassLoader = new ExtensionClassLoader("web-api", new URL[0], this.getClass().getClassLoader()); + classLoaders.add(frameworkClassLoader); + + // we want to use the system class loader as the parent of the extension class loaders + ClassLoader systemClassLoader = FlowPersistenceProvider.class.getClassLoader(); + + // add a class loader for each extension dir + final Set extensionDirs = properties.getExtensionsDirs(); + for (final String dir : extensionDirs) { + if (!StringUtils.isBlank(dir)) { + final ExtensionClassLoader classLoader = createClassLoader(dir, systemClassLoader); + if (classLoader != null) { + classLoaders.add(classLoader); + } + } + } + + return classLoaders; + } + + /** + * Creates a class loader for the given directory of resources. + * + * @param dir the dir of resources to add to the class loader + * @param parentClassLoader the parent class loader + * @return a class loader including all of the resources in the given dir, with the specified parent class loader + */ + private ExtensionClassLoader createClassLoader(final String dir, final ClassLoader parentClassLoader) { + final File dirFile = new File(dir); + + if (!dirFile.exists()) { + LOGGER.warn("Skipping extension directory that does not exist: " + dir); + return null; + } + + if (!dirFile.canRead()) { + LOGGER.warn("Skipping extension directory that can not be read: " + dir); + return null; + } + + final List resources = new LinkedList<>(); + + try { + resources.add(dirFile.toURI().toURL()); + } catch (final MalformedURLException mfe) { + LOGGER.warn("Unable to add {} to classpath due to {}", + new Object[]{ dirFile.getAbsolutePath(), mfe.getMessage()}, mfe); + } + + if (dirFile.isDirectory()) { + final File[] files = dirFile.listFiles(); + if (files != null) { + for (final File resource : files) { + if (resource.isDirectory()) { + LOGGER.warn("Recursive directories are not supported, skipping " + resource.getAbsolutePath()); + } else { + try { + resources.add(resource.toURI().toURL()); + } catch (final MalformedURLException mfe) { + LOGGER.warn("Unable to add {} to classpath due to {}", + new Object[]{ resource.getAbsolutePath(), mfe.getMessage()}, mfe); + } + } + } + } + } + + final URL[] urls = resources.toArray(new URL[resources.size()]); + return new ExtensionClassLoader(dir, urls, parentClassLoader); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/ProviderFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/ProviderFactory.java new file mode 100644 index 0000000000..609ec5a082 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/ProviderFactory.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider; + +import java.util.List; + +import org.apache.nifi.registry.extension.BundlePersistenceProvider; +import org.apache.nifi.registry.flow.FlowPersistenceProvider; +import org.apache.nifi.registry.hook.EventHookProvider; + +/** + * A factory for obtaining the configured providers. + */ +public interface ProviderFactory { + + /** + * Initialize the factory. + * + * @throws ProviderFactoryException if an error occurs during initialization + */ + void initialize() throws ProviderFactoryException; + + /** + * @return the configured FlowPersistenceProvider + */ + FlowPersistenceProvider getFlowPersistenceProvider(); + + /** + * @return the configured FlowHookProviders + */ + List getEventHookProviders(); + + /** + * @return the configured BundlePersistenceProvider + */ + BundlePersistenceProvider getBundlePersistenceProvider(); + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/ProviderFactoryException.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/ProviderFactoryException.java new file mode 100644 index 0000000000..3842b9ee36 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/ProviderFactoryException.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider; + +/** + * An error that occurs while initializing a ProviderFactory. + */ +public class ProviderFactoryException extends RuntimeException { + + public ProviderFactoryException() { + } + + public ProviderFactoryException(String message) { + super(message); + } + + public ProviderFactoryException(String message, Throwable cause) { + super(message, cause); + } + + public ProviderFactoryException(Throwable cause) { + super(cause); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/StandardProviderConfigurationContext.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/StandardProviderConfigurationContext.java new file mode 100644 index 0000000000..8f186fdfdf --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/StandardProviderConfigurationContext.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Standard configuration context to be passed to onConfigured method of Providers. + */ +public class StandardProviderConfigurationContext implements ProviderConfigurationContext { + + private final Map properties; + + public StandardProviderConfigurationContext(final Map properties) { + this.properties = Collections.unmodifiableMap(new HashMap<>(properties)); + } + + @Override + public Map getProperties() { + return properties; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/StandardProviderFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/StandardProviderFactory.java new file mode 100644 index 0000000000..3d14d6740c --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/StandardProviderFactory.java @@ -0,0 +1,326 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider; + +import org.apache.nifi.registry.extension.BundlePersistenceProvider; +import org.apache.nifi.registry.extension.ExtensionManager; +import org.apache.nifi.registry.flow.FlowPersistenceProvider; +import org.apache.nifi.registry.hook.EventHookProvider; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.apache.nifi.registry.provider.generated.Property; +import org.apache.nifi.registry.provider.generated.Providers; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.xml.sax.SAXException; + +import javax.annotation.PostConstruct; +import javax.sql.DataSource; +import javax.xml.XMLConstants; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBElement; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import java.io.File; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Standard implementation of ProviderFactory. + */ +@Configuration +public class StandardProviderFactory implements ProviderFactory, DisposableBean { + + private static final Logger LOGGER = LoggerFactory.getLogger(StandardProviderFactory.class); + + private static final String PROVIDERS_XSD = "/providers.xsd"; + private static final String JAXB_GENERATED_PATH = "org.apache.nifi.registry.provider.generated"; + private static final JAXBContext JAXB_CONTEXT = initializeJaxbContext(); + + /** + * Load the JAXBContext. + */ + private static JAXBContext initializeJaxbContext() { + try { + return JAXBContext.newInstance(JAXB_GENERATED_PATH, StandardProviderFactory.class.getClassLoader()); + } catch (JAXBException e) { + throw new RuntimeException("Unable to create JAXBContext.", e); + } + } + + private final NiFiRegistryProperties properties; + private final ExtensionManager extensionManager; + private final DataSource dataSource; + private final AtomicReference providersHolder = new AtomicReference<>(null); + + private FlowPersistenceProvider flowPersistenceProvider; + private List eventHookProviders; + private BundlePersistenceProvider bundlePersistenceProvider; + + @Autowired + public StandardProviderFactory(final NiFiRegistryProperties properties, final ExtensionManager extensionManager, final DataSource dataSource) { + this.properties = properties; + this.extensionManager = extensionManager; + this.dataSource = dataSource; + + if (this.properties == null) { + throw new IllegalStateException("NiFiRegistryProperties cannot be null"); + } + + if (this.extensionManager == null) { + throw new IllegalStateException("ExtensionManager cannot be null"); + } + + if (this.dataSource == null) { + throw new IllegalStateException("DataSource cannot be null"); + } + } + + @PostConstruct + @Override + public synchronized void initialize() throws ProviderFactoryException { + if (providersHolder.get() == null) { + final File providersConfigFile = properties.getProvidersConfigurationFile(); + if (providersConfigFile.exists()) { + try { + // find the schema + final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + final Schema schema = schemaFactory.newSchema(StandardProviderFactory.class.getResource(PROVIDERS_XSD)); + + // attempt to unmarshal + final Unmarshaller unmarshaller = JAXB_CONTEXT.createUnmarshaller(); + unmarshaller.setSchema(schema); + + // set the holder for later use + final JAXBElement element = unmarshaller.unmarshal(new StreamSource(providersConfigFile), Providers.class); + providersHolder.set(element.getValue()); + } catch (SAXException | JAXBException e) { + LOGGER.error(e.getMessage(), e); + throw new ProviderFactoryException("Unable to load the providers configuration file at: " + providersConfigFile.getAbsolutePath(), e); + } + } else { + throw new ProviderFactoryException("Unable to find the providers configuration file at " + providersConfigFile.getAbsolutePath()); + } + } + } + + @Bean + @Override + public synchronized FlowPersistenceProvider getFlowPersistenceProvider() { + if (flowPersistenceProvider == null) { + if (providersHolder.get() == null) { + throw new ProviderFactoryException("ProviderFactory must be initialized before obtaining a Provider"); + } + + final Providers providers = providersHolder.get(); + final org.apache.nifi.registry.provider.generated.Provider jaxbFlowProvider = providers.getFlowPersistenceProvider(); + final String flowProviderClassName = jaxbFlowProvider.getClazz(); + + try { + final ClassLoader classLoader = extensionManager.getExtensionClassLoader(flowProviderClassName); + if (classLoader == null) { + throw new IllegalStateException("Extension not found in any of the configured class loaders: " + flowProviderClassName); + } + + final Class rawFlowProviderClass = Class.forName(flowProviderClassName, true, classLoader); + final Class flowProviderClass = rawFlowProviderClass.asSubclass(FlowPersistenceProvider.class); + + final Constructor constructor = flowProviderClass.getConstructor(); + flowPersistenceProvider = (FlowPersistenceProvider) constructor.newInstance(); + + performMethodInjection(flowPersistenceProvider, flowProviderClass); + + LOGGER.info("Instantiated FlowPersistenceProvider with class name {}", new Object[]{flowProviderClassName}); + } catch (Exception e) { + LOGGER.error(e.getMessage(), e); + throw new ProviderFactoryException("Error creating FlowPersistenceProvider with class name: " + flowProviderClassName, e); + } + + final ProviderConfigurationContext configurationContext = createConfigurationContext(jaxbFlowProvider.getProperty()); + flowPersistenceProvider.onConfigured(configurationContext); + LOGGER.info("Configured FlowPersistenceProvider with class name {}", new Object[]{flowProviderClassName}); + } + + return flowPersistenceProvider; + } + + @Bean + @Override + public List getEventHookProviders() { + if (eventHookProviders == null) { + eventHookProviders = new ArrayList<>(); + + if (providersHolder.get() == null) { + throw new ProviderFactoryException("ProviderFactory must be initialized before obtaining a Provider"); + } + + final Providers providers = providersHolder.get(); + final List jaxbHookProvider = providers.getEventHookProvider(); + + if(jaxbHookProvider == null || jaxbHookProvider.isEmpty()) { + // no hook provided + return eventHookProviders; + } + + for (org.apache.nifi.registry.provider.generated.Provider hookProvider : jaxbHookProvider) { + + final String hookProviderClassName = hookProvider.getClazz(); + EventHookProvider hook; + + try { + final ClassLoader classLoader = extensionManager.getExtensionClassLoader(hookProviderClassName); + if (classLoader == null) { + throw new IllegalStateException("Extension not found in any of the configured class loaders: " + hookProviderClassName); + } + + final Class rawHookProviderClass = Class.forName(hookProviderClassName, true, classLoader); + final Class hookProviderClass = rawHookProviderClass.asSubclass(EventHookProvider.class); + + final Constructor constructor = hookProviderClass.getConstructor(); + hook = (EventHookProvider) constructor.newInstance(); + + performMethodInjection(hook, hookProviderClass); + + LOGGER.info("Instantiated EventHookProvider with class name {}", new Object[] {hookProviderClassName}); + } catch (Exception e) { + LOGGER.error(e.getMessage(), e); + throw new ProviderFactoryException("Error creating EventHookProvider with class name: " + hookProviderClassName, e); + } + + final ProviderConfigurationContext configurationContext = createConfigurationContext(hookProvider.getProperty()); + hook.onConfigured(configurationContext); + eventHookProviders.add(hook); + LOGGER.info("Configured EventHookProvider with class name {}", new Object[] {hookProviderClassName}); + } + } + + return eventHookProviders; + } + + @Bean + @Override + public synchronized BundlePersistenceProvider getBundlePersistenceProvider() { + if (bundlePersistenceProvider == null) { + if (providersHolder.get() == null) { + throw new ProviderFactoryException("ProviderFactory must be initialized before obtaining a Provider"); + } + + final Providers providers = providersHolder.get(); + final org.apache.nifi.registry.provider.generated.Provider jaxbExtensionBundleProvider = providers.getExtensionBundlePersistenceProvider(); + final String extensionBundleProviderClassName = jaxbExtensionBundleProvider.getClazz(); + + try { + final ClassLoader classLoader = extensionManager.getExtensionClassLoader(extensionBundleProviderClassName); + if (classLoader == null) { + throw new IllegalStateException("Extension not found in any of the configured class loaders: " + extensionBundleProviderClassName); + } + + final Class rawProviderClass = Class.forName(extensionBundleProviderClassName, true, classLoader); + + final Class extensionBundleProviderClass = + rawProviderClass.asSubclass(BundlePersistenceProvider.class); + + final Constructor constructor = extensionBundleProviderClass.getConstructor(); + bundlePersistenceProvider = (BundlePersistenceProvider) constructor.newInstance(); + + performMethodInjection(bundlePersistenceProvider, extensionBundleProviderClass); + + LOGGER.info("Instantiated BundlePersistenceProvider with class name {}", new Object[] {extensionBundleProviderClassName}); + } catch (Exception e) { + LOGGER.error(e.getMessage(), e); + throw new ProviderFactoryException("Error creating BundlePersistenceProvider with class name: " + extensionBundleProviderClassName, e); + } + + final ProviderConfigurationContext configurationContext = createConfigurationContext(jaxbExtensionBundleProvider.getProperty()); + bundlePersistenceProvider.onConfigured(configurationContext); + LOGGER.info("Configured BundlePersistenceProvider with class name {}", new Object[] {extensionBundleProviderClassName}); + } + + return bundlePersistenceProvider; + } + + @Override + public void destroy() throws Exception { + final List providers = new ArrayList<>(eventHookProviders); + providers.add(flowPersistenceProvider); + providers.add(bundlePersistenceProvider); + + for (final Provider provider : providers) { + if (provider != null) { + try { + provider.preDestruction(); + } catch (Throwable t) { + LOGGER.error(t.getMessage(), t); + } + } + } + } + + private ProviderConfigurationContext createConfigurationContext(final List configProperties) { + final Map properties = new HashMap<>(); + + if (configProperties != null) { + configProperties.stream().forEach(p -> properties.put(p.getName(), p.getValue())); + } + + return new StandardProviderConfigurationContext(properties); + } + + private void performMethodInjection(final Object instance, final Class providerClass) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { + for (final Method method : providerClass.getMethods()) { + if (method.isAnnotationPresent(ProviderContext.class)) { + // make the method accessible + final boolean isAccessible = method.isAccessible(); + method.setAccessible(true); + + try { + final Class[] argumentTypes = method.getParameterTypes(); + + // look for setters (single argument) + if (argumentTypes.length == 1) { + final Class argumentType = argumentTypes[0]; + + // look for well known types, currently we only support injecting the DataSource + if (DataSource.class.isAssignableFrom(argumentType)) { + method.invoke(instance, dataSource); + } + } + } finally { + method.setAccessible(isAccessible); + } + } + } + + final Class parentClass = providerClass.getSuperclass(); + if (parentClass != null && Provider.class.isAssignableFrom(parentClass)) { + performMethodInjection(instance, parentClass); + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/FileSystemBundlePersistenceProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/FileSystemBundlePersistenceProvider.java new file mode 100644 index 0000000000..555260a227 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/FileSystemBundlePersistenceProvider.java @@ -0,0 +1,249 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider.extension; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.extension.BundleCoordinate; +import org.apache.nifi.registry.extension.BundlePersistenceContext; +import org.apache.nifi.registry.extension.BundlePersistenceException; +import org.apache.nifi.registry.extension.BundlePersistenceProvider; +import org.apache.nifi.registry.extension.BundleVersionCoordinate; +import org.apache.nifi.registry.extension.BundleVersionType; +import org.apache.nifi.registry.flow.FlowPersistenceException; +import org.apache.nifi.registry.provider.ProviderConfigurationContext; +import org.apache.nifi.registry.provider.ProviderCreationException; +import org.apache.nifi.registry.util.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Map; + +/** + * An {@link BundlePersistenceProvider} that uses local file-system for storage. + */ +public class FileSystemBundlePersistenceProvider implements BundlePersistenceProvider { + + private static final Logger LOGGER = LoggerFactory.getLogger(FileSystemBundlePersistenceProvider.class); + + static final String BUNDLE_STORAGE_DIR_PROP = "Extension Bundle Storage Directory"; + + static final String NAR_EXTENSION = ".nar"; + static final String CPP_EXTENSION = ".cpp"; + + private File bundleStorageDir; + + @Override + public void onConfigured(final ProviderConfigurationContext configurationContext) + throws ProviderCreationException { + final Map props = configurationContext.getProperties(); + if (!props.containsKey(BUNDLE_STORAGE_DIR_PROP)) { + throw new ProviderCreationException("The property " + BUNDLE_STORAGE_DIR_PROP + " must be provided"); + } + + final String bundleStorageDirValue = props.get(BUNDLE_STORAGE_DIR_PROP); + if (StringUtils.isBlank(bundleStorageDirValue)) { + throw new ProviderCreationException("The property " + BUNDLE_STORAGE_DIR_PROP + " cannot be null or blank"); + } + + try { + bundleStorageDir = new File(bundleStorageDirValue); + FileUtils.ensureDirectoryExistAndCanReadAndWrite(bundleStorageDir); + LOGGER.info("Configured BundlePersistenceProvider with Extension Bundle Storage Directory {}", + new Object[] {bundleStorageDir.getAbsolutePath()}); + } catch (IOException e) { + throw new ProviderCreationException(e); + } + } + + @Override + public synchronized void createBundleVersion(final BundlePersistenceContext context, final InputStream contentStream) + throws BundlePersistenceException { + saveOrUpdateBundleVersion(context, contentStream, false); + } + + @Override + public synchronized void updateBundleVersion(final BundlePersistenceContext context, final InputStream contentStream) throws BundlePersistenceException { + saveOrUpdateBundleVersion(context, contentStream, true); + } + + private synchronized void saveOrUpdateBundleVersion(final BundlePersistenceContext context, final InputStream contentStream, + final boolean overwrite) throws BundlePersistenceException { + final BundleVersionCoordinate versionCoordinate = context.getCoordinate(); + final File bundleVersionDir = getBundleVersionDirectory(bundleStorageDir, versionCoordinate); + try { + FileUtils.ensureDirectoryExistAndCanReadAndWrite(bundleVersionDir); + } catch (IOException e) { + throw new FlowPersistenceException("Error accessing directory for extension bundle version at " + + bundleVersionDir.getAbsolutePath(), e); + } + + final File bundleFile = getBundleFile(bundleVersionDir, versionCoordinate); + if (bundleFile.exists() && !overwrite) { + final String existingPath = bundleFile.getAbsolutePath(); + throw new BundlePersistenceException("Unable to save because a bundle versions already exists at " + existingPath); + } + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Writing extension bundle to {}", new Object[]{bundleFile.getAbsolutePath()}); + } + + try (final OutputStream out = new FileOutputStream(bundleFile)) { + IOUtils.copy(contentStream, out); + out.flush(); + } catch (Exception e) { + throw new FlowPersistenceException("Unable to write bundle file to disk due to " + e.getMessage(), e); + } + } + + @Override + public synchronized void getBundleVersionContent(final BundleVersionCoordinate versionCoordinate, final OutputStream outputStream) + throws BundlePersistenceException { + + final File bundleFile = getBundleFile(versionCoordinate); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Reading extension bundle from {}", new Object[]{bundleFile.getAbsolutePath()}); + } + + try (final InputStream in = new FileInputStream(bundleFile); + final BufferedInputStream bufIn = new BufferedInputStream(in)) { + IOUtils.copy(bufIn, outputStream); + outputStream.flush(); + } catch (FileNotFoundException e) { + throw new BundlePersistenceException("Extension bundle content was not found for: " + bundleFile.getAbsolutePath(), e); + } catch (IOException e) { + throw new BundlePersistenceException("Error reading extension bundle content", e); + } + } + + @Override + public synchronized void deleteBundleVersion(final BundleVersionCoordinate versionCoordinate) throws BundlePersistenceException { + final File bundleFile = getBundleFile(versionCoordinate); + if (!bundleFile.exists()) { + LOGGER.warn("Extension bundle content does not exist at {}", new Object[] {bundleFile.getAbsolutePath()}); + return; + } + + final boolean deleted = bundleFile.delete(); + if (!deleted) { + throw new BundlePersistenceException("Unable to delete extension bundle content at " + bundleFile.getAbsolutePath()); + } + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Deleted extension bundle content at {}", new Object[] {bundleFile.getAbsolutePath()}); + } + } + + @Override + public synchronized void deleteAllBundleVersions(final BundleCoordinate bundleCoordinate) throws BundlePersistenceException { + final File bundleDir = getBundleDirectory(bundleStorageDir, bundleCoordinate); + if (!bundleDir.exists()) { + LOGGER.warn("Extension bundle directory does not exist at {}", new Object[] {bundleDir.getAbsolutePath()}); + return; + } + + // delete everything under the bundle directory + try { + org.apache.commons.io.FileUtils.cleanDirectory(bundleDir); + } catch (IOException e) { + throw new FlowPersistenceException("Error deleting extension bundles at " + bundleDir.getAbsolutePath(), e); + } + + // delete the directory for the bundle + final boolean bundleDirDeleted = bundleDir.delete(); + if (!bundleDirDeleted) { + LOGGER.error("Unable to delete extension bundle directory: " + bundleDir.getAbsolutePath()); + } + + // delete the directory for the group and bucket if there is nothing left + final File groupDir = bundleDir.getParentFile(); + final File[] groupFiles = groupDir.listFiles(); + if (groupFiles.length == 0) { + final boolean deletedGroup = groupDir.delete(); + if (!deletedGroup) { + LOGGER.error("Unable to delete group directory: " + groupDir.getAbsolutePath()); + } else { + final File bucketDir = groupDir.getParentFile(); + final File[] bucketFiles = bucketDir.listFiles(); + if (bucketFiles.length == 0){ + final boolean deletedBucket = bucketDir.delete(); + if (!deletedBucket) { + LOGGER.error("Unable to delete bucket directory: " + bucketDir.getAbsolutePath()); + } + } + } + } + } + + private File getBundleFile(final BundleVersionCoordinate coordinate) { + final File bundleVersionDir = getBundleVersionDirectory(bundleStorageDir, coordinate); + return getBundleFile(bundleVersionDir, coordinate); + } + + static File getBundleDirectory(final File bundleStorageDir, final BundleCoordinate bundleCoordinate) { + final String bucketId = bundleCoordinate.getBucketId(); + final String groupId = bundleCoordinate.getGroupId(); + final String artifactId = bundleCoordinate.getArtifactId(); + + return new File(bundleStorageDir, sanitize(bucketId) + "/" + sanitize(groupId) + "/" + sanitize(artifactId)); + } + + static File getBundleVersionDirectory(final File bundleStorageDir, final BundleVersionCoordinate versionCoordinate) { + final String bucketId = versionCoordinate.getBucketId(); + final String groupId = versionCoordinate.getGroupId(); + final String artifactId = versionCoordinate.getArtifactId(); + final String version = versionCoordinate.getVersion(); + + return new File(bundleStorageDir, sanitize(bucketId) + "/" + sanitize(groupId) + "/" + sanitize(artifactId) + "/" + sanitize(version)); + } + + static File getBundleFile(final File parentDir, final BundleVersionCoordinate versionCoordinate) { + final String artifactId = versionCoordinate.getArtifactId(); + final String version = versionCoordinate.getVersion(); + final BundleVersionType bundleType = versionCoordinate.getType(); + + + final String bundleFileExtension = getBundleFileExtension(bundleType); + final String bundleFilename = sanitize(artifactId) + "-" + sanitize(version) + bundleFileExtension; + return new File(parentDir, bundleFilename); + } + + static String sanitize(final String input) { + return FileUtils.sanitizeFilename(input).trim().toLowerCase(); + } + + static String getBundleFileExtension(final BundleVersionType bundleType) { + switch (bundleType) { + case NIFI_NAR: + return NAR_EXTENSION; + case MINIFI_CPP: + return CPP_EXTENSION; + default: + LOGGER.warn("Unknown bundle type: " + bundleType); + return ""; + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/StandardBundleCoordinate.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/StandardBundleCoordinate.java new file mode 100644 index 0000000000..c05cbdffd7 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/StandardBundleCoordinate.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider.extension; + +import org.apache.commons.lang3.Validate; +import org.apache.nifi.registry.extension.BundleCoordinate; + +import java.util.Objects; + +public class StandardBundleCoordinate implements BundleCoordinate { + + private final String bucketId; + private final String groupId; + private final String artifactId; + + private StandardBundleCoordinate(final Builder builder) { + this.bucketId = builder.bucketId; + this.groupId = builder.groupId; + this.artifactId = builder.artifactId; + Validate.notBlank(this.bucketId, "Bucket Id is required"); + Validate.notBlank(this.groupId, "Group Id is required"); + Validate.notBlank(this.artifactId, "Artifact Id is required"); + } + + @Override + public String getBucketId() { + return bucketId; + } + + @Override + public String getGroupId() { + return groupId; + } + + @Override + public String getArtifactId() { + return artifactId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final StandardBundleCoordinate that = (StandardBundleCoordinate) o; + return bucketId.equals(that.bucketId) + && groupId.equals(that.groupId) + && artifactId.equals(that.artifactId); + } + + @Override + public int hashCode() { + return Objects.hash(bucketId, groupId, artifactId); + } + + public static class Builder { + + private String bucketId; + private String groupId; + private String artifactId; + + public Builder bucketId(final String bucketId) { + this.bucketId = bucketId; + return this; + } + + public Builder groupId(final String groupId) { + this.groupId = groupId; + return this; + } + + public Builder artifactId(final String artifactId) { + this.artifactId = artifactId; + return this; + } + + + public StandardBundleCoordinate build() { + return new StandardBundleCoordinate(this); + } + + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/StandardBundlePersistenceContext.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/StandardBundlePersistenceContext.java new file mode 100644 index 0000000000..249be1918e --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/StandardBundlePersistenceContext.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider.extension; + +import org.apache.commons.lang3.Validate; +import org.apache.nifi.registry.extension.BundlePersistenceContext; +import org.apache.nifi.registry.extension.BundleVersionCoordinate; + +public class StandardBundlePersistenceContext implements BundlePersistenceContext { + + private final BundleVersionCoordinate coordinate; + private final String author; + private final long timestamp; + private final long bundleSize; + + private StandardBundlePersistenceContext(final Builder builder) { + this.coordinate = builder.coordinate; + this.bundleSize = builder.bundleSize; + this.author = builder.author; + this.timestamp = builder.timestamp; + Validate.notNull(this.coordinate); + Validate.notBlank(this.author); + } + + + @Override + public BundleVersionCoordinate getCoordinate() { + return coordinate; + } + + @Override + public long getSize() { + return bundleSize; + } + + @Override + public long getTimestamp() { + return timestamp; + } + + @Override + public String getAuthor() { + return author; + } + + public static class Builder { + + private BundleVersionCoordinate coordinate; + private String author; + private long timestamp; + private long bundleSize; + + public Builder coordinate(final BundleVersionCoordinate identifier) { + this.coordinate = identifier; + return this; + } + + public Builder author(final String author) { + this.author = author; + return this; + } + + public Builder timestamp(final long timestamp) { + this.timestamp = timestamp; + return this; + } + + public Builder bundleSize(final long size) { + this.bundleSize = size; + return this; + } + + public StandardBundlePersistenceContext build() { + return new StandardBundlePersistenceContext(this); + } + + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/StandardBundleVersionCoordinate.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/StandardBundleVersionCoordinate.java new file mode 100644 index 0000000000..c51ae1be93 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/StandardBundleVersionCoordinate.java @@ -0,0 +1,137 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider.extension; + +import org.apache.commons.lang3.Validate; +import org.apache.nifi.registry.extension.BundleVersionCoordinate; +import org.apache.nifi.registry.extension.BundleVersionType; + +import java.util.Objects; + +public class StandardBundleVersionCoordinate implements BundleVersionCoordinate { + + private final String bucketId; + private final String groupId; + private final String artifactId; + private final String version; + private final BundleVersionType type; + + private StandardBundleVersionCoordinate(final Builder builder) { + this.bucketId = builder.bucketId; + this.groupId = builder.groupId; + this.artifactId = builder.artifactId; + this.version = builder.version; + this.type = builder.type; + Validate.notBlank(this.bucketId, "Bucket Id is required"); + Validate.notBlank(this.groupId, "Group Id is required"); + Validate.notBlank(this.artifactId, "Artifact Id is required"); + Validate.notBlank(this.version, "Version is required"); + Validate.notNull(this.type, "BundleVersionType is required"); + } + + @Override + public String getBucketId() { + return bucketId; + } + + @Override + public String getGroupId() { + return groupId; + } + + @Override + public String getArtifactId() { + return artifactId; + } + + @Override + public String getVersion() { + return version; + } + + @Override + public BundleVersionType getType() { + return type; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final StandardBundleVersionCoordinate that = (StandardBundleVersionCoordinate) o; + return bucketId.equals(that.bucketId) + && groupId.equals(that.groupId) + && artifactId.equals(that.artifactId) + && version.equals(that.version) + && type == that.type; + } + + @Override + public int hashCode() { + return Objects.hash(bucketId, groupId, artifactId, version, type); + } + + @Override + public String toString() { + return "BundleVersionCoordinate [" + + "bucketId='" + bucketId + '\'' + + ", groupId='" + groupId + '\'' + + ", artifactId='" + artifactId + '\'' + + ", version='" + version + '\'' + + ", type=" + type + + ']'; + } + + public static class Builder { + + private String bucketId; + private String groupId; + private String artifactId; + private String version; + private BundleVersionType type; + + public Builder bucketId(final String bucketId) { + this.bucketId = bucketId; + return this; + } + + public Builder groupId(final String groupId) { + this.groupId = groupId; + return this; + } + + public Builder artifactId(final String artifactId) { + this.artifactId = artifactId; + return this; + } + + public Builder version(final String version) { + this.version = version; + return this; + } + + public Builder type(final BundleVersionType type) { + this.type = type; + return this; + } + + public StandardBundleVersionCoordinate build() { + return new StandardBundleVersionCoordinate(this); + } + + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/DatabaseFlowPersistenceProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/DatabaseFlowPersistenceProvider.java new file mode 100644 index 0000000000..ee385921dc --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/DatabaseFlowPersistenceProvider.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider.flow; + +import org.apache.nifi.registry.flow.FlowPersistenceException; +import org.apache.nifi.registry.flow.FlowPersistenceProvider; +import org.apache.nifi.registry.flow.FlowSnapshotContext; +import org.apache.nifi.registry.provider.ProviderConfigurationContext; +import org.apache.nifi.registry.provider.ProviderContext; +import org.apache.nifi.registry.provider.ProviderCreationException; +import org.springframework.jdbc.core.JdbcTemplate; + +import javax.sql.DataSource; +import java.util.ArrayList; +import java.util.List; + +/** + * A FlowPersistenceProvider that uses a database table for storage. The intent is to use the same database as the rest + * of the application so that all data can be stored together and benefit from any replication/scaling of the database. + */ +public class DatabaseFlowPersistenceProvider implements FlowPersistenceProvider { + + private DataSource dataSource; + private JdbcTemplate jdbcTemplate; + + @ProviderContext + public void setDataSource(final DataSource dataSource) { + this.dataSource = dataSource; + this.jdbcTemplate = new JdbcTemplate(this.dataSource); + } + + @Override + public void onConfigured(final ProviderConfigurationContext configurationContext) throws ProviderCreationException { + // there is no config since we get the DataSource from the framework + } + + @Override + public void saveFlowContent(final FlowSnapshotContext context, final byte[] content) throws FlowPersistenceException { + final String sql = "INSERT INTO FLOW_PERSISTENCE_PROVIDER (BUCKET_ID, FLOW_ID, VERSION, FLOW_CONTENT) VALUES (?, ?, ?, ?)"; + jdbcTemplate.update(sql, context.getBucketId(), context.getFlowId(), context.getVersion(), content); + } + + @Override + public byte[] getFlowContent(final String bucketId, final String flowId, final int version) throws FlowPersistenceException { + final List results = new ArrayList<>(); + final String sql = "SELECT FLOW_CONTENT FROM FLOW_PERSISTENCE_PROVIDER WHERE BUCKET_ID = ? and FLOW_ID = ? and VERSION = ?"; + + jdbcTemplate.query(sql, new Object[] {bucketId, flowId, version}, (rs) -> { + final byte[] content = rs.getBytes("FLOW_CONTENT"); + results.add(content); + }); + + if (results.isEmpty()) { + return null; + } else { + return results.get(0); + } + } + + @Override + public void deleteAllFlowContent(final String bucketId, final String flowId) throws FlowPersistenceException { + final String sql = "DELETE FROM FLOW_PERSISTENCE_PROVIDER WHERE BUCKET_ID = ? and FLOW_ID = ?"; + jdbcTemplate.update(sql, bucketId, flowId); + } + + @Override + public void deleteFlowContent(final String bucketId, final String flowId, final int version) throws FlowPersistenceException { + final String sql = "DELETE FROM FLOW_PERSISTENCE_PROVIDER WHERE BUCKET_ID = ? and FLOW_ID = ? and VERSION = ?"; + jdbcTemplate.update(sql, bucketId, flowId, version); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/FileSystemFlowPersistenceProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/FileSystemFlowPersistenceProvider.java new file mode 100644 index 0000000000..071656d0eb --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/FileSystemFlowPersistenceProvider.java @@ -0,0 +1,186 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider.flow; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.flow.FlowPersistenceException; +import org.apache.nifi.registry.flow.FlowPersistenceProvider; +import org.apache.nifi.registry.flow.FlowSnapshotContext; +import org.apache.nifi.registry.provider.ProviderConfigurationContext; +import org.apache.nifi.registry.provider.ProviderCreationException; +import org.apache.nifi.registry.util.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Map; + +/** + * A FlowPersistenceProvider that uses the local filesystem for storage. + */ +public class FileSystemFlowPersistenceProvider implements FlowPersistenceProvider { + + static final Logger LOGGER = LoggerFactory.getLogger(FileSystemFlowPersistenceProvider.class); + + static final String FLOW_STORAGE_DIR_PROP = "Flow Storage Directory"; + + static final String SNAPSHOT_EXTENSION = ".snapshot"; + + private File flowStorageDir; + + @Override + public void onConfigured(final ProviderConfigurationContext configurationContext) throws ProviderCreationException { + final Map props = configurationContext.getProperties(); + if (!props.containsKey(FLOW_STORAGE_DIR_PROP)) { + throw new ProviderCreationException("The property " + FLOW_STORAGE_DIR_PROP + " must be provided"); + } + + final String flowStorageDirValue = props.get(FLOW_STORAGE_DIR_PROP); + if (StringUtils.isBlank(flowStorageDirValue)) { + throw new ProviderCreationException("The property " + FLOW_STORAGE_DIR_PROP + " cannot be null or blank"); + } + + try { + flowStorageDir = new File(flowStorageDirValue); + FileUtils.ensureDirectoryExistAndCanReadAndWrite(flowStorageDir); + LOGGER.info("Configured FileSystemFlowPersistenceProvider with Flow Storage Directory {}", new Object[] {flowStorageDir.getAbsolutePath()}); + } catch (IOException e) { + throw new ProviderCreationException(e); + } + } + + @Override + public synchronized void saveFlowContent(final FlowSnapshotContext context, final byte[] content) throws FlowPersistenceException { + final File bucketDir = new File(flowStorageDir, context.getBucketId()); + try { + FileUtils.ensureDirectoryExistAndCanReadAndWrite(bucketDir); + } catch (IOException e) { + throw new FlowPersistenceException("Error accessing bucket directory at " + bucketDir.getAbsolutePath(), e); + } + + final File flowDir = new File(bucketDir, context.getFlowId()); + try { + FileUtils.ensureDirectoryExistAndCanReadAndWrite(flowDir); + } catch (IOException e) { + throw new FlowPersistenceException("Error accessing flow directory at " + flowDir.getAbsolutePath(), e); + } + + final String versionString = String.valueOf(context.getVersion()); + final File versionDir = new File(flowDir, versionString); + try { + FileUtils.ensureDirectoryExistAndCanReadAndWrite(versionDir); + } catch (IOException e) { + throw new FlowPersistenceException("Error accessing version directory at " + versionDir.getAbsolutePath(), e); + } + + final File versionFile = new File(versionDir, versionString + SNAPSHOT_EXTENSION); + if (versionFile.exists()) { + throw new FlowPersistenceException("Unable to save, a snapshot already exists with version " + versionString); + } + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Saving snapshot with filename {}", new Object[] {versionFile.getAbsolutePath()}); + } + + try (final OutputStream out = new FileOutputStream(versionFile)) { + out.write(content); + out.flush(); + } catch (Exception e) { + throw new FlowPersistenceException("Unable to write snapshot to disk due to " + e.getMessage(), e); + } + } + + @Override + public synchronized byte[] getFlowContent(final String bucketId, final String flowId, final int version) throws FlowPersistenceException { + final File snapshotFile = getSnapshotFile(bucketId, flowId, version); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Retrieving snapshot with filename {}", new Object[] {snapshotFile.getAbsolutePath()}); + } + + if (!snapshotFile.exists()) { + return null; + } + + try (final InputStream in = new FileInputStream(snapshotFile)){ + return IOUtils.toByteArray(in); + } catch (IOException e) { + throw new FlowPersistenceException("Error reading snapshot file: " + snapshotFile.getAbsolutePath(), e); + } + } + + @Override + public synchronized void deleteAllFlowContent(final String bucketId, final String flowId) throws FlowPersistenceException { + final File flowDir = new File(flowStorageDir, bucketId + "/" + flowId); + if (!flowDir.exists()) { + LOGGER.debug("Snapshot directory does not exist at {}", new Object[] {flowDir.getAbsolutePath()}); + return; + } + + // delete everything under the flow directory + try { + org.apache.commons.io.FileUtils.cleanDirectory(flowDir); + } catch (IOException e) { + throw new FlowPersistenceException("Error deleting snapshots at " + flowDir.getAbsolutePath(), e); + } + + // delete the directory for the flow + final boolean flowDirDeleted = flowDir.delete(); + if (!flowDirDeleted) { + LOGGER.error("Unable to delete flow directory: " + flowDir.getAbsolutePath()); + } + + // delete the directory for the bucket if there is nothing left + final File bucketDir = new File(flowStorageDir, bucketId); + final File[] bucketFiles = bucketDir.listFiles(); + if (bucketFiles.length == 0) { + final boolean deletedBucket = bucketDir.delete(); + if (!deletedBucket) { + LOGGER.error("Unable to delete bucket directory: " + flowDir.getAbsolutePath()); + } + } + } + + @Override + public synchronized void deleteFlowContent(final String bucketId, final String flowId, final int version) throws FlowPersistenceException { + final File snapshotFile = getSnapshotFile(bucketId, flowId, version); + if (!snapshotFile.exists()) { + LOGGER.debug("Snapshot file does not exist at {}", new Object[] {snapshotFile.getAbsolutePath()}); + return; + } + + final boolean deleted = snapshotFile.delete(); + if (!deleted) { + throw new FlowPersistenceException("Unable to delete snapshot at " + snapshotFile.getAbsolutePath()); + } + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Deleted snapshot at {}", new Object[] {snapshotFile.getAbsolutePath()}); + } + } + + protected File getSnapshotFile(final String bucketId, final String flowId, final int version) { + final String snapshotFilename = bucketId + "/" + flowId + "/" + version + "/" + version + SNAPSHOT_EXTENSION; + return new File(flowStorageDir, snapshotFilename); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/FlowMetadataSynchronizer.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/FlowMetadataSynchronizer.java new file mode 100644 index 0000000000..3b8a2147e4 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/FlowMetadataSynchronizer.java @@ -0,0 +1,138 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider.flow; + +import org.apache.nifi.registry.db.entity.BucketEntity; +import org.apache.nifi.registry.db.entity.BucketItemEntityType; +import org.apache.nifi.registry.db.entity.FlowEntity; +import org.apache.nifi.registry.db.entity.FlowSnapshotEntity; +import org.apache.nifi.registry.flow.FlowPersistenceProvider; +import org.apache.nifi.registry.flow.MetadataAwareFlowPersistenceProvider; +import org.apache.nifi.registry.metadata.BucketMetadata; +import org.apache.nifi.registry.metadata.FlowMetadata; +import org.apache.nifi.registry.metadata.FlowSnapshotMetadata; +import org.apache.nifi.registry.service.MetadataService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.List; + +@Component +public class FlowMetadataSynchronizer { + + private static final Logger LOGGER = LoggerFactory.getLogger(FlowMetadataSynchronizer.class); + + private MetadataService metadataService; + private FlowPersistenceProvider persistenceProvider; + + @Autowired + public FlowMetadataSynchronizer(final MetadataService metadataService, + final FlowPersistenceProvider persistenceProvider) { + this.metadataService = metadataService; + this.persistenceProvider = persistenceProvider; + } + + @EventListener(ContextRefreshedEvent.class) + public void synchronize() { + LOGGER.info("**************************************************"); + + if (!(persistenceProvider instanceof MetadataAwareFlowPersistenceProvider)) { + LOGGER.info("* FlowPersistenceProvider is not metadata-aware, nothing to synchronize"); + LOGGER.info("**************************************************"); + return; + } else { + LOGGER.info("* Found metadata-aware FlowPersistenceProvider..."); + } + + if (!metadataService.getAllBuckets().isEmpty()) { + LOGGER.info("* Found existing buckets, will not synchronize metadata"); + LOGGER.info("**************************************************"); + return; + } + + final MetadataAwareFlowPersistenceProvider metadataAwareFlowPersistenceProvider = (MetadataAwareFlowPersistenceProvider) persistenceProvider; + LOGGER.info("* Synchronizing metadata from FlowPersistenceProvider to metadata database..."); + + final List metadata = metadataAwareFlowPersistenceProvider.getMetadata(); + LOGGER.info("* Synchronizing {} bucket(s)", new Object[]{metadata.size()}); + + for (final BucketMetadata bucketMetadata : metadata) { + final BucketEntity bucketEntity = new BucketEntity(); + bucketEntity.setId(bucketMetadata.getIdentifier()); + bucketEntity.setName(bucketMetadata.getName()); + bucketEntity.setDescription(bucketMetadata.getDescription()); + bucketEntity.setCreated(new Date()); + metadataService.createBucket(bucketEntity); + createFlows(bucketMetadata); + } + + LOGGER.info("* Done synchronizing metadata!"); + LOGGER.info("**************************************************"); + } + + private void createFlows(final BucketMetadata bucketMetadata) { + LOGGER.info("* Synchronizing {} flow(s) for bucket {}", + new Object[]{bucketMetadata.getFlowMetadata().size(), bucketMetadata.getIdentifier()}); + + for (final FlowMetadata flowMetadata : bucketMetadata.getFlowMetadata()) { + final FlowEntity flowEntity = new FlowEntity(); + flowEntity.setType(BucketItemEntityType.FLOW); + flowEntity.setId(flowMetadata.getIdentifier()); + flowEntity.setName(flowMetadata.getName()); + flowEntity.setDescription(flowMetadata.getDescription()); + flowEntity.setBucketId(bucketMetadata.getIdentifier()); + flowEntity.setCreated(new Date()); + flowEntity.setModified(new Date()); + metadataService.createFlow(flowEntity); + + createFlowSnapshots(flowMetadata); + } + } + + private void createFlowSnapshots(final FlowMetadata flowMetadata) { + LOGGER.info("* Synchronizing {} version(s) for flow {}", + new Object[]{flowMetadata.getFlowSnapshotMetadata().size(), + flowMetadata.getIdentifier()}); + + for (final FlowSnapshotMetadata snapshotMetadata : flowMetadata.getFlowSnapshotMetadata()) { + final FlowSnapshotEntity snapshotEntity = new FlowSnapshotEntity(); + snapshotEntity.setFlowId(flowMetadata.getIdentifier()); + snapshotEntity.setVersion(snapshotMetadata.getVersion()); + snapshotEntity.setComments(snapshotMetadata.getComments()); + + String author = snapshotMetadata.getAuthor(); + if (author == null) { + author = "unknown"; + } + snapshotEntity.setCreatedBy(author); + + Long created = snapshotMetadata.getCreated(); + if (created == null) { + created = Long.valueOf(System.currentTimeMillis()); + } + snapshotEntity.setCreated(new Date(created)); + + metadataService.createFlowSnapshot(snapshotEntity); + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/StandardFlowSnapshotContext.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/StandardFlowSnapshotContext.java new file mode 100644 index 0000000000..dc0676927d --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/StandardFlowSnapshotContext.java @@ -0,0 +1,211 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider.flow; + +import org.apache.commons.lang3.Validate; +import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.flow.FlowSnapshotContext; +import org.apache.nifi.registry.flow.VersionedFlow; +import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata; + +import java.util.Objects; + +/** + * Standard implementation of FlowSnapshotContext. + */ +public class StandardFlowSnapshotContext implements FlowSnapshotContext { + + private final String bucketId; + private final String bucketName; + private final String flowId; + private final String flowName; + private final String flowDescription; + private final int version; + private final String comments; + private final String author; + private final long snapshotTimestamp; + + private StandardFlowSnapshotContext(final Builder builder) { + this.bucketId = builder.bucketId; + this.bucketName = builder.bucketName; + this.flowId = builder.flowId; + this.flowName = builder.flowName; + this.flowDescription = builder.flowDescription; + this.version = builder.version; + this.comments = builder.comments; + this.author = builder.author; + this.snapshotTimestamp = builder.snapshotTimestamp; + + Validate.notBlank(bucketId); + Validate.notBlank(bucketName); + Validate.notBlank(flowId); + Validate.notBlank(flowName); + Validate.isTrue(version > 0); + Validate.isTrue(snapshotTimestamp > 0); + } + + @Override + public String getBucketId() { + return bucketId; + } + + @Override + public String getBucketName() { + return bucketName; + } + + @Override + public String getFlowId() { + return flowId; + } + + @Override + public String getFlowName() { + return flowName; + } + + @Override + public String getFlowDescription() { + return flowDescription; + } + + @Override + public int getVersion() { + return version; + } + + @Override + public String getComments() { + return comments; + } + + @Override + public long getSnapshotTimestamp() { + return snapshotTimestamp; + } + + @Override + public String getAuthor() { + return author; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + StandardFlowSnapshotContext that = (StandardFlowSnapshotContext) o; + return version == that.version && snapshotTimestamp == that.snapshotTimestamp + && Objects.equals(bucketId, that.bucketId) + && Objects.equals(bucketName, that.bucketName) + && Objects.equals(flowId, that.flowId) + && Objects.equals(flowName, that.flowName) + && Objects.equals(comments, that.comments) + && Objects.equals(author, that.author); + } + + @Override + public int hashCode() { + return Objects.hash(bucketId, bucketName, flowId, flowName, version, comments, author, snapshotTimestamp); + } + + /** + * Builder for creating instances of StandardFlowSnapshotContext. + */ + public static class Builder { + + private String bucketId; + private String bucketName; + private String flowId; + private String flowName; + private String flowDescription; + private int version; + private String comments; + private String author; + private long snapshotTimestamp; + + public Builder() { + + } + + public Builder(final Bucket bucket, final VersionedFlow versionedFlow, final VersionedFlowSnapshotMetadata snapshotMetadata) { + bucketId(bucket.getIdentifier()); + bucketName(bucket.getName()); + flowId(snapshotMetadata.getFlowIdentifier()); + flowName(versionedFlow.getName()); + flowDescription(versionedFlow.getDescription()); + version(snapshotMetadata.getVersion()); + comments(snapshotMetadata.getComments()); + author(snapshotMetadata.getAuthor()); + snapshotTimestamp(snapshotMetadata.getTimestamp()); + } + + public Builder bucketId(final String bucketId) { + this.bucketId = bucketId; + return this; + } + + public Builder bucketName(final String bucketName) { + this.bucketName = bucketName; + return this; + } + + public Builder flowId(final String flowId) { + this.flowId = flowId; + return this; + } + + public Builder flowName(final String flowName) { + this.flowName = flowName; + return this; + } + + public Builder flowDescription(final String flowDescription) { + this.flowDescription = flowDescription; + return this; + } + + public Builder version(final int version) { + this.version = version; + return this; + } + + public Builder comments(final String comments) { + this.comments = comments; + return this; + } + + public Builder author(final String author) { + this.author = author; + return this; + } + + public Builder snapshotTimestamp(final long snapshotTimestamp) { + this.snapshotTimestamp = snapshotTimestamp; + return this; + } + + public StandardFlowSnapshotContext build() { + return new StandardFlowSnapshotContext(this); + } + + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/Bucket.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/Bucket.java new file mode 100644 index 0000000000..5772d231a4 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/Bucket.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider.flow.git; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +class Bucket { + private final String bucketId; + private String bucketDirName; + + /** + * Flow ID to Flow. + */ + private Map flows = new HashMap<>(); + + public Bucket(String bucketId) { + this.bucketId = bucketId; + } + + public String getBucketId() { + return bucketId; + } + + /** + * Returns the directory name of this bucket. + * @return can be different from original bucket name if it contained sanitized character. + */ + public String getBucketDirName() { + return bucketDirName; + } + + /** + * Set the name of bucket directory. + * @param bucketDirName The directory name must be sanitized, use {@link org.apache.nifi.registry.util.FileUtils#sanitizeFilename(String)} to do so. + */ + public void setBucketDirName(String bucketDirName) { + this.bucketDirName = bucketDirName; + } + + public Flow getFlowOrCreate(String flowId) { + return this.flows.computeIfAbsent(flowId, k -> new Flow(flowId)); + } + + public Optional getFlow(String flowId) { + return Optional.ofNullable(flows.get(flowId)); + } + + public void removeFlow(String flowId) { + flows.remove(flowId); + } + + public boolean isEmpty() { + return flows.isEmpty(); + } + + Map getFlows() { + return flows; + } + + /** + * Serialize the latest version of this Bucket meta data. + * @return serialized bucket + */ + Map serialize() { + final Map map = new HashMap<>(); + + map.put(GitFlowMetaData.LAYOUT_VERSION, GitFlowMetaData.CURRENT_LAYOUT_VERSION); + map.put(GitFlowMetaData.BUCKET_ID, bucketId); + map.put(GitFlowMetaData.FLOWS, + flows.keySet().stream().collect(Collectors.toMap(k -> k, k -> flows.get(k).serialize()))); + + return map; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/Flow.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/Flow.java new file mode 100644 index 0000000000..488a619e0f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/Flow.java @@ -0,0 +1,175 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider.flow.git; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +class Flow { + /** + * The ID of a Flow. It never changes. + */ + private final String flowId; + + /** + * A version to a Flow pointer. + */ + private final Map versions = new HashMap<>(); + + public Flow(String flowId) { + this.flowId = flowId; + } + + public boolean hasVersion(int version) { + return versions.containsKey(version); + } + + public FlowPointer getFlowVersion(int version) { + return versions.get(version); + } + + public void putVersion(int version, FlowPointer pointer) { + versions.put(version, pointer); + } + + Map getVersions() { + return versions; + } + + public static class FlowPointer { + private String gitRev; + private String objectId; + private final String fileName; + + // May not be populated pre-0.3.0 + private String flowName; + private String flowDescription; + private String author; + private String comment; + private Long created; + + /** + * Create new FlowPointer instance. + * @param fileName The filename must be sanitized, use {@link org.apache.nifi.registry.util.FileUtils#sanitizeFilename(String)} to do so. + */ + public FlowPointer(String fileName) { + this.fileName = fileName; + } + + public void setGitRev(String gitRev) { + this.gitRev = gitRev; + } + + public String getGitRev() { + return gitRev; + } + + public String getFileName() { + return fileName; + } + + public String getObjectId() { + return objectId; + } + + public void setObjectId(String objectId) { + this.objectId = objectId; + } + + public String getFlowName() { + return flowName; + } + + public void setFlowName(String flowName) { + this.flowName = flowName; + } + + public String getFlowDescription() { + return flowDescription; + } + + public void setFlowDescription(String flowDescription) { + this.flowDescription = flowDescription; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } + + public Long getCreated() { + return created; + } + + public void setCreated(Long created) { + this.created = created; + } + } + + /** + * Serialize the latest version of this Flow meta data. + * @return serialized flow + */ + Map serialize() { + final Map map = new HashMap<>(); + final Optional latestVerOpt = getLatestVersion(); + if (!latestVerOpt.isPresent()) { + throw new IllegalStateException("Flow version is not added yet, can not be serialized."); + } + + final Integer latestVer = latestVerOpt.get(); + final Flow.FlowPointer latestFlowPointer = versions.get(latestVer); + + map.put(GitFlowMetaData.VER, latestVer); + map.put(GitFlowMetaData.FILE, latestFlowPointer.fileName); + + if (latestFlowPointer.flowName != null) { + map.put(GitFlowMetaData.FLOW_NAME, latestFlowPointer.flowName); + } + if (latestFlowPointer.flowDescription != null) { + map.put(GitFlowMetaData.FLOW_DESC, latestFlowPointer.flowDescription); + } + if (latestFlowPointer.author != null) { + map.put(GitFlowMetaData.AUTHOR, latestFlowPointer.author); + } + if (latestFlowPointer.comment != null) { + map.put(GitFlowMetaData.COMMENTS, latestFlowPointer.comment); + } + if (latestFlowPointer.created != null) { + map.put(GitFlowMetaData.CREATED, latestFlowPointer.created); + } + + return map; + } + + Optional getLatestVersion() { + return versions.keySet().stream().reduce(Integer::max); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/GitFlowMetaData.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/GitFlowMetaData.java new file mode 100644 index 0000000000..8ed146dfd2 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/GitFlowMetaData.java @@ -0,0 +1,528 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider.flow.git; + +import org.apache.commons.lang3.concurrent.BasicThreadFactory; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.LsRemoteCommand; +import org.eclipse.jgit.api.PushCommand; +import org.eclipse.jgit.api.Status; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.NoHeadException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.RepositoryCache; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevTree; +import org.eclipse.jgit.storage.file.FileRepositoryBuilder; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.eclipse.jgit.transport.PushResult; +import org.eclipse.jgit.transport.RemoteConfig; +import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.util.FS; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.yaml.snakeyaml.Yaml; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static java.lang.String.format; +import static org.apache.commons.lang3.StringUtils.isEmpty; + +class GitFlowMetaData { + + static final int CURRENT_LAYOUT_VERSION = 1; + + static final String LAYOUT_VERSION = "layoutVer"; + static final String BUCKET_ID = "bucketId"; + static final String FLOWS = "flows"; + static final String VER = "ver"; + static final String FILE = "file"; + static final String FLOW_NAME = "flowName"; + static final String FLOW_DESC = "flowDesc"; + static final String AUTHOR = "author"; + static final String COMMENTS = "comments"; + static final String CREATED = "created"; + static final String BUCKET_FILENAME = "bucket.yml"; + + private static final Logger logger = LoggerFactory.getLogger(GitFlowMetaData.class); + + private Repository gitRepo; + private String remoteToPush; + private CredentialsProvider credentialsProvider; + + private final BlockingQueue pushQueue = new ArrayBlockingQueue<>(1); + + /** + * Bucket ID to Bucket. + */ + private Map buckets = new HashMap<>(); + + public void setRemoteToPush(String remoteToPush) { + this.remoteToPush = remoteToPush; + } + + public void setRemoteCredential(String userName, String password) { + this.credentialsProvider = new UsernamePasswordCredentialsProvider(userName, password); + } + + /** + * Open a Git repository using the specified directory. + * @param gitProjectRootDir a root directory of a Git project + * @return created Repository + * @throws IOException thrown when the specified directory does not exist, + * does not have read/write privilege or not containing .git directory + */ + private Repository openRepository(final File gitProjectRootDir) throws IOException { + + // Instead of using FileUtils.ensureDirectoryExistAndCanReadAndWrite, check availability manually here. + // Because the util will try to create a dir if not exist. + // The git dir should be initialized and configured by users. + if (!gitProjectRootDir.isDirectory()) { + throw new IOException(format("'%s' is not a directory or does not exist.", gitProjectRootDir)); + } + + if (!(gitProjectRootDir.canRead() && gitProjectRootDir.canWrite())) { + throw new IOException(format("Directory '%s' does not have read/write privilege.", gitProjectRootDir)); + } + + // Search .git dir but avoid searching parent directories. + final FileRepositoryBuilder builder = new FileRepositoryBuilder() + .readEnvironment() + .setMustExist(true) + .addCeilingDirectory(gitProjectRootDir) + .findGitDir(gitProjectRootDir); + + if (builder.getGitDir() == null) { + throw new IOException(format("Directory '%s' does not contain a .git directory." + + " Please init and configure the directory with 'git init' command before using it from NiFi Registry.", + gitProjectRootDir)); + } + + return builder.build(); + } + + private static boolean hasAtLeastOneReference(Repository repo) { + logger.info("Checking references for repository {}", repo.toString()); + for (Ref ref : repo.getAllRefs().values()) { + if (ref.getObjectId() == null) { + continue; + } + return true; + } + return false; + } + + /** + * Check if the provided local repository exists or not, provided by the 'Flow Storage Directory' + * configuration in the providers.xml. + * @param localRepo {@link File} object of the 'Flow Storage Directory' configuration + * @return true if the local repository exists, false otherwise + * @throws IOException if the .git directory of the local repository cannot be opened + */ + public boolean localRepoExists(File localRepo) throws IOException { + if (!localRepo.isDirectory()) { + logger.info("{} is not a directory or does not exist.", localRepo.getPath()); + return false; + } + + if (RepositoryCache.FileKey.isGitRepository(new File(localRepo.getPath()+"/.git"), FS.DETECTED)) { + final Git git = Git.open(new File(localRepo.getPath() + "/.git")); + final Repository repository = git.getRepository(); + logger.info("Checking for git references in {}", localRepo.getPath()); + final boolean referenceExists = hasAtLeastOneReference(repository); + if (referenceExists) { + logger.info("{} local repository exists with references so no need to clone remote", localRepo.getPath()); + } + // Can be an empty repository if no references are present should we pull from remote? + return true; + } + return false; + } + + /** + * Validate if the provided 'Remote Clone Repository' configuration in the providers.xml exists or not. + * If the remote repository does not exist, an {@link IllegalArgumentException} will be thrown. + * @param remoteRepository the URI value of the 'Remote Clone Repository' configuration + * @throws IOException if creating the repository fails + */ + public void remoteRepoExists(String remoteRepository) throws IOException { + final Git git = new Git(FileRepositoryBuilder.create(new File(remoteRepository))); + final LsRemoteCommand lsCmd = git.lsRemote(); + try { + lsCmd.setRemote(remoteRepository); + lsCmd.setCredentialsProvider(this.credentialsProvider); + lsCmd.call(); + } catch (Exception e){ + throw new IllegalArgumentException("InvalidRemoteRepository : Given remote repository is not valid"); + } + } + + /** + * If validation of remote clone repository throws no exception then clone the repository given + * in the 'Remote Clone Repository' configuration. Currently the default branch of remote will be cloned. + * @param localRepo {@link File} object of the 'Flow Storage Directory' configuration + * @param remoteRepository the URI value of the 'Remote Clone Repository' configuration + * @throws GitAPIException if unable to call the remote repository + */ + public void cloneRepository(File localRepo, String remoteRepository) throws GitAPIException { + logger.info("Cloning the repository {} in {}", remoteRepository, localRepo.getPath()); + Git.cloneRepository() + .setURI(remoteRepository) + .setCredentialsProvider(this.credentialsProvider) + .setDirectory(localRepo) + .call(); + } + + @SuppressWarnings("unchecked") + public void loadGitRepository(File gitProjectRootDir) throws IOException, GitAPIException { + gitRepo = openRepository(gitProjectRootDir); + + try (final Git git = new Git(gitRepo)) { + + // Check if remote exists. + if (!isEmpty(remoteToPush)) { + final List remotes = git.remoteList().call(); + final boolean isRemoteExist = remotes.stream().anyMatch(remote -> remote.getName().equals(remoteToPush)); + if (!isRemoteExist) { + final List remoteNames = remotes.stream().map(RemoteConfig::getName).collect(Collectors.toList()); + throw new IllegalArgumentException( + format("The configured remote '%s' to push does not exist. Available remotes are %s", remoteToPush, remoteNames)); + } + } + + boolean isLatestCommit = true; + try { + for (RevCommit commit : git.log().call()) { + final String shortCommitId = commit.getId().abbreviate(7).name(); + logger.debug("Processing a commit: {}", shortCommitId); + final RevTree tree = commit.getTree(); + + try (final TreeWalk treeWalk = new TreeWalk(gitRepo)) { + treeWalk.addTree(tree); + + // Path -> ObjectId + final Map bucketObjectIds = new HashMap<>(); + final Map flowSnapshotObjectIds = new HashMap<>(); + while (treeWalk.next()) { + if (treeWalk.isSubtree()) { + treeWalk.enterSubtree(); + } else { + final String pathString = treeWalk.getPathString(); + // TODO: what is this nth?? When does it get grater than 0? Tree count seems to be always 1.. + if (pathString.endsWith("/" + BUCKET_FILENAME)) { + bucketObjectIds.put(pathString, treeWalk.getObjectId(0)); + } else if (pathString.endsWith(GitFlowPersistenceProvider.SNAPSHOT_EXTENSION)) { + flowSnapshotObjectIds.put(pathString, treeWalk.getObjectId(0)); + } + } + } + + if (bucketObjectIds.isEmpty()) { + // No bucket.yml means at this point, all flows are deleted. No need to scan older commits because those are already deleted. + logger.debug("Tree at commit {} does not contain any " + BUCKET_FILENAME + ". Stop loading commits here.", shortCommitId); + return; + } + + loadBuckets(gitRepo, commit, isLatestCommit, bucketObjectIds, flowSnapshotObjectIds); + isLatestCommit = false; + } + } + } catch (NoHeadException e) { + logger.debug("'{}' does not have any commit yet. Starting with empty buckets.", gitProjectRootDir); + } + + } + } + + void startPushThread() { + // If successfully loaded, start pushing thread if necessary. + if (isEmpty(remoteToPush)) { + return; + } + + final ThreadFactory threadFactory = new BasicThreadFactory.Builder() + .daemon(true).namingPattern(getClass().getSimpleName() + " Push thread").build(); + + // Use scheduled fixed delay to control the minimum interval between push activities. + // The necessity of executing push is controlled by offering messages to the pushQueue. + // If multiple commits are made within this time window, those are pushed by a single push execution. + final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(threadFactory); + executorService.scheduleWithFixedDelay(() -> { + + final Long offeredTimestamp; + try { + offeredTimestamp = pushQueue.take(); + } catch (InterruptedException e) { + logger.warn("Waiting for push request has been interrupted due to {}", e.getMessage(), e); + return; + } + + logger.debug("Took a push request sent at {} to {}...", offeredTimestamp, remoteToPush); + final PushCommand pushCommand = new Git(gitRepo).push().setRemote(remoteToPush); + if (credentialsProvider != null) { + pushCommand.setCredentialsProvider(credentialsProvider); + } + + try { + final Iterable pushResults = pushCommand.call(); + for (PushResult pushResult : pushResults) { + logger.debug(pushResult.getMessages()); + } + } catch (GitAPIException e) { + logger.error(format("Failed to push commits to %s due to %s", remoteToPush, e), e); + } + + }, 10, 10, TimeUnit.SECONDS); + } + + @SuppressWarnings("unchecked") + private void loadBuckets(Repository gitRepo, RevCommit commit, boolean isLatestCommit, Map bucketObjectIds, Map flowSnapshotObjectIds) throws IOException { + final Yaml yaml = new Yaml(); + for (String bucketFilePath : bucketObjectIds.keySet()) { + final ObjectId bucketObjectId = bucketObjectIds.get(bucketFilePath); + final Map bucketMeta; + try (InputStream bucketIn = gitRepo.newObjectReader().open(bucketObjectId).openStream()) { + bucketMeta = yaml.load(bucketIn); + } + + if (!validateRequiredValue(bucketMeta, bucketFilePath, LAYOUT_VERSION, BUCKET_ID, FLOWS)) { + continue; + } + + int layoutVersion = (int) bucketMeta.get(LAYOUT_VERSION); + if (layoutVersion > CURRENT_LAYOUT_VERSION) { + logger.warn("{} has unsupported {} {}. This Registry can only support {} or lower. Skipping it.", + bucketFilePath, LAYOUT_VERSION, layoutVersion, CURRENT_LAYOUT_VERSION); + continue; + } + + final String bucketId = (String) bucketMeta.get(BUCKET_ID); + + final Bucket bucket; + if (isLatestCommit) { + // If this is the latest commit, then create one. + bucket = getBucketOrCreate(bucketId); + } else { + // Otherwise non-existing bucket means it's already deleted. + final Optional bucketOpt = getBucket(bucketId); + if (bucketOpt.isPresent()) { + bucket = bucketOpt.get(); + } else { + logger.debug("Bucket {} does not exist any longer. It may have been deleted.", bucketId); + continue; + } + } + + // Since the bucketName is restored from pathname, it can be different from the original bucket name when it sanitized. + final String bucketDirName = bucketFilePath.substring(0, bucketFilePath.lastIndexOf("/")); + + // Since commits are read in LIFO order, avoid old commits overriding the latest bucket name. + if (isEmpty(bucket.getBucketDirName())) { + bucket.setBucketDirName(bucketDirName); + } + + final Map flows = (Map) bucketMeta.get(FLOWS); + loadFlows(commit, isLatestCommit, bucket, bucketFilePath, flows, flowSnapshotObjectIds); + } + } + + @SuppressWarnings("unchecked") + private void loadFlows(RevCommit commit, boolean isLatestCommit, Bucket bucket, String backetFilePath, Map flows, Map flowSnapshotObjectIds) { + for (String flowId : flows.keySet()) { + final Map flowMeta = (Map) flows.get(flowId); + + if (!validateRequiredValue(flowMeta, backetFilePath + ":" + flowId, VER, FILE)) { + continue; + } + + final Flow flow; + if (isLatestCommit) { + // If this is the latest commit, then create one. + flow = bucket.getFlowOrCreate(flowId); + } else { + // Otherwise non-existing flow means it's already deleted. + final Optional flowOpt = bucket.getFlow(flowId); + if (flowOpt.isPresent()) { + flow = flowOpt.get(); + } else { + logger.debug("Flow {} does not exist in bucket {}:{} any longer. It may have been deleted.", flowId, bucket.getBucketDirName(), bucket.getBucketId()); + continue; + } + } + + final int version = (int) flowMeta.get(VER); + final String flowSnapshotFilename = (String) flowMeta.get(FILE); + + // Since commits are read in LIFO order, avoid old commits overriding the latest pointer. + if (!flow.hasVersion(version)) { + final Flow.FlowPointer pointer = new Flow.FlowPointer(flowSnapshotFilename); + final File flowSnapshotFile = new File(new File(backetFilePath).getParent(), flowSnapshotFilename); + final ObjectId objectId = flowSnapshotObjectIds.get(flowSnapshotFile.getPath()); + if (objectId == null) { + logger.warn("Git object id for Flow {} version {} with path {} in bucket {}:{} was not found. Ignoring this entry.", + flowId, version, flowSnapshotFile.getPath(), bucket.getBucketDirName(), bucket.getBucketId()); + continue; + } + pointer.setGitRev(commit.getName()); + pointer.setObjectId(objectId.getName()); + + if (flowMeta.containsKey(FLOW_NAME)) { + pointer.setFlowName((String)flowMeta.get(FLOW_NAME)); + } + if (flowMeta.containsKey(FLOW_DESC)) { + pointer.setFlowDescription((String)flowMeta.get(FLOW_DESC)); + } + if (flowMeta.containsKey(AUTHOR)) { + pointer.setAuthor((String)flowMeta.get(AUTHOR)); + } + if (flowMeta.containsKey(COMMENTS)) { + pointer.setComment((String)flowMeta.get(COMMENTS)); + } + if (flowMeta.containsKey(CREATED)) { + pointer.setCreated((long)flowMeta.get(CREATED)); + } + + flow.putVersion(version, pointer); + } + } + } + + private boolean validateRequiredValue(final Map map, String nameOfMap, Object ... keys) { + for (Object key : keys) { + if (!map.containsKey(key)) { + logger.warn("{} does not have {}. Skipping it.", nameOfMap, key); + return false; + } + } + return true; + } + + public Bucket getBucketOrCreate(String bucketId) { + return buckets.computeIfAbsent(bucketId, k -> new Bucket(bucketId)); + } + + public Optional getBucket(String bucketId) { + return Optional.ofNullable(buckets.get(bucketId)); + } + + Map getBuckets() { + return buckets; + } + + void saveBucket(final Bucket bucket, final File bucketDir) throws IOException { + final Yaml yaml = new Yaml(); + final Map serializedBucket = bucket.serialize(); + final File bucketFile = new File(bucketDir, GitFlowMetaData.BUCKET_FILENAME); + + try (final Writer writer = new OutputStreamWriter( + new FileOutputStream(bucketFile), StandardCharsets.UTF_8)) { + yaml.dump(serializedBucket, writer); + } + } + + boolean isGitDirectoryClean() throws GitAPIException { + final Status status = new Git(gitRepo).status().call(); + return status.isClean() && !status.hasUncommittedChanges(); + } + + /** + * Create a Git commit. + * @param author The name of a NiFi Registry user who created the snapshot. It will be added to the commit message. + * @param message Commit message. + * @param bucket A bucket to commit. + * @param flowPointer A flow pointer for the flow snapshot which is updated. + * After a commit is created, new commit rev id and flow snapshot file object id are set to this pointer. + * It can be null if none of flow content is modified. + */ + void commit(String author, String message, Bucket bucket, Flow.FlowPointer flowPointer) throws GitAPIException, IOException { + try (final Git git = new Git(gitRepo)) { + // Execute add command for newly added files (if any). + git.add().addFilepattern(".").call(); + + // Execute add command again for deleted files (if any). + git.add().addFilepattern(".").setUpdate(true).call(); + + final String commitMessage = isEmpty(author) ? message + : format("%s\n\nBy NiFi Registry user: %s", message, author); + final RevCommit commit = git.commit() + .setMessage(commitMessage) + .setSign(false) + .call(); + + if (flowPointer != null) { + final RevTree tree = commit.getTree(); + final String bucketDirName = bucket.getBucketDirName(); + final String flowSnapshotPath = new File(bucketDirName, flowPointer.getFileName()).getPath(); + try (final TreeWalk treeWalk = new TreeWalk(gitRepo)) { + treeWalk.addTree(tree); + + while (treeWalk.next()) { + if (treeWalk.isSubtree()) { + treeWalk.enterSubtree(); + } else { + final String pathString = treeWalk.getPathString(); + if (pathString.equals(flowSnapshotPath)) { + // Capture updated object id. + final String flowSnapshotObjectId = treeWalk.getObjectId(0).getName(); + flowPointer.setObjectId(flowSnapshotObjectId); + break; + } + } + } + } + + flowPointer.setGitRev(commit.getName()); + } + + // Push if necessary. + if (!isEmpty(remoteToPush)) { + // Use different thread since it takes longer. + final long offeredTimestamp = System.currentTimeMillis(); + if (pushQueue.offer(offeredTimestamp)) { + logger.debug("New push request is offered at {}.", offeredTimestamp); + } + } + + } + } + + byte[] getContent(String objectId) throws IOException { + final ObjectId flowSnapshotObjectId = gitRepo.resolve(objectId); + return gitRepo.newObjectReader().open(flowSnapshotObjectId).getBytes(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/GitFlowPersistenceProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/GitFlowPersistenceProvider.java new file mode 100644 index 0000000000..d511f877b5 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/GitFlowPersistenceProvider.java @@ -0,0 +1,363 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider.flow.git; + +import org.apache.nifi.registry.flow.FlowPersistenceException; +import org.apache.nifi.registry.flow.FlowSnapshotContext; +import org.apache.nifi.registry.flow.MetadataAwareFlowPersistenceProvider; +import org.apache.nifi.registry.metadata.BucketMetadata; +import org.apache.nifi.registry.metadata.FlowMetadata; +import org.apache.nifi.registry.metadata.FlowSnapshotMetadata; +import org.apache.nifi.registry.provider.ProviderConfigurationContext; +import org.apache.nifi.registry.provider.ProviderCreationException; +import org.apache.nifi.registry.util.FileUtils; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static java.lang.String.format; +import static org.apache.commons.lang3.StringUtils.isEmpty; +import static org.apache.nifi.registry.util.FileUtils.sanitizeFilename; + +public class GitFlowPersistenceProvider implements MetadataAwareFlowPersistenceProvider { + + private static final Logger logger = LoggerFactory.getLogger(GitFlowMetaData.class); + static final String FLOW_STORAGE_DIR_PROP = "Flow Storage Directory"; + private static final String REMOTE_TO_PUSH = "Remote To Push"; + private static final String REMOTE_ACCESS_USER = "Remote Access User"; + private static final String REMOTE_ACCESS_PASSWORD = "Remote Access Password"; + private static final String REMOTE_CLONE_REPOSITORY = "Remote Clone Repository"; + static final String SNAPSHOT_EXTENSION = ".snapshot"; + + private File flowStorageDir; + private GitFlowMetaData flowMetaData; + + @Override + public void onConfigured(ProviderConfigurationContext configurationContext) throws ProviderCreationException { + flowMetaData = new GitFlowMetaData(); + + final Map props = configurationContext.getProperties(); + if (!props.containsKey(FLOW_STORAGE_DIR_PROP)) { + throw new ProviderCreationException("The property " + FLOW_STORAGE_DIR_PROP + " must be provided"); + } + + final String flowStorageDirValue = props.get(FLOW_STORAGE_DIR_PROP); + if (isEmpty(flowStorageDirValue)) { + throw new ProviderCreationException("The property " + FLOW_STORAGE_DIR_PROP + " cannot be null or blank"); + } + + flowMetaData.setRemoteToPush(props.get(REMOTE_TO_PUSH)); + + final String remoteUser = props.get(REMOTE_ACCESS_USER); + final String remotePassword = props.get(REMOTE_ACCESS_PASSWORD); + final String remoteRepo = props.get(REMOTE_CLONE_REPOSITORY); + if (!isEmpty(remoteRepo)) { + if (isEmpty(remoteUser) || isEmpty(remotePassword)) { + throw new ProviderCreationException(format("The property %s needs remote username and remote password", + REMOTE_CLONE_REPOSITORY)); + } + } + if (!isEmpty(remoteUser) && isEmpty(remotePassword)) { + throw new ProviderCreationException(format("The property %s is specified but %s is not." + + " %s is required for username password authentication.", + REMOTE_ACCESS_USER, REMOTE_ACCESS_PASSWORD, REMOTE_ACCESS_PASSWORD)); + } + if (!isEmpty(remotePassword)) { + flowMetaData.setRemoteCredential(remoteUser, remotePassword); + } + + try { + flowStorageDir = new File(flowStorageDirValue); + final boolean localRepoExists = flowMetaData.localRepoExists(flowStorageDir); + if (remoteRepo != null && !remoteRepo.isEmpty() && !localRepoExists){ + flowMetaData.remoteRepoExists(remoteRepo); + flowMetaData.cloneRepository(flowStorageDir, remoteRepo); + } + flowMetaData.loadGitRepository(flowStorageDir); + flowMetaData.startPushThread(); + logger.info("Configured GitFlowPersistenceProvider with Flow Storage Directory {}", + new Object[] {flowStorageDir.getAbsolutePath()}); + } catch (IOException|GitAPIException e) { + throw new ProviderCreationException("Failed to load a git repository " + flowStorageDir, e); + } + } + + @Override + public void saveFlowContent(FlowSnapshotContext context, byte[] content) throws FlowPersistenceException { + + try { + // Check if working dir is clean, any uncommitted file? + if (!flowMetaData.isGitDirectoryClean()) { + throw new FlowPersistenceException(format("Git directory %s is not clean" + + " or has uncommitted changes, resolve those changes first to save flow contents.", + flowStorageDir)); + } + } catch (GitAPIException e) { + throw new FlowPersistenceException(format("Failed to get Git status for directory %s due to %s", + flowStorageDir, e)); + } + + final String bucketId = context.getBucketId(); + final Bucket bucket = flowMetaData.getBucketOrCreate(bucketId); + final String currentBucketDirName = bucket.getBucketDirName(); + final String bucketDirName = sanitizeFilename(context.getBucketName()); + final boolean isBucketNameChanged = !bucketDirName.equals(currentBucketDirName); + bucket.setBucketDirName(bucketDirName); + + final Flow flow = bucket.getFlowOrCreate(context.getFlowId()); + final String flowSnapshotFilename = sanitizeFilename(context.getFlowName()) + SNAPSHOT_EXTENSION; + + final Optional currentFlowSnapshotFilename = flow + .getLatestVersion().map(flow::getFlowVersion).map(Flow.FlowPointer::getFileName); + + // Add new version. + final Flow.FlowPointer flowPointer = new Flow.FlowPointer(flowSnapshotFilename); + flowPointer.setFlowName(context.getFlowName()); + flowPointer.setFlowDescription(context.getFlowDescription()); + flowPointer.setAuthor(context.getAuthor()); + flowPointer.setComment(context.getComments()); + flowPointer.setCreated(context.getSnapshotTimestamp()); + + flow.putVersion(context.getVersion(), flowPointer); + + final File bucketDir = new File(flowStorageDir, bucketDirName); + final File flowSnippetFile = new File(bucketDir, flowSnapshotFilename); + + final File currentBucketDir = isEmpty(currentBucketDirName) ? null : new File(flowStorageDir, currentBucketDirName); + if (currentBucketDir != null && currentBucketDir.isDirectory()) { + if (isBucketNameChanged) { + logger.debug("Detected bucket name change from {} to {}, moving it.", currentBucketDirName, bucketDirName); + if (!currentBucketDir.renameTo(bucketDir)) { + throw new FlowPersistenceException(format("Failed to move existing bucket %s to %s.", currentBucketDir, bucketDir)); + } + } + } else { + if (!bucketDir.mkdirs()) { + throw new FlowPersistenceException(format("Failed to create new bucket dir %s.", bucketDir)); + } + } + + + try { + if (currentFlowSnapshotFilename.isPresent() && !flowSnapshotFilename.equals(currentFlowSnapshotFilename.get())) { + // Delete old file if flow name has been changed. + final File latestFlowSnapshotFile = new File(bucketDir, currentFlowSnapshotFilename.get()); + logger.debug("Detected flow name change from {} to {}, deleting the old snapshot file.", + currentFlowSnapshotFilename.get(), flowSnapshotFilename); + latestFlowSnapshotFile.delete(); + } + + // Save the content. + try (final OutputStream os = new FileOutputStream(flowSnippetFile)) { + os.write(content); + os.flush(); + } + + // Write a bucket file. + flowMetaData.saveBucket(bucket, bucketDir); + + // Create a Git Commit. + flowMetaData.commit(context.getAuthor(), context.getComments(), bucket, flowPointer); + + } catch (IOException|GitAPIException e) { + throw new FlowPersistenceException("Failed to persist flow.", e); + } + + // TODO: What if user rebased commits? Version number to Commit ID mapping will be broken. + } + + @Override + public byte[] getFlowContent(String bucketId, String flowId, int version) throws FlowPersistenceException { + + final Bucket bucket = getBucketOrFail(bucketId); + final Flow flow = getFlowOrFail(bucket, flowId); + if (!flow.hasVersion(version)) { + throw new FlowPersistenceException(format("Flow ID %s version %d was not found in bucket %s:%s.", + flowId, version, bucket.getBucketDirName(), bucketId)); + } + + final Flow.FlowPointer flowPointer = flow.getFlowVersion(version); + try { + return flowMetaData.getContent(flowPointer.getObjectId()); + } catch (IOException e) { + throw new FlowPersistenceException(format("Failed to get content of Flow ID %s version %d in bucket %s:%s due to %s.", + flowId, version, bucket.getBucketDirName(), bucketId, e), e); + } + } + + // TODO: Need to add userId argument? + @Override + public void deleteAllFlowContent(String bucketId, String flowId) throws FlowPersistenceException { + final Bucket bucket = getBucketOrFail(bucketId); + final Optional flowOpt = bucket.getFlow(flowId); + if (!flowOpt.isPresent()) { + logger.debug(format("Tried deleting all versions, but the Flow ID %s was not found in bucket %s:%s.", + flowId, bucket.getBucketDirName(), bucket.getBucketId())); + return; + } + + final Flow flow = flowOpt.get(); + final Optional latestVersionOpt = flow.getLatestVersion(); + if (!latestVersionOpt.isPresent()) { + throw new IllegalStateException("Flow version is not added yet, can not be deleted."); + } + + final Integer latestVersion = latestVersionOpt.get(); + final Flow.FlowPointer flowPointer = flow.getFlowVersion(latestVersion); + + // Delete the flow snapshot. + final File bucketDir = new File(flowStorageDir, bucket.getBucketDirName()); + final File flowSnapshotFile = new File(bucketDir, flowPointer.getFileName()); + if (flowSnapshotFile.exists()) { + if (!flowSnapshotFile.delete()) { + throw new FlowPersistenceException(format("Failed to delete flow content for %s:%s in bucket %s:%s", + flowPointer.getFileName(), flowId, bucket.getBucketDirName(), bucketId)); + } + } + + bucket.removeFlow(flowId); + + try { + + if (bucket.isEmpty()) { + // delete bucket dir if this is the last flow. + FileUtils.deleteFile(bucketDir, true); + } else { + // Write a bucket file. + flowMetaData.saveBucket(bucket, bucketDir); + } + + // Create a Git Commit. + final String commitMessage = format("Deleted flow %s:%s in bucket %s:%s.", + flowPointer.getFileName(), flowId, bucket.getBucketDirName(), bucketId); + flowMetaData.commit(null, commitMessage, bucket, null); + + } catch (IOException|GitAPIException e) { + throw new FlowPersistenceException(format("Failed to delete flow %s:%s in bucket %s:%s due to %s", + flowPointer.getFileName(), flowId, bucket.getBucketDirName(), bucketId, e), e); + } + + } + + private Bucket getBucketOrFail(String bucketId) throws FlowPersistenceException { + final Optional bucketOpt = flowMetaData.getBucket(bucketId); + if (!bucketOpt.isPresent()) { + throw new FlowPersistenceException(format("Bucket ID %s was not found.", bucketId)); + } + + return bucketOpt.get(); + } + + private Flow getFlowOrFail(Bucket bucket, String flowId) throws FlowPersistenceException { + final Optional flowOpt = bucket.getFlow(flowId); + if (!flowOpt.isPresent()) { + throw new FlowPersistenceException(format("Flow ID %s was not found in bucket %s:%s.", + flowId, bucket.getBucketDirName(), bucket.getBucketId())); + } + + return flowOpt.get(); + } + + @Override + public void deleteFlowContent(String bucketId, String flowId, int version) throws FlowPersistenceException { + // TODO: Do nothing? This signature is not used. Actually there's nothing to do to the old versions as those exist in old commits even if this method is called. + } + + @Override + public List getMetadata() { + final Map gitBuckets = flowMetaData.getBuckets(); + if (gitBuckets == null || gitBuckets.isEmpty()) { + return Collections.emptyList(); + } + + final List bucketMetadataList = new ArrayList<>(); + for (Map.Entry bucketEntry : gitBuckets.entrySet()) { + final String bucketId = bucketEntry.getKey(); + final Bucket gitBucket = bucketEntry.getValue(); + + final BucketMetadata bucketMetadata = new BucketMetadata(); + bucketMetadata.setIdentifier(bucketId); + bucketMetadata.setName(gitBucket.getBucketDirName()); + bucketMetadata.setFlowMetadata(createFlowMetadata(gitBucket)); + bucketMetadataList.add(bucketMetadata); + } + return bucketMetadataList; + } + + private List createFlowMetadata(final Bucket bucket) { + if (bucket.isEmpty()) { + return Collections.emptyList(); + } + + final List flowMetadataList = new ArrayList<>(); + for (Map.Entry flowEntry : bucket.getFlows().entrySet()) { + final String flowId = flowEntry.getKey(); + final Flow flow = flowEntry.getValue(); + + final Optional latestVersion = flow.getLatestVersion(); + if (latestVersion.isPresent()) { + final Flow.FlowPointer latestFlowPointer = flow.getFlowVersion(latestVersion.get()); + + String flowName = latestFlowPointer.getFlowName(); + if (flowName == null) { + flowName = latestFlowPointer.getFileName(); + if (flowName.endsWith(".snapshot")) { + flowName = flowName.substring(0, flowName.lastIndexOf(".")); + } + } + + + final FlowMetadata flowMetadata = new FlowMetadata(); + flowMetadata.setIdentifier(flowId); + flowMetadata.setName(flowName); + flowMetadata.setDescription(latestFlowPointer.getFlowDescription()); + flowMetadata.setFlowSnapshotMetadata(createFlowSnapshotMetdata(flow)); + flowMetadataList.add(flowMetadata); + } + } + return flowMetadataList; + } + + private List createFlowSnapshotMetdata(final Flow flow) { + final List flowSnapshotMetadataList = new ArrayList<>(); + + final Map versions = flow.getVersions(); + for (Map.Entry entry : versions.entrySet()) { + final Integer version = entry.getKey(); + final Flow.FlowPointer flowPointer = entry.getValue(); + + final FlowSnapshotMetadata snapshotMetadata = new FlowSnapshotMetadata(); + snapshotMetadata.setVersion(version); + snapshotMetadata.setAuthor(flowPointer.getAuthor()); + snapshotMetadata.setComments(flowPointer.getComment()); + snapshotMetadata.setCreated(flowPointer.getCreated()); + flowSnapshotMetadataList.add(snapshotMetadata); + } + + return flowSnapshotMetadataList; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/hook/LoggingEventHookProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/hook/LoggingEventHookProvider.java new file mode 100644 index 0000000000..9ceb59f6a4 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/hook/LoggingEventHookProvider.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider.hook; + +import org.apache.nifi.registry.hook.Event; +import org.apache.nifi.registry.hook.EventField; +import org.apache.nifi.registry.hook.EventHookException; +import org.apache.nifi.registry.hook.EventHookProvider; +import org.apache.nifi.registry.provider.ProviderConfigurationContext; +import org.apache.nifi.registry.provider.ProviderCreationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LoggingEventHookProvider + implements EventHookProvider { + + static final Logger LOGGER = LoggerFactory.getLogger(LoggingEventHookProvider.class); + + @Override + public void onConfigured(ProviderConfigurationContext configurationContext) throws ProviderCreationException { + // Nothing to do + } + + @Override + public void handle(final Event event) throws EventHookException { + + final StringBuilder builder = new StringBuilder() + .append(event.getEventType()) + .append(" ["); + + int count = 0; + for (final EventField argument : event.getFields()) { + if (count > 0) { + builder.append(", "); + } + builder.append(argument.getName()).append("=").append(argument.getValue()); + count++; + } + + builder.append("] "); + + LOGGER.info(builder.toString()); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/hook/ScriptEventHookProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/hook/ScriptEventHookProvider.java new file mode 100644 index 0000000000..f96115e795 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/hook/ScriptEventHookProvider.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider.hook; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.hook.Event; +import org.apache.nifi.registry.hook.EventField; +import org.apache.nifi.registry.hook.WhitelistFilteringEventHookProvider; +import org.apache.nifi.registry.provider.ProviderConfigurationContext; +import org.apache.nifi.registry.provider.ProviderCreationException; +import org.apache.nifi.registry.util.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A EventHookProvider that is used to execute a script to handle the event. + */ +public class ScriptEventHookProvider + extends WhitelistFilteringEventHookProvider { + + static final Logger LOGGER = LoggerFactory.getLogger(ScriptEventHookProvider.class); + static final String SCRIPT_PATH_PROP = "Script Path"; + static final String SCRIPT_WORKDIR_PROP = "Working Directory"; + private File scriptFile; + private File workDirFile; + + + @Override + public void handle(final Event event) { + List command = new ArrayList<>(); + command.add(scriptFile.getAbsolutePath()); + command.add(event.getEventType().name()); + + for (EventField arg : event.getFields()) { + command.add(arg.getValue()); + } + + final String commandString = StringUtils.join(command, " "); + final ProcessBuilder builder = new ProcessBuilder(command); + builder.directory(workDirFile); + LOGGER.debug("Execution of " + commandString); + + try { + builder.start(); + } catch (IOException e) { + LOGGER.error("Execution of {0} failed with: {1}", new Object[] { commandString, e.getLocalizedMessage() }, e); + } + } + + @Override + public void onConfigured(ProviderConfigurationContext configurationContext) throws ProviderCreationException { + super.onConfigured(configurationContext); + + final Map props = configurationContext.getProperties(); + if (!props.containsKey(SCRIPT_PATH_PROP)) { + throw new ProviderCreationException("The property " + SCRIPT_PATH_PROP + " must be provided"); + } + + final String scripPath = props.get(SCRIPT_PATH_PROP); + if (StringUtils.isBlank(scripPath)) { + throw new ProviderCreationException("The property " + SCRIPT_PATH_PROP + " cannot be null or blank"); + } + + if(props.containsKey(SCRIPT_WORKDIR_PROP) && !StringUtils.isBlank(props.get(SCRIPT_WORKDIR_PROP))) { + final String workdir = props.get(SCRIPT_WORKDIR_PROP); + try { + workDirFile = new File(workdir); + FileUtils.ensureDirectoryExistAndCanRead(workDirFile); + } catch (IOException e) { + throw new ProviderCreationException("The working directory " + workdir + " cannot be read."); + } + } + + scriptFile = new File(scripPath); + if(scriptFile.isFile() && scriptFile.canExecute()) { + LOGGER.info("Configured ScriptEventHookProvider with script {}", new Object[] {scriptFile.getAbsolutePath()}); + } else { + throw new ProviderCreationException("The script file " + scriptFile.getAbsolutePath() + " cannot be executed."); + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderFactory.java new file mode 100644 index 0000000000..3c2a3f4b02 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderFactory.java @@ -0,0 +1,291 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authentication; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.extension.ExtensionManager; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.apache.nifi.registry.properties.SensitivePropertyProtectionException; +import org.apache.nifi.registry.properties.SensitivePropertyProvider; +import org.apache.nifi.registry.security.authentication.annotation.IdentityProviderContext; +import org.apache.nifi.registry.security.authentication.generated.IdentityProviders; +import org.apache.nifi.registry.security.authentication.generated.Property; +import org.apache.nifi.registry.security.authentication.generated.Provider; +import org.apache.nifi.registry.security.util.XmlUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.lang.Nullable; +import org.xml.sax.SAXException; + +import javax.xml.XMLConstants; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBElement; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; +import javax.xml.stream.XMLStreamReader; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import java.io.File; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +@Configuration +public class IdentityProviderFactory implements IdentityProviderLookup, DisposableBean { + + private static final Logger logger = LoggerFactory.getLogger(IdentityProviderFactory.class); + private static final String LOGIN_IDENTITY_PROVIDERS_XSD = "/identity-providers.xsd"; + private static final String JAXB_GENERATED_PATH = "org.apache.nifi.registry.security.authentication.generated"; + private static final JAXBContext JAXB_CONTEXT = initializeJaxbContext(); + + private static JAXBContext initializeJaxbContext() { + try { + return JAXBContext.newInstance(JAXB_GENERATED_PATH, IdentityProviderFactory.class.getClassLoader()); + } catch (JAXBException e) { + throw new RuntimeException("Unable to create JAXBContext."); + } + } + + private NiFiRegistryProperties properties; + private ExtensionManager extensionManager; + private SensitivePropertyProvider sensitivePropertyProvider; + private IdentityProvider identityProvider; + private final Map identityProviders = new HashMap<>(); + + @Autowired + public IdentityProviderFactory( + final NiFiRegistryProperties properties, + final ExtensionManager extensionManager, + @Nullable final SensitivePropertyProvider sensitivePropertyProvider) { + this.properties = properties; + this.extensionManager = extensionManager; + this.sensitivePropertyProvider = sensitivePropertyProvider; + + if (this.properties == null) { + throw new IllegalStateException("NiFiRegistryProperties cannot be null"); + } + + if (this.extensionManager == null) { + throw new IllegalStateException("ExtensionManager cannot be null"); + } + } + + @Override + public IdentityProvider getIdentityProvider(String identifier) { + return identityProviders.get(identifier); + } + + @Bean + @Primary + public IdentityProvider getIdentityProvider() throws Exception { + if (identityProvider == null) { + // look up the login identity provider to use + final String loginIdentityProviderIdentifier = properties.getProperty(NiFiRegistryProperties.SECURITY_IDENTITY_PROVIDER); + + // ensure the login identity provider class name was specified + if (StringUtils.isNotBlank(loginIdentityProviderIdentifier)) { + final IdentityProviders loginIdentityProviderConfiguration = loadLoginIdentityProvidersConfiguration(); + + // create each login identity provider + for (final Provider provider : loginIdentityProviderConfiguration.getProvider()) { + identityProviders.put(provider.getIdentifier(), createLoginIdentityProvider(provider.getIdentifier(), provider.getClazz())); + } + + // configure each login identity provider + for (final Provider provider : loginIdentityProviderConfiguration.getProvider()) { + final IdentityProvider instance = identityProviders.get(provider.getIdentifier()); + instance.onConfigured(loadLoginIdentityProviderConfiguration(provider)); + } + + // get the login identity provider instance + identityProvider = getIdentityProvider(loginIdentityProviderIdentifier); + + // ensure it was found + if (identityProvider == null) { + throw new Exception(String.format("The specified login identity provider '%s' could not be found.", loginIdentityProviderIdentifier)); + } + } + } + + return identityProvider; + } + + @Override + public void destroy() throws Exception { + if (identityProviders != null) { + identityProviders.entrySet().stream().forEach(e -> e.getValue().preDestruction()); + } + } + + private IdentityProviders loadLoginIdentityProvidersConfiguration() throws Exception { + final File loginIdentityProvidersConfigurationFile = properties.getIdentityProviderConfigurationFile(); + + // load the users from the specified file + if (loginIdentityProvidersConfigurationFile.exists()) { + try { + // find the schema + final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + final Schema schema = schemaFactory.newSchema(IdentityProviders.class.getResource(LOGIN_IDENTITY_PROVIDERS_XSD)); + + // attempt to unmarshal + XMLStreamReader xsr = XmlUtils.createSafeReader(new StreamSource(loginIdentityProvidersConfigurationFile)); + final Unmarshaller unmarshaller = JAXB_CONTEXT.createUnmarshaller(); + unmarshaller.setSchema(schema); + final JAXBElement element = unmarshaller.unmarshal(xsr, IdentityProviders.class); + return element.getValue(); + } catch (SAXException | JAXBException e) { + throw new Exception("Unable to load the login identity provider configuration file at: " + loginIdentityProvidersConfigurationFile.getAbsolutePath()); + } + } else { + throw new Exception("Unable to find the login identity provider configuration file at " + loginIdentityProvidersConfigurationFile.getAbsolutePath()); + } + } + + private IdentityProvider createLoginIdentityProvider(final String identifier, final String loginIdentityProviderClassName) throws Exception { + final IdentityProvider instance; + + final ClassLoader classLoader = extensionManager.getExtensionClassLoader(loginIdentityProviderClassName); + if (classLoader == null) { + throw new IllegalStateException("Extension not found in any of the configured class loaders: " + loginIdentityProviderClassName); + } + + // attempt to load the class + Class rawLoginIdentityProviderClass = Class.forName(loginIdentityProviderClassName, true, classLoader); + Class loginIdentityProviderClass = rawLoginIdentityProviderClass.asSubclass(IdentityProvider.class); + + // otherwise create a new instance + Constructor constructor = loginIdentityProviderClass.getConstructor(); + instance = (IdentityProvider) constructor.newInstance(); + + // method injection + performMethodInjection(instance, loginIdentityProviderClass); + + // field injection + performFieldInjection(instance, loginIdentityProviderClass); + + return instance; + } + + private IdentityProviderConfigurationContext loadLoginIdentityProviderConfiguration(final Provider provider) { + final Map providerProperties = new HashMap<>(); + + for (final Property property : provider.getProperty()) { + if (!StringUtils.isBlank(property.getEncryption())) { + String decryptedValue = decryptValue(property.getValue(), property.getEncryption()); + providerProperties.put(property.getName(), decryptedValue); + } else { + providerProperties.put(property.getName(), property.getValue()); + } + } + + return new StandardIdentityProviderConfigurationContext(provider.getIdentifier(), this, providerProperties); + } + + private void performMethodInjection(final IdentityProvider instance, final Class loginIdentityProviderClass) + throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { + + for (final Method method : loginIdentityProviderClass.getMethods()) { + if (method.isAnnotationPresent(IdentityProviderContext.class)) { + // make the method accessible + final boolean isAccessible = method.isAccessible(); + method.setAccessible(true); + + try { + final Class[] argumentTypes = method.getParameterTypes(); + + // look for setters (single argument) + if (argumentTypes.length == 1) { + final Class argumentType = argumentTypes[0]; + + // look for well known types + if (NiFiRegistryProperties.class.isAssignableFrom(argumentType)) { + // nifi properties injection + method.invoke(instance, properties); + } + } + } finally { + method.setAccessible(isAccessible); + } + } + } + + final Class parentClass = loginIdentityProviderClass.getSuperclass(); + if (parentClass != null && IdentityProvider.class.isAssignableFrom(parentClass)) { + performMethodInjection(instance, parentClass); + } + } + + private void performFieldInjection(final IdentityProvider instance, final Class loginIdentityProviderClass) throws IllegalArgumentException, IllegalAccessException { + for (final Field field : loginIdentityProviderClass.getDeclaredFields()) { + if (field.isAnnotationPresent(IdentityProviderContext.class)) { + // make the method accessible + final boolean isAccessible = field.isAccessible(); + field.setAccessible(true); + + try { + // get the type + final Class fieldType = field.getType(); + + // only consider this field if it isn't set yet + if (field.get(instance) == null) { + // look for well known types + if (NiFiRegistryProperties.class.isAssignableFrom(fieldType)) { + // nifi properties injection + field.set(instance, properties); + } + } + + } finally { + field.setAccessible(isAccessible); + } + } + } + + final Class parentClass = loginIdentityProviderClass.getSuperclass(); + if (parentClass != null && IdentityProvider.class.isAssignableFrom(parentClass)) { + performFieldInjection(instance, parentClass); + } + } + + private String decryptValue(String cipherText, String encryptionScheme) throws SensitivePropertyProtectionException { + if (sensitivePropertyProvider == null) { + throw new SensitivePropertyProtectionException("Sensitive Property Provider dependency was never wired, so protected " + + "properties cannot be decrypted. This usually indicates that a master key for this NiFi Registry was not " + + "detected and configured during the bootstrap startup sequence. Contact the system administrator."); + } + + if (!sensitivePropertyProvider.getIdentifierKey().equalsIgnoreCase(encryptionScheme)) { + throw new SensitivePropertyProtectionException("Identity Provider configuration XML was protected using " + + encryptionScheme + + ", but the configured Sensitive Property Provider supports " + + sensitivePropertyProvider.getIdentifierKey() + + ". Cannot configure this Identity Provider due to failing to decrypt protected configuration properties."); + } + + return sensitivePropertyProvider.unprotect(cipherText); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authentication/StandardIdentityProviderConfigurationContext.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authentication/StandardIdentityProviderConfigurationContext.java new file mode 100644 index 0000000000..3e89dcce4d --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authentication/StandardIdentityProviderConfigurationContext.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authentication; + +import java.util.Collections; +import java.util.Map; + +public class StandardIdentityProviderConfigurationContext implements IdentityProviderConfigurationContext { + + private final String identifier; + private final IdentityProviderLookup lookup; + private final Map properties; + + public StandardIdentityProviderConfigurationContext(String identifier, final IdentityProviderLookup lookup, Map properties) { + this.identifier = identifier; + this.lookup = lookup; + this.properties = properties; + } + + @Override + public String getIdentifier() { + return identifier; + } + + @Override + public IdentityProviderLookup getIdentityProviderLookup() { + return lookup; + } + + @Override + public Map getProperties() { + return Collections.unmodifiableMap(properties); + } + + @Override + public String getProperty(String property) { + return properties.get(property); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AbstractConfigurableAccessPolicyProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AbstractConfigurableAccessPolicyProvider.java new file mode 100644 index 0000000000..beb3f954bd --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AbstractConfigurableAccessPolicyProvider.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.util.PropertyValue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class AbstractConfigurableAccessPolicyProvider implements ConfigurableAccessPolicyProvider { + + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractConfigurableAccessPolicyProvider.class); + + public static final String PROP_USER_GROUP_PROVIDER = "User Group Provider"; + + private UserGroupProvider userGroupProvider; + private UserGroupProviderLookup userGroupProviderLookup; + + @Override + public final void initialize(final AccessPolicyProviderInitializationContext initializationContext) throws SecurityProviderCreationException { + LOGGER.debug("Initializing " + getClass().getCanonicalName()); + userGroupProviderLookup = initializationContext.getUserGroupProviderLookup(); + doInitialize(initializationContext); + LOGGER.debug("Done initializing " + getClass().getCanonicalName()); + } + + /** + * Sub-classes can override this method to perform additional initialization. + */ + protected void doInitialize(final AccessPolicyProviderInitializationContext initializationContext) + throws SecurityProviderCreationException { + + } + + @Override + public final void onConfigured(final AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException { + try { + LOGGER.debug("Configuring " + getClass().getCanonicalName()); + + final PropertyValue userGroupProviderIdentifier = configurationContext.getProperty(PROP_USER_GROUP_PROVIDER); + if (!userGroupProviderIdentifier.isSet()) { + throw new SecurityProviderCreationException("The user group provider must be specified."); + } + + userGroupProvider = userGroupProviderLookup.getUserGroupProvider(userGroupProviderIdentifier.getValue()); + if (userGroupProvider == null) { + throw new SecurityProviderCreationException("Unable to locate user group provider with identifier '" + userGroupProviderIdentifier.getValue() + "'"); + } + + doOnConfigured(configurationContext); + + LOGGER.debug("Done configuring " + getClass().getCanonicalName()); + } catch (Exception e) { + throw new SecurityProviderCreationException(e); + } + } + + /** + * Sub-classes can override this method to perform additional actions during onConfigured. + */ + protected void doOnConfigured(final AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException { + + } + + @Override + public UserGroupProvider getUserGroupProvider() { + return userGroupProvider; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AbstractPolicyBasedAuthorizer.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AbstractPolicyBasedAuthorizer.java new file mode 100644 index 0000000000..0509a3806f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AbstractPolicyBasedAuthorizer.java @@ -0,0 +1,782 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException; +import org.apache.nifi.registry.security.authorization.exception.UninheritableAuthorizationsException; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Set; + +/** + * An Authorizer that provides management of users, groups, and policies. + */ +public abstract class AbstractPolicyBasedAuthorizer implements ManagedAuthorizer { + + static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance(); + static final XMLOutputFactory XML_OUTPUT_FACTORY = XMLOutputFactory.newInstance(); + + static final String USER_ELEMENT = "user"; + static final String GROUP_USER_ELEMENT = "groupUser"; + static final String GROUP_ELEMENT = "group"; + static final String POLICY_ELEMENT = "policy"; + static final String POLICY_USER_ELEMENT = "policyUser"; + static final String POLICY_GROUP_ELEMENT = "policyGroup"; + static final String IDENTIFIER_ATTR = "identifier"; + static final String IDENTITY_ATTR = "identity"; + static final String NAME_ATTR = "name"; + static final String RESOURCE_ATTR = "resource"; + static final String ACTIONS_ATTR = "actions"; + + @Override + public final void onConfigured(final AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException { + doOnConfigured(configurationContext); + } + + /** + * Allows sub-classes to take action when onConfigured is called. + * + * @param configurationContext the configuration context + * @throws SecurityProviderCreationException if an error occurs during onConfigured process + */ + protected abstract void doOnConfigured(final AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException; + + @Override + public final AuthorizationResult authorize(AuthorizationRequest request) throws AuthorizationAccessException { + final UsersAndAccessPolicies usersAndAccessPolicies = getUsersAndAccessPolicies(); + final String resourceIdentifier = request.getResource().getIdentifier(); + + final AccessPolicy policy = usersAndAccessPolicies.getAccessPolicy(resourceIdentifier, request.getAction()); + if (policy == null) { + return AuthorizationResult.resourceNotFound(); + } + + final User user = usersAndAccessPolicies.getUser(request.getIdentity()); + if (user == null) { + return AuthorizationResult.denied(String.format("Unknown user with identity '%s'.", request.getIdentity())); + } + + final Set userGroups = usersAndAccessPolicies.getGroups(user.getIdentity()); + if (policy.getUsers().contains(user.getIdentifier()) || containsGroup(userGroups, policy)) { + return AuthorizationResult.approved(); + } + + return AuthorizationResult.denied(request.getExplanationSupplier().get()); + } + + /** + * Determines if the policy contains one of the user's groups. + * + * @param userGroups the set of the user's groups + * @param policy the policy + * @return true if one of the Groups in userGroups is contained in the policy + */ + private boolean containsGroup(final Set userGroups, final AccessPolicy policy) { + if (userGroups.isEmpty() || policy.getGroups().isEmpty()) { + return false; + } + + for (Group userGroup : userGroups) { + if (policy.getGroups().contains(userGroup.getIdentifier())) { + return true; + } + } + + return false; + } + + /** + * Adds a new group. + * + * @param group the Group to add + * @return the added Group + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + * @throws IllegalStateException if a group with the same name already exists + */ + public final synchronized Group addGroup(Group group) throws AuthorizationAccessException { + return doAddGroup(group); + } + + /** + * Adds a new group. + * + * @param group the Group to add + * @return the added Group + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract Group doAddGroup(Group group) throws AuthorizationAccessException; + + /** + * Retrieves a Group by id. + * + * @param identifier the identifier of the Group to retrieve + * @return the Group with the given identifier, or null if no matching group was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract Group getGroup(String identifier) throws AuthorizationAccessException; + + /** + * The group represented by the provided instance will be updated based on the provided instance. + * + * @param group an updated group instance + * @return the updated group instance, or null if no matching group was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + * @throws IllegalStateException if there is already a group with the same name + */ + public final synchronized Group updateGroup(Group group) throws AuthorizationAccessException { + return doUpdateGroup(group); + } + + /** + * The group represented by the provided instance will be updated based on the provided instance. + * + * @param group an updated group instance + * @return the updated group instance, or null if no matching group was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract Group doUpdateGroup(Group group) throws AuthorizationAccessException; + + /** + * Deletes the given group. + * + * @param group the group to delete + * @return the deleted group, or null if no matching group was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract Group deleteGroup(Group group) throws AuthorizationAccessException; + + /** + * Retrieves all groups. + * + * @return a list of groups + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract Set getGroups() throws AuthorizationAccessException; + + + /** + * Adds the given user. + * + * @param user the user to add + * @return the user that was added + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + * @throws IllegalStateException if there is already a user with the same identity + */ + public final synchronized User addUser(User user) throws AuthorizationAccessException { + return doAddUser(user); + } + + /** + * Adds the given user. + * + * @param user the user to add + * @return the user that was added + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract User doAddUser(User user) throws AuthorizationAccessException; + + /** + * Retrieves the user with the given identifier. + * + * @param identifier the id of the user to retrieve + * @return the user with the given id, or null if no matching user was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract User getUser(String identifier) throws AuthorizationAccessException; + + /** + * Retrieves the user with the given identity. + * + * @param identity the identity of the user to retrieve + * @return the user with the given identity, or null if no matching user was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract User getUserByIdentity(String identity) throws AuthorizationAccessException; + + /** + * The user represented by the provided instance will be updated based on the provided instance. + * + * @param user an updated user instance + * @return the updated user instance, or null if no matching user was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + * @throws IllegalStateException if there is already a user with the same identity + */ + public final synchronized User updateUser(final User user) throws AuthorizationAccessException { + return doUpdateUser(user); + } + + /** + * The user represented by the provided instance will be updated based on the provided instance. + * + * @param user an updated user instance + * @return the updated user instance, or null if no matching user was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract User doUpdateUser(User user) throws AuthorizationAccessException; + + /** + * Deletes the given user. + * + * @param user the user to delete + * @return the user that was deleted, or null if no matching user was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract User deleteUser(User user) throws AuthorizationAccessException; + + /** + * Retrieves all users. + * + * @return a list of users + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract Set getUsers() throws AuthorizationAccessException; + + /** + * Adds the given policy ensuring that multiple policies can not be added for the same resource and action. + * + * @param accessPolicy the policy to add + * @return the policy that was added + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public final synchronized AccessPolicy addAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException { + return doAddAccessPolicy(accessPolicy); + } + + /** + * Adds the given policy. + * + * @param accessPolicy the policy to add + * @return the policy that was added + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + protected abstract AccessPolicy doAddAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException; + + /** + * Retrieves the policy with the given identifier. + * + * @param identifier the id of the policy to retrieve + * @return the policy with the given id, or null if no matching policy exists + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract AccessPolicy getAccessPolicy(String identifier) throws AuthorizationAccessException; + + /** + * The policy represented by the provided instance will be updated based on the provided instance. + * + * @param accessPolicy an updated policy + * @return the updated policy, or null if no matching policy was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract AccessPolicy updateAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException; + + /** + * Deletes the given policy. + * + * @param policy the policy to delete + * @return the deleted policy, or null if no matching policy was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract AccessPolicy deleteAccessPolicy(AccessPolicy policy) throws AuthorizationAccessException; + + /** + * Retrieves all access policies. + * + * @return a list of policies + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract Set getAccessPolicies() throws AuthorizationAccessException; + + /** + * Returns the UserAccessPolicies instance. + * + * @return the UserAccessPolicies instance + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + public abstract UsersAndAccessPolicies getUsersAndAccessPolicies() throws AuthorizationAccessException; + + /** + * Returns whether the proposed fingerprint is inheritable. + * + * @param proposedFingerprint the proposed fingerprint + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + * @throws UninheritableAuthorizationsException if the proposed fingerprint was uninheritable + */ + @Override + public final void checkInheritability(String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException { + try { + // ensure we understand the proposed fingerprint + parsePoliciesUsersAndGroups(proposedFingerprint); + } catch (final AuthorizationAccessException e) { + throw new UninheritableAuthorizationsException("Unable to parse proposed fingerprint: " + e); + } + + final List users = getSortedUsers(); + final List groups = getSortedGroups(); + final List accessPolicies = getSortedAccessPolicies(); + + // ensure we're in a state to inherit + if (!users.isEmpty() || !groups.isEmpty() || !accessPolicies.isEmpty()) { + throw new UninheritableAuthorizationsException("Proposed fingerprint is not inheritable because the current Authorizations is not empty.."); + } + } + + /** + * Parses the fingerprint and adds any users, groups, and policies to the current Authorizer. + * + * @param fingerprint the fingerprint that was obtained from calling getFingerprint() on another Authorizer. + */ + @Override + public final void inheritFingerprint(final String fingerprint) throws AuthorizationAccessException { + if (fingerprint == null || fingerprint.trim().isEmpty()) { + return; + } + + final PoliciesUsersAndGroups policiesUsersAndGroups = parsePoliciesUsersAndGroups(fingerprint); + policiesUsersAndGroups.getUsers().forEach(user -> addUser(user)); + policiesUsersAndGroups.getGroups().forEach(group -> addGroup(group)); + policiesUsersAndGroups.getAccessPolicies().forEach(policy -> addAccessPolicy(policy)); + } + + private PoliciesUsersAndGroups parsePoliciesUsersAndGroups(final String fingerprint) { + final List accessPolicies = new ArrayList<>(); + final List users = new ArrayList<>(); + final List groups = new ArrayList<>(); + + final byte[] fingerprintBytes = fingerprint.getBytes(StandardCharsets.UTF_8); + try (final ByteArrayInputStream in = new ByteArrayInputStream(fingerprintBytes)) { + final DocumentBuilder docBuilder = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder(); + final Document document = docBuilder.parse(in); + final Element rootElement = document.getDocumentElement(); + + // parse all the users and add them to the current authorizer + NodeList userNodes = rootElement.getElementsByTagName(USER_ELEMENT); + for (int i=0; i < userNodes.getLength(); i++) { + Node userNode = userNodes.item(i); + users.add(parseUser((Element) userNode)); + } + + // parse all the groups and add them to the current authorizer + NodeList groupNodes = rootElement.getElementsByTagName(GROUP_ELEMENT); + for (int i=0; i < groupNodes.getLength(); i++) { + Node groupNode = groupNodes.item(i); + groups.add(parseGroup((Element) groupNode)); + } + + // parse all the policies and add them to the current authorizer + NodeList policyNodes = rootElement.getElementsByTagName(POLICY_ELEMENT); + for (int i=0; i < policyNodes.getLength(); i++) { + Node policyNode = policyNodes.item(i); + accessPolicies.add(parsePolicy((Element) policyNode)); + } + } catch (SAXException | ParserConfigurationException | IOException e) { + throw new AuthorizationAccessException("Unable to parse fingerprint", e); + } + + return new PoliciesUsersAndGroups(accessPolicies, users, groups); + } + + private User parseUser(final Element element) { + final User.Builder builder = new User.Builder() + .identifier(element.getAttribute(IDENTIFIER_ATTR)) + .identity(element.getAttribute(IDENTITY_ATTR)); + + return builder.build(); + } + + private Group parseGroup(final Element element) { + final Group.Builder builder = new Group.Builder() + .identifier(element.getAttribute(IDENTIFIER_ATTR)) + .name(element.getAttribute(NAME_ATTR)); + + NodeList groupUsers = element.getElementsByTagName(GROUP_USER_ELEMENT); + for (int i=0; i < groupUsers.getLength(); i++) { + Element groupUserNode = (Element) groupUsers.item(i); + builder.addUser(groupUserNode.getAttribute(IDENTIFIER_ATTR)); + } + + return builder.build(); + } + + private AccessPolicy parsePolicy(final Element element) { + final AccessPolicy.Builder builder = new AccessPolicy.Builder() + .identifier(element.getAttribute(IDENTIFIER_ATTR)) + .resource(element.getAttribute(RESOURCE_ATTR)); + + final String actions = element.getAttribute(ACTIONS_ATTR); + if (actions.equals(RequestAction.READ.name())) { + builder.action(RequestAction.READ); + } else if (actions.equals(RequestAction.WRITE.name())) { + builder.action(RequestAction.WRITE); + } else if (actions.equals(RequestAction.DELETE.name())) { + builder.action(RequestAction.DELETE); + } else { + throw new IllegalStateException("Unknown Policy Action: " + actions); + } + + NodeList policyUsers = element.getElementsByTagName(POLICY_USER_ELEMENT); + for (int i=0; i < policyUsers.getLength(); i++) { + Element policyUserNode = (Element) policyUsers.item(i); + builder.addUser(policyUserNode.getAttribute(IDENTIFIER_ATTR)); + } + + NodeList policyGroups = element.getElementsByTagName(POLICY_GROUP_ELEMENT); + for (int i=0; i < policyGroups.getLength(); i++) { + Element policyGroupNode = (Element) policyGroups.item(i); + builder.addGroup(policyGroupNode.getAttribute(IDENTIFIER_ATTR)); + } + + return builder.build(); + } + + @Override + public final AccessPolicyProvider getAccessPolicyProvider() { + return new ConfigurableAccessPolicyProvider() { + @Override + public Set getAccessPolicies() throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.getAccessPolicies(); + } + + @Override + public AccessPolicy getAccessPolicy(String identifier) throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.getAccessPolicy(identifier); + } + + @Override + public AccessPolicy addAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.addAccessPolicy(accessPolicy); + } + + @Override + public AccessPolicy updateAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.updateAccessPolicy(accessPolicy); + } + + @Override + public AccessPolicy deleteAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.deleteAccessPolicy(accessPolicy); + } + + @Override + public AccessPolicy getAccessPolicy(String resourceIdentifier, RequestAction action) throws AuthorizationAccessException { + final UsersAndAccessPolicies usersAndAccessPolicies = AbstractPolicyBasedAuthorizer.this.getUsersAndAccessPolicies(); + return usersAndAccessPolicies.getAccessPolicy(resourceIdentifier, action); + } + + @Override + public String getFingerprint() throws AuthorizationAccessException { + // fingerprint is managed by the encapsulating class + throw new UnsupportedOperationException(); + } + + @Override + public void inheritFingerprint(String fingerprint) throws AuthorizationAccessException { + // fingerprint is managed by the encapsulating class + throw new UnsupportedOperationException(); + } + + @Override + public void checkInheritability(String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException { + // fingerprint is managed by the encapsulating class + throw new UnsupportedOperationException(); + } + + @Override + public UserGroupProvider getUserGroupProvider() { + return new ConfigurableUserGroupProvider() { + @Override + public User addUser(User user) throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.addUser(user); + } + + @Override + public User updateUser(User user) throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.updateUser(user); + } + + @Override + public User deleteUser(User user) throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.deleteUser(user); + } + + @Override + public Group addGroup(Group group) throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.addGroup(group); + } + + @Override + public Group updateGroup(Group group) throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.updateGroup(group); + } + + @Override + public Group deleteGroup(Group group) throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.deleteGroup(group); + } + + @Override + public Set getUsers() throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.getUsers(); + } + + @Override + public User getUser(String identifier) throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.getUser(identifier); + } + + @Override + public User getUserByIdentity(String identity) throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.getUserByIdentity(identity); + } + + @Override + public Set getGroups() throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.getGroups(); + } + + @Override + public Group getGroup(String identifier) throws AuthorizationAccessException { + return AbstractPolicyBasedAuthorizer.this.getGroup(identifier); + } + + @Override + public UserAndGroups getUserAndGroups(String identity) throws AuthorizationAccessException { + final UsersAndAccessPolicies usersAndAccessPolicies = AbstractPolicyBasedAuthorizer.this.getUsersAndAccessPolicies(); + final User user = usersAndAccessPolicies.getUser(identity); + final Set groups = usersAndAccessPolicies.getGroups(identity); + + return new UserAndGroups() { + @Override + public User getUser() { + return user; + } + + @Override + public Set getGroups() { + return groups; + } + }; + } + + @Override + public String getFingerprint() throws AuthorizationAccessException { + // fingerprint is managed by the encapsulating class + throw new UnsupportedOperationException(); + } + + @Override + public void inheritFingerprint(String fingerprint) throws AuthorizationAccessException { + // fingerprint is managed by the encapsulating class + throw new UnsupportedOperationException(); + } + + @Override + public void checkInheritability(String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException { + // fingerprint is managed by the encapsulating class + throw new UnsupportedOperationException(); + } + + @Override + public void initialize(UserGroupProviderInitializationContext initializationContext) throws SecurityProviderCreationException { + } + + @Override + public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException { + } + + @Override + public void preDestruction() throws SecurityProviderDestructionException { + } + }; + } + + @Override + public void initialize(AccessPolicyProviderInitializationContext initializationContext) throws SecurityProviderCreationException { + } + + @Override + public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException { + } + + @Override + public void preDestruction() throws SecurityProviderDestructionException { + } + }; + } + + /** + * Returns a fingerprint representing the authorizations managed by this authorizer. The fingerprint will be + * used for comparison to determine if two policy-based authorizers represent a compatible set of users, + * groups, and policies. + * + * @return the fingerprint for this Authorizer + */ + @Override + public final String getFingerprint() throws AuthorizationAccessException { + final List users = getSortedUsers(); + final List groups = getSortedGroups(); + final List policies = getSortedAccessPolicies(); + + XMLStreamWriter writer = null; + final StringWriter out = new StringWriter(); + try { + writer = XML_OUTPUT_FACTORY.createXMLStreamWriter(out); + writer.writeStartDocument(); + writer.writeStartElement("authorizations"); + + for (User user : users) { + writeUser(writer, user); + } + for (Group group : groups) { + writeGroup(writer, group); + } + for (AccessPolicy policy : policies) { + writePolicy(writer, policy); + } + + writer.writeEndElement(); + writer.writeEndDocument(); + writer.flush(); + } catch (XMLStreamException e) { + throw new AuthorizationAccessException("Unable to generate fingerprint", e); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (XMLStreamException e) { + // nothing to do here + } + } + } + + return out.toString(); + } + + private void writeUser(final XMLStreamWriter writer, final User user) throws XMLStreamException { + writer.writeStartElement(USER_ELEMENT); + writer.writeAttribute(IDENTIFIER_ATTR, user.getIdentifier()); + writer.writeAttribute(IDENTITY_ATTR, user.getIdentity()); + writer.writeEndElement(); + } + + private void writeGroup(final XMLStreamWriter writer, final Group group) throws XMLStreamException { + List users = new ArrayList<>(group.getUsers()); + Collections.sort(users); + + writer.writeStartElement(GROUP_ELEMENT); + writer.writeAttribute(IDENTIFIER_ATTR, group.getIdentifier()); + writer.writeAttribute(NAME_ATTR, group.getName()); + + for (String user : users) { + writer.writeStartElement(GROUP_USER_ELEMENT); + writer.writeAttribute(IDENTIFIER_ATTR, user); + writer.writeEndElement(); + } + + writer.writeEndElement(); + } + + private void writePolicy(final XMLStreamWriter writer, final AccessPolicy policy) throws XMLStreamException { + // sort the users for the policy + List policyUsers = new ArrayList<>(policy.getUsers()); + Collections.sort(policyUsers); + + // sort the groups for this policy + List policyGroups = new ArrayList<>(policy.getGroups()); + Collections.sort(policyGroups); + + writer.writeStartElement(POLICY_ELEMENT); + writer.writeAttribute(IDENTIFIER_ATTR, policy.getIdentifier()); + writer.writeAttribute(RESOURCE_ATTR, policy.getResource()); + writer.writeAttribute(ACTIONS_ATTR, policy.getAction().name()); + + for (String policyUser : policyUsers) { + writer.writeStartElement(POLICY_USER_ELEMENT); + writer.writeAttribute(IDENTIFIER_ATTR, policyUser); + writer.writeEndElement(); + } + + for (String policyGroup : policyGroups) { + writer.writeStartElement(POLICY_GROUP_ELEMENT); + writer.writeAttribute(IDENTIFIER_ATTR, policyGroup); + writer.writeEndElement(); + } + + writer.writeEndElement(); + } + + private List getSortedAccessPolicies() { + final List policies = new ArrayList<>(getAccessPolicies()); + Collections.sort(policies, Comparator.comparing(AccessPolicy::getIdentifier)); + return policies; + } + + private List getSortedGroups() { + final List groups = new ArrayList<>(getGroups()); + Collections.sort(groups, Comparator.comparing(Group::getIdentifier)); + return groups; + } + + private List getSortedUsers() { + final List users = new ArrayList<>(getUsers()); + Collections.sort(users, Comparator.comparing(User::getIdentifier)); + return users; + } + + private static class PoliciesUsersAndGroups { + final List accessPolicies; + final List users; + final List groups; + + public PoliciesUsersAndGroups(List accessPolicies, List users, List groups) { + this.accessPolicies = accessPolicies; + this.users = users; + this.groups = groups; + } + + public List getAccessPolicies() { + return accessPolicies; + } + + public List getUsers() { + return users; + } + + public List getGroups() { + return groups; + } + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizableLookup.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizableLookup.java new file mode 100644 index 0000000000..49db37a035 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizableLookup.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +import org.apache.nifi.registry.security.authorization.resource.Authorizable; + +public interface AuthorizableLookup { + + /** + * Get the authorizable for /actuator. + * + * @return authorizable + */ + Authorizable getActuatorAuthorizable(); + + /** + * Get the authorizable for /swagger. + * + * @return authorizable + */ + Authorizable getSwaggerAuthorizable(); + + /** + * Get the authorizable for /proxy. + * + * @return authorizable + */ + Authorizable getProxyAuthorizable(); + + /** + * Get the authorizable for all tenants. + * + * Get the {@link Authorizable} that represents the resource of users and user groups. + * @return authorizable + */ + Authorizable getTenantsAuthorizable(); + + /** + * Get the authorizable for all access policies. + * + * @return authorizable + */ + Authorizable getPoliciesAuthorizable(); + + /** + * Get the authorizable for all Buckets. + * + * @return authorizable + */ + Authorizable getBucketsAuthorizable(); + + /** + * Get the authorizable for the Bucket with the bucket id. + * + * @param bucketIdentifier bucket id + * @return authorizable + */ + Authorizable getBucketAuthorizable(String bucketIdentifier); + + /** + * Get the authorizable of the specified resource. + * If the resource is authorized by its base/top-level + * resource type, the authorizable for the base type will be returned. + * + * @param resource resource + * @return authorizable + */ + Authorizable getAuthorizableByResource(final String resource); + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerCapabilityDetection.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerCapabilityDetection.java new file mode 100644 index 0000000000..065258310d --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerCapabilityDetection.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +public final class AuthorizerCapabilityDetection { + + public static boolean isManagedAuthorizer(final Authorizer authorizer) { + return authorizer instanceof ManagedAuthorizer; + } + + public static boolean isConfigurableAccessPolicyProvider(final Authorizer authorizer) { + if (!isManagedAuthorizer(authorizer)) { + return false; + } + + final ManagedAuthorizer managedAuthorizer = (ManagedAuthorizer) authorizer; + return managedAuthorizer.getAccessPolicyProvider() instanceof ConfigurableAccessPolicyProvider; + } + + public static boolean isConfigurableUserGroupProvider(final Authorizer authorizer) { + if (!isManagedAuthorizer(authorizer)) { + return false; + } + + final ManagedAuthorizer managedAuthorizer = (ManagedAuthorizer) authorizer; + final AccessPolicyProvider accessPolicyProvider = managedAuthorizer.getAccessPolicyProvider(); + return accessPolicyProvider.getUserGroupProvider() instanceof ConfigurableUserGroupProvider; + } + + public static boolean isUserConfigurable(final Authorizer authorizer, final User user) { + if (!isConfigurableUserGroupProvider(authorizer)) { + return false; + } + + final ManagedAuthorizer managedAuthorizer = (ManagedAuthorizer) authorizer; + final ConfigurableUserGroupProvider configurableUserGroupProvider = (ConfigurableUserGroupProvider) managedAuthorizer.getAccessPolicyProvider().getUserGroupProvider(); + return configurableUserGroupProvider.isConfigurable(user); + } + + public static boolean isGroupConfigurable(final Authorizer authorizer, final Group group) { + if (!isConfigurableUserGroupProvider(authorizer)) { + return false; + } + + final ManagedAuthorizer managedAuthorizer = (ManagedAuthorizer) authorizer; + final ConfigurableUserGroupProvider configurableUserGroupProvider = (ConfigurableUserGroupProvider) managedAuthorizer.getAccessPolicyProvider().getUserGroupProvider(); + return configurableUserGroupProvider.isConfigurable(group); + } + + public static boolean isAccessPolicyConfigurable(final Authorizer authorizer, final AccessPolicy accessPolicy) { + if (!isConfigurableAccessPolicyProvider(authorizer)) { + return false; + } + + final ManagedAuthorizer managedAuthorizer = (ManagedAuthorizer) authorizer; + final ConfigurableAccessPolicyProvider configurableAccessPolicyProvider = (ConfigurableAccessPolicyProvider) managedAuthorizer.getAccessPolicyProvider(); + return configurableAccessPolicyProvider.isConfigurable(accessPolicy); + } + + private AuthorizerCapabilityDetection() {} +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerFactory.java new file mode 100644 index 0000000000..1fb3d90584 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerFactory.java @@ -0,0 +1,994 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.nifi.registry.extension.ExtensionClassLoader; +import org.apache.nifi.registry.extension.ExtensionCloseable; +import org.apache.nifi.registry.extension.ExtensionManager; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.apache.nifi.registry.properties.SensitivePropertyProtectionException; +import org.apache.nifi.registry.properties.SensitivePropertyProvider; +import org.apache.nifi.registry.security.authorization.annotation.AuthorizerContext; +import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException; +import org.apache.nifi.registry.security.authorization.exception.UninheritableAuthorizationsException; +import org.apache.nifi.registry.security.authorization.generated.Authorizers; +import org.apache.nifi.registry.security.authorization.generated.Prop; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException; +import org.apache.nifi.registry.security.identity.IdentityMapper; +import org.apache.nifi.registry.security.util.ClassLoaderUtils; +import org.apache.nifi.registry.security.util.XmlUtils; +import org.apache.nifi.registry.service.RegistryService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.Nullable; +import org.springframework.transaction.annotation.Transactional; +import org.xml.sax.SAXException; + +import javax.sql.DataSource; +import javax.xml.XMLConstants; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBElement; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; +import javax.xml.stream.XMLStreamException; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import java.io.File; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Creates and configures Authorizers and their providers based on the configuration (authorizers.xml). + * + * This implementation of AuthorizerFactory in NiFi Registry is based on a combination of + * NiFi's AuthorizerFactory and AuthorizerFactoryBean. + * + * This class is annotated with Spring's @Transactional because a provider may have a DataSource injected and perform + * database operations during initialization and configuration when we are outside of the application's normal + * transactional scope during the processing of an incoming request. + */ +@Transactional +@Configuration("authorizerFactory") +public class AuthorizerFactory implements UserGroupProviderLookup, AccessPolicyProviderLookup, AuthorizerLookup, DisposableBean { + + private static final Logger logger = LoggerFactory.getLogger(AuthorizerFactory.class); + + private static final String AUTHORIZERS_XSD = "/authorizers.xsd"; + private static final String JAXB_GENERATED_PATH = "org.apache.nifi.registry.security.authorization.generated"; + private static final JAXBContext JAXB_CONTEXT = initializeJaxbContext(); + + /** + * Load the JAXBContext. + */ + private static JAXBContext initializeJaxbContext() { + try { + return JAXBContext.newInstance(JAXB_GENERATED_PATH, AuthorizerFactory.class.getClassLoader()); + } catch (JAXBException e) { + throw new RuntimeException("Unable to create JAXBContext.", e); + } + } + + private final NiFiRegistryProperties properties; + private final ExtensionManager extensionManager; + private final SensitivePropertyProvider sensitivePropertyProvider; + private final RegistryService registryService; + private final DataSource dataSource; + private final IdentityMapper identityMapper; + + private Authorizer authorizer; + private final Map userGroupProviders = new HashMap<>(); + private final Map accessPolicyProviders = new HashMap<>(); + private final Map authorizers = new HashMap<>(); + + @Autowired + public AuthorizerFactory( + final NiFiRegistryProperties properties, + final ExtensionManager extensionManager, + @Nullable final SensitivePropertyProvider sensitivePropertyProvider, + final RegistryService registryService, + final DataSource dataSource, + final IdentityMapper identityMapper) { + + this.properties = Validate.notNull(properties); + this.extensionManager = Validate.notNull(extensionManager); + this.sensitivePropertyProvider = sensitivePropertyProvider; + this.registryService = Validate.notNull(registryService); + this.dataSource = Validate.notNull(dataSource); + this.identityMapper = Validate.notNull(identityMapper); + } + + /***** UserGroupProviderLookup *****/ + + @Override + public UserGroupProvider getUserGroupProvider(String identifier) { + return userGroupProviders.get(identifier); + } + + /***** AccessPolicyProviderLookup *****/ + + @Override + public AccessPolicyProvider getAccessPolicyProvider(String identifier) { + return accessPolicyProviders.get(identifier); + } + + + /***** AuthorizerLookup *****/ + + @Override + public Authorizer getAuthorizer(String identifier) { + return authorizers.get(identifier); + } + + /***** AuthorizerFactory / DisposableBean *****/ + + @Bean + public Authorizer getAuthorizer() throws AuthorizerFactoryException { + if (authorizer == null) { + if (properties.getSslPort() == null) { + // use a default authorizer... only allowable when running not securely + authorizer = createDefaultAuthorizer(); + } else { + // look up the authorizer to use + final String authorizerIdentifier = properties.getProperty(NiFiRegistryProperties.SECURITY_AUTHORIZER); + + // ensure the authorizer class name was specified + if (StringUtils.isBlank(authorizerIdentifier)) { + throw new AuthorizerFactoryException("When running securely, the authorizer identifier must be specified in the nifi-registry.properties file."); + } else { + + try { + final Authorizers authorizerConfiguration = loadAuthorizersConfiguration(); + + // create each user group provider + for (final org.apache.nifi.registry.security.authorization.generated.UserGroupProvider userGroupProvider : authorizerConfiguration.getUserGroupProvider()) { + if (userGroupProviders.containsKey(userGroupProvider.getIdentifier())) { + throw new AuthorizerFactoryException("Duplicate User Group Provider identifier in Authorizers configuration: " + userGroupProvider.getIdentifier()); + } + userGroupProviders.put(userGroupProvider.getIdentifier(), createUserGroupProvider(userGroupProvider.getIdentifier(), userGroupProvider.getClazz())); + } + + // configure each user group provider + for (final org.apache.nifi.registry.security.authorization.generated.UserGroupProvider provider : authorizerConfiguration.getUserGroupProvider()) { + final UserGroupProvider instance = userGroupProviders.get(provider.getIdentifier()); + final ClassLoader instanceClassLoader = instance.getClass().getClassLoader(); + try (final ExtensionCloseable extClosable = ExtensionCloseable.withClassLoader(instanceClassLoader)) { + instance.onConfigured(loadAuthorizerConfiguration(provider.getIdentifier(), provider.getProperty())); + } + } + + // create each access policy provider + for (final org.apache.nifi.registry.security.authorization.generated.AccessPolicyProvider accessPolicyProvider : authorizerConfiguration.getAccessPolicyProvider()) { + if (accessPolicyProviders.containsKey(accessPolicyProvider.getIdentifier())) { + throw new AuthorizerFactoryException("Duplicate Access Policy Provider identifier in Authorizers configuration: " + accessPolicyProvider.getIdentifier()); + } + accessPolicyProviders.put(accessPolicyProvider.getIdentifier(), createAccessPolicyProvider(accessPolicyProvider.getIdentifier(), accessPolicyProvider.getClazz())); + } + + // configure each access policy provider + for (final org.apache.nifi.registry.security.authorization.generated.AccessPolicyProvider provider : authorizerConfiguration.getAccessPolicyProvider()) { + final AccessPolicyProvider instance = accessPolicyProviders.get(provider.getIdentifier()); + final ClassLoader instanceClassLoader = instance.getClass().getClassLoader(); + try (final ExtensionCloseable extClosable = ExtensionCloseable.withClassLoader(instanceClassLoader)) { + instance.onConfigured(loadAuthorizerConfiguration(provider.getIdentifier(), provider.getProperty())); + } + } + + // create each authorizer + for (final org.apache.nifi.registry.security.authorization.generated.Authorizer authorizer : authorizerConfiguration.getAuthorizer()) { + if (authorizers.containsKey(authorizer.getIdentifier())) { + throw new AuthorizerFactoryException("Duplicate Authorizer identifier in Authorizers configuration: " + authorizer.getIdentifier()); + } + authorizers.put(authorizer.getIdentifier(), createAuthorizer(authorizer.getIdentifier(), authorizer.getClazz(), authorizer.getClasspath())); + } + + // configure each authorizer + for (final org.apache.nifi.registry.security.authorization.generated.Authorizer provider : authorizerConfiguration.getAuthorizer()) { + if (provider.getIdentifier().equals(authorizerIdentifier)) { + continue; + } + final Authorizer instance = authorizers.get(provider.getIdentifier()); + final ClassLoader instanceClassLoader = instance.getClass().getClassLoader(); + try (final ExtensionCloseable extClosable = ExtensionCloseable.withClassLoader(instanceClassLoader)) { + instance.onConfigured(loadAuthorizerConfiguration(provider.getIdentifier(), provider.getProperty())); + } + } + + // get the authorizer instance + authorizer = getAuthorizer(authorizerIdentifier); + + // ensure it was found + if (authorizer == null) { + throw new AuthorizerFactoryException(String.format("The specified authorizer '%s' could not be found.", authorizerIdentifier)); + } else { + // get the ClassLoader of the Authorizer before wrapping it with anything + final ClassLoader authorizerClassLoader = authorizer.getClass().getClassLoader(); + + // install integrity checks + authorizer = AuthorizerFactory.installIntegrityChecks(authorizer); + + // load the configuration context for the selected authorizer + AuthorizerConfigurationContext authorizerConfigurationContext = null; + for (final org.apache.nifi.registry.security.authorization.generated.Authorizer provider : authorizerConfiguration.getAuthorizer()) { + if (provider.getIdentifier().equals(authorizerIdentifier)) { + authorizerConfigurationContext = loadAuthorizerConfiguration(provider.getIdentifier(), provider.getProperty()); + break; + } + } + + if (authorizerConfigurationContext == null) { + throw new IllegalStateException("Unable to load configuration for authorizer with id: " + authorizerIdentifier); + } + + // configure the authorizer that is wrapped with integrity checks + // set the context ClassLoader the wrapped authorizer's ClassLoader + try (final ExtensionCloseable extClosable = ExtensionCloseable.withClassLoader(authorizerClassLoader)) { + authorizer.onConfigured(authorizerConfigurationContext); + } + } + + } catch (AuthorizerFactoryException e) { + throw e; + } catch (Exception e) { + throw new AuthorizerFactoryException("Failed to construct Authorizer.", e); + } + } + } + } + return authorizer; + } + + @Override + public void destroy() throws Exception { + if (authorizers != null) { + authorizers.forEach((key, value) -> value.preDestruction()); + } + + if (accessPolicyProviders != null) { + accessPolicyProviders.forEach((key, value) -> value.preDestruction()); + } + + if (userGroupProviders != null) { + userGroupProviders.forEach((key, value) -> value.preDestruction()); + } + } + + private Authorizers loadAuthorizersConfiguration() throws Exception { + final File authorizersConfigurationFile = properties.getAuthorizersConfigurationFile(); + + // load the authorizers from the specified file + if (authorizersConfigurationFile.exists()) { + try { + // find the schema + final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + final Schema schema = schemaFactory.newSchema(Authorizers.class.getResource(AUTHORIZERS_XSD)); + + // attempt to unmarshal + final Unmarshaller unmarshaller = JAXB_CONTEXT.createUnmarshaller(); + unmarshaller.setSchema(schema); + final JAXBElement element = unmarshaller.unmarshal(XmlUtils.createSafeReader(new StreamSource(authorizersConfigurationFile)), Authorizers.class); + return element.getValue(); + } catch (XMLStreamException | SAXException | JAXBException e) { + throw new Exception("Unable to load the authorizer configuration file at: " + authorizersConfigurationFile.getAbsolutePath(), e); + } + } else { + throw new Exception("Unable to find the authorizer configuration file at " + authorizersConfigurationFile.getAbsolutePath()); + } + } + + private AuthorizerConfigurationContext loadAuthorizerConfiguration(final String identifier, final List properties) { + final Map authorizerProperties = new HashMap<>(); + + for (final Prop property : properties) { + if (!StringUtils.isBlank(property.getEncryption())) { + String decryptedValue = decryptValue(property.getValue(), property.getEncryption()); + authorizerProperties.put(property.getName(), decryptedValue); + } else { + authorizerProperties.put(property.getName(), property.getValue()); + } + } + return new StandardAuthorizerConfigurationContext(identifier, authorizerProperties); + } + + private UserGroupProvider createUserGroupProvider(final String identifier, final String userGroupProviderClassName) throws Exception { + final UserGroupProvider instance; + + final ClassLoader classLoader = extensionManager.getExtensionClassLoader(userGroupProviderClassName); + if (classLoader == null) { + throw new IllegalStateException("Extension not found in any of the configured class loaders: " + userGroupProviderClassName); + } + + try (final ExtensionCloseable closeable = ExtensionCloseable.withClassLoader(classLoader)) { + // attempt to load the class + Class rawUserGroupProviderClass = Class.forName(userGroupProviderClassName, true, classLoader); + Class userGroupProviderClass = rawUserGroupProviderClass.asSubclass(UserGroupProvider.class); + + // otherwise create a new instance + Constructor constructor = userGroupProviderClass.getConstructor(); + instance = (UserGroupProvider) constructor.newInstance(); + + // method injection + performMethodInjection(instance, userGroupProviderClass); + + // field injection + performFieldInjection(instance, userGroupProviderClass); + + // call post construction lifecycle event + instance.initialize(new StandardAuthorizerInitializationContext(identifier, this, this, this)); + } + + return instance; + } + + private AccessPolicyProvider createAccessPolicyProvider(final String identifier, final String accessPolicyProviderClassName) throws Exception { + final AccessPolicyProvider instance; + + final ClassLoader classLoader = extensionManager.getExtensionClassLoader(accessPolicyProviderClassName); + if (classLoader == null) { + throw new IllegalStateException("Extension not found in any of the configured class loaders: " + accessPolicyProviderClassName); + } + + try (final ExtensionCloseable closeable = ExtensionCloseable.withClassLoader(classLoader)) { + // attempt to load the class + Class rawAccessPolicyProviderClass = Class.forName(accessPolicyProviderClassName, true, classLoader); + Class accessPolicyClass = rawAccessPolicyProviderClass.asSubclass(AccessPolicyProvider.class); + + // otherwise create a new instance + Constructor constructor = accessPolicyClass.getConstructor(); + instance = (AccessPolicyProvider) constructor.newInstance(); + + // method injection + performMethodInjection(instance, accessPolicyClass); + + // field injection + performFieldInjection(instance, accessPolicyClass); + + // call post construction lifecycle event + instance.initialize(new StandardAuthorizerInitializationContext(identifier, this, this, this)); + } + + return instance; + } + + private Authorizer createAuthorizer(final String identifier, final String authorizerClassName, final String classpathResources) throws Exception { + final Authorizer instance; + + ClassLoader classLoader; + + final ExtensionClassLoader extensionClassLoader = extensionManager.getExtensionClassLoader(authorizerClassName); + if (extensionClassLoader == null) { + throw new IllegalStateException("Extension not found in any of the configured class loaders: " + authorizerClassName); + } + + // if additional classpath resources were specified, replace with a new ClassLoader that contains + // the combined resources of the original ClassLoader + the additional resources + if (StringUtils.isNotEmpty(classpathResources)) { + logger.info(String.format("Replacing Authorizer ClassLoader for '%s' to include additional resources: %s", identifier, classpathResources)); + final URL[] originalUrls = extensionClassLoader.getURLs(); + final URL[] additionalUrls = ClassLoaderUtils.getURLsForClasspath(classpathResources, null, true); + + final Set combinedUrls = new HashSet<>(); + combinedUrls.addAll(Arrays.asList(originalUrls)); + combinedUrls.addAll(Arrays.asList(additionalUrls)); + + final URL[] urls = combinedUrls.toArray(new URL[combinedUrls.size()]); + classLoader = new URLClassLoader(urls, extensionClassLoader.getParent()); + } else { + // no additional resources so just use the ExtensionClassLoader + classLoader = extensionClassLoader; + } + + try (final ExtensionCloseable closeable = ExtensionCloseable.withClassLoader(classLoader)) { + // attempt to load the class + Class rawAuthorizerClass = Class.forName(authorizerClassName, true, classLoader); + Class authorizerClass = rawAuthorizerClass.asSubclass(Authorizer.class); + + // otherwise create a new instance + Constructor constructor = authorizerClass.getConstructor(); + instance = (Authorizer) constructor.newInstance(); + + // method injection + performMethodInjection(instance, authorizerClass); + + // field injection + performFieldInjection(instance, authorizerClass); + + // call post construction lifecycle event + instance.initialize(new StandardAuthorizerInitializationContext(identifier, this, this, this)); + } + + return instance; + } + + private void performMethodInjection(final Object instance, final Class authorizerClass) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { + for (final Method method : authorizerClass.getMethods()) { + if (method.isAnnotationPresent(AuthorizerContext.class)) { + // make the method accessible + final boolean isAccessible = method.isAccessible(); + method.setAccessible(true); + + try { + final Class[] argumentTypes = method.getParameterTypes(); + + // look for setters (single argument) + if (argumentTypes.length == 1) { + final Class argumentType = argumentTypes[0]; + + // look for well known types + if (NiFiRegistryProperties.class.isAssignableFrom(argumentType)) { + // nifi properties injection + method.invoke(instance, properties); + } else if (DataSource.class.isAssignableFrom(argumentType)) { + // data source injection + method.invoke(instance, dataSource); + } else if (IdentityMapper.class.isAssignableFrom(argumentType)) { + // identity mapper injection + method.invoke(instance, identityMapper); + } + } + } finally { + method.setAccessible(isAccessible); + } + } + } + + final Class parentClass = authorizerClass.getSuperclass(); + if (parentClass != null && Authorizer.class.isAssignableFrom(parentClass)) { + performMethodInjection(instance, parentClass); + } + } + + private void performFieldInjection(final Object instance, final Class authorizerClass) throws IllegalArgumentException, IllegalAccessException { + for (final Field field : authorizerClass.getDeclaredFields()) { + if (field.isAnnotationPresent(AuthorizerContext.class)) { + // make the method accessible + final boolean isAccessible = field.isAccessible(); + field.setAccessible(true); + + try { + // get the type + final Class fieldType = field.getType(); + + // only consider this field if it isn't set yet + if (field.get(instance) == null) { + // look for well known types + if (NiFiRegistryProperties.class.isAssignableFrom(fieldType)) { + // nifi properties injection + field.set(instance, properties); + } else if (DataSource.class.isAssignableFrom(fieldType)) { + // data source injection + field.set(instance, dataSource); + } else if (IdentityMapper.class.isAssignableFrom(fieldType)) { + // identity mapper injection + field.set(instance, identityMapper); + } + } + + } finally { + field.setAccessible(isAccessible); + } + } + } + + final Class parentClass = authorizerClass.getSuperclass(); + if (parentClass != null && Authorizer.class.isAssignableFrom(parentClass)) { + performFieldInjection(instance, parentClass); + } + } + + private String decryptValue(String cipherText, String encryptionScheme) throws SensitivePropertyProtectionException { + if (sensitivePropertyProvider == null) { + throw new SensitivePropertyProtectionException("Sensitive Property Provider dependency was never wired, so protected" + + "properties cannot be decrypted. This usually indicates that a master key for this NiFi Registry was not " + + "detected and configured during the bootstrap startup sequence. Contact the system administrator."); + } + + if (!sensitivePropertyProvider.getIdentifierKey().equalsIgnoreCase(encryptionScheme)) { + throw new SensitivePropertyProtectionException("Identity Provider configuration XML was protected using " + + encryptionScheme + + ", but the configured Sensitive Property Provider supports " + + sensitivePropertyProvider.getIdentifierKey() + + ". Cannot configure this Identity Provider due to failing to decrypt protected configuration properties."); + } + + return sensitivePropertyProvider.unprotect(cipherText); + } + + + /** + * @return a default Authorizer to use when running unsecurely with no authorizer configured + */ + private Authorizer createDefaultAuthorizer() { + return new Authorizer() { + @Override + public AuthorizationResult authorize(final AuthorizationRequest request) throws AuthorizationAccessException { + return AuthorizationResult.approved(); + } + + @Override + public void initialize(AuthorizerInitializationContext initializationContext) throws SecurityProviderCreationException { + } + + @Override + public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException { + } + + @Override + public void preDestruction() throws SecurityProviderCreationException { + } + }; + } + + private interface WrappedAuthorizer { + Authorizer getBaseAuthorizer(); + } + + private static class ManagedAuthorizerWrapper implements ManagedAuthorizer, WrappedAuthorizer { + private final ManagedAuthorizer baseManagedAuthorizer; + + public ManagedAuthorizerWrapper(ManagedAuthorizer baseManagedAuthorizer) { + this.baseManagedAuthorizer = baseManagedAuthorizer; + } + + @Override + public Authorizer getBaseAuthorizer() { + return baseManagedAuthorizer; + } + + @Override + public String getFingerprint() throws AuthorizationAccessException { + return baseManagedAuthorizer.getFingerprint(); + } + + @Override + public void inheritFingerprint(String fingerprint) throws AuthorizationAccessException { + baseManagedAuthorizer.inheritFingerprint(fingerprint); + } + + @Override + public void checkInheritability(String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException { + baseManagedAuthorizer.checkInheritability(proposedFingerprint); + } + + @Override + public AccessPolicyProvider getAccessPolicyProvider() { + final AccessPolicyProvider baseAccessPolicyProvider = baseManagedAuthorizer.getAccessPolicyProvider(); + if (baseAccessPolicyProvider instanceof ConfigurableAccessPolicyProvider) { + final ConfigurableAccessPolicyProvider baseConfigurableAccessPolicyProvider = (ConfigurableAccessPolicyProvider) baseAccessPolicyProvider; + return new ConfigurableAccessPolicyProvider() { + @Override + public String getFingerprint() throws AuthorizationAccessException { + return baseConfigurableAccessPolicyProvider.getFingerprint(); + } + + @Override + public void inheritFingerprint(String fingerprint) throws AuthorizationAccessException { + baseConfigurableAccessPolicyProvider.inheritFingerprint(fingerprint); + } + + @Override + public void checkInheritability(String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException { + baseConfigurableAccessPolicyProvider.checkInheritability(proposedFingerprint); + } + + @Override + public AccessPolicy addAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException { + if (policyExists(baseConfigurableAccessPolicyProvider, accessPolicy)) { + throw new IllegalStateException(String.format("Found multiple policies for '%s' with '%s'.", accessPolicy.getResource(), accessPolicy.getAction())); + } + return baseConfigurableAccessPolicyProvider.addAccessPolicy(accessPolicy); + } + + @Override + public boolean isConfigurable(AccessPolicy accessPolicy) { + return baseConfigurableAccessPolicyProvider.isConfigurable(accessPolicy); + } + + @Override + public AccessPolicy updateAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException { + if (!baseConfigurableAccessPolicyProvider.isConfigurable(accessPolicy)) { + throw new IllegalArgumentException("The specified access policy is not support modification."); + } + return baseConfigurableAccessPolicyProvider.updateAccessPolicy(accessPolicy); + } + + @Override + public AccessPolicy deleteAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException { + if (!baseConfigurableAccessPolicyProvider.isConfigurable(accessPolicy)) { + throw new IllegalArgumentException("The specified access policy is not support modification."); + } + return baseConfigurableAccessPolicyProvider.deleteAccessPolicy(accessPolicy); + } + + @Override + public Set getAccessPolicies() throws AuthorizationAccessException { + return baseConfigurableAccessPolicyProvider.getAccessPolicies(); + } + + @Override + public AccessPolicy getAccessPolicy(String identifier) throws AuthorizationAccessException { + return baseConfigurableAccessPolicyProvider.getAccessPolicy(identifier); + } + + @Override + public AccessPolicy getAccessPolicy(String resourceIdentifier, RequestAction action) throws AuthorizationAccessException { + return baseConfigurableAccessPolicyProvider.getAccessPolicy(resourceIdentifier, action); + } + + @Override + public UserGroupProvider getUserGroupProvider() { + final UserGroupProvider baseUserGroupProvider = baseConfigurableAccessPolicyProvider.getUserGroupProvider(); + if (baseUserGroupProvider instanceof ConfigurableUserGroupProvider) { + final ConfigurableUserGroupProvider baseConfigurableUserGroupProvider = (ConfigurableUserGroupProvider) baseUserGroupProvider; + return new ConfigurableUserGroupProvider() { + @Override + public String getFingerprint() throws AuthorizationAccessException { + return baseConfigurableUserGroupProvider.getFingerprint(); + } + + @Override + public void inheritFingerprint(String fingerprint) throws AuthorizationAccessException { + baseConfigurableUserGroupProvider.inheritFingerprint(fingerprint); + } + + @Override + public void checkInheritability(String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException { + baseConfigurableUserGroupProvider.checkInheritability(proposedFingerprint); + } + + @Override + public User addUser(User user) throws AuthorizationAccessException { + if (userExists(baseConfigurableUserGroupProvider, user.getIdentifier(), user.getIdentity())) { + throw new IllegalStateException(String.format("User/user group already exists with the identity '%s'.", user.getIdentity())); + } + return baseConfigurableUserGroupProvider.addUser(user); + } + + @Override + public boolean isConfigurable(User user) { + return baseConfigurableUserGroupProvider.isConfigurable(user); + } + + @Override + public User updateUser(User user) throws AuthorizationAccessException { + if (userExists(baseConfigurableUserGroupProvider, user.getIdentifier(), user.getIdentity())) { + throw new IllegalStateException(String.format("User/user group already exists with the identity '%s'.", user.getIdentity())); + } + if (!baseConfigurableUserGroupProvider.isConfigurable(user)) { + throw new IllegalArgumentException("The specified user does not support modification."); + } + return baseConfigurableUserGroupProvider.updateUser(user); + } + + @Override + public User deleteUser(User user) throws AuthorizationAccessException { + if (!baseConfigurableUserGroupProvider.isConfigurable(user)) { + throw new IllegalArgumentException("The specified user does not support modification."); + } + return baseConfigurableUserGroupProvider.deleteUser(user); + } + + @Override + public Group addGroup(Group group) throws AuthorizationAccessException { + if (groupExists(baseConfigurableUserGroupProvider, group.getIdentifier(), group.getName())) { + throw new IllegalStateException(String.format("User/user group already exists with the identity '%s'.", group.getName())); + } + if (!allGroupUsersExist(baseUserGroupProvider, group)) { + throw new IllegalStateException(String.format("Cannot create group '%s' with users that don't exist.", group.getName())); + } + return baseConfigurableUserGroupProvider.addGroup(group); + } + + @Override + public boolean isConfigurable(Group group) { + return baseConfigurableUserGroupProvider.isConfigurable(group); + } + + @Override + public Group updateGroup(Group group) throws AuthorizationAccessException { + if (groupExists(baseConfigurableUserGroupProvider, group.getIdentifier(), group.getName())) { + throw new IllegalStateException(String.format("User/user group already exists with the identity '%s'.", group.getName())); + } + if (!baseConfigurableUserGroupProvider.isConfigurable(group)) { + throw new IllegalArgumentException("The specified group does not support modification."); + } + if (!allGroupUsersExist(baseUserGroupProvider, group)) { + throw new IllegalStateException(String.format("Cannot update group '%s' to add users that don't exist.", group.getName())); + } + return baseConfigurableUserGroupProvider.updateGroup(group); + } + + @Override + public Group deleteGroup(Group group) throws AuthorizationAccessException { + if (!baseConfigurableUserGroupProvider.isConfigurable(group)) { + throw new IllegalArgumentException("The specified group does not support modification."); + } + return baseConfigurableUserGroupProvider.deleteGroup(group); + } + + @Override + public Set getUsers() throws AuthorizationAccessException { + return baseConfigurableUserGroupProvider.getUsers(); + } + + @Override + public User getUser(String identifier) throws AuthorizationAccessException { + return baseConfigurableUserGroupProvider.getUser(identifier); + } + + @Override + public User getUserByIdentity(String identity) throws AuthorizationAccessException { + return baseConfigurableUserGroupProvider.getUserByIdentity(identity); + } + + @Override + public Set getGroups() throws AuthorizationAccessException { + return baseConfigurableUserGroupProvider.getGroups(); + } + + @Override + public Group getGroup(String identifier) throws AuthorizationAccessException { + return baseConfigurableUserGroupProvider.getGroup(identifier); + } + + @Override + public UserAndGroups getUserAndGroups(String identity) throws AuthorizationAccessException { + return baseConfigurableUserGroupProvider.getUserAndGroups(identity); + } + + @Override + public void initialize(UserGroupProviderInitializationContext initializationContext) throws SecurityProviderCreationException { + baseConfigurableUserGroupProvider.initialize(initializationContext); + } + + @Override + public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException { + baseConfigurableUserGroupProvider.onConfigured(configurationContext); + } + + @Override + public void preDestruction() throws SecurityProviderDestructionException { + baseConfigurableUserGroupProvider.preDestruction(); + } + }; + } else { + return baseUserGroupProvider; + } + } + + @Override + public void initialize(AccessPolicyProviderInitializationContext initializationContext) throws SecurityProviderCreationException { + baseConfigurableAccessPolicyProvider.initialize(initializationContext); + } + + @Override + public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException { + baseConfigurableAccessPolicyProvider.onConfigured(configurationContext); + } + + @Override + public void preDestruction() throws SecurityProviderDestructionException { + baseConfigurableAccessPolicyProvider.preDestruction(); + } + }; + } else { + return baseAccessPolicyProvider; + } + } + + @Override + public AuthorizationResult authorize(AuthorizationRequest request) throws AuthorizationAccessException { + final AuthorizationResult result = baseManagedAuthorizer.authorize(request); + + // audit the authorization request + audit(baseManagedAuthorizer, request, result); + + return result; + } + + @Override + public void initialize(AuthorizerInitializationContext initializationContext) throws SecurityProviderCreationException { + baseManagedAuthorizer.initialize(initializationContext); + } + + @Override + public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException { + baseManagedAuthorizer.onConfigured(configurationContext); + + final AccessPolicyProvider accessPolicyProvider = baseManagedAuthorizer.getAccessPolicyProvider(); + final UserGroupProvider userGroupProvider = accessPolicyProvider.getUserGroupProvider(); + + // ensure that only one policy per resource-action exists + final Set allPolicies = accessPolicyProvider.getAccessPolicies(); + for (AccessPolicy accessPolicy : allPolicies) { + if (policyExists(allPolicies, accessPolicy)) { + throw new SecurityProviderCreationException(String.format("Found multiple policies for '%s' with '%s'.", accessPolicy.getResource(), accessPolicy.getAction())); + } + } + + // ensure that only one group exists per identity + for (User user : userGroupProvider.getUsers()) { + if (userExists(userGroupProvider, user.getIdentifier(), user.getIdentity())) { + throw new SecurityProviderCreationException(String.format("Found multiple users/user groups with identity '%s'.", user.getIdentity())); + } + } + + // ensure that only one group exists per identity + for (Group group : userGroupProvider.getGroups()) { + if (groupExists(userGroupProvider, group.getIdentifier(), group.getName())) { + throw new SecurityProviderCreationException(String.format("Found multiple users/user groups with name '%s'.", group.getName())); + } + } + } + + @Override + public void preDestruction() throws SecurityProviderDestructionException { + baseManagedAuthorizer.preDestruction(); + } + } + + private static class AuthorizerWrapper implements Authorizer, WrappedAuthorizer { + private final Authorizer baseAuthorizer; + + public AuthorizerWrapper(Authorizer baseAuthorizer) { + this.baseAuthorizer = baseAuthorizer; + } + + @Override + public Authorizer getBaseAuthorizer() { + return baseAuthorizer; + } + + @Override + public AuthorizationResult authorize(AuthorizationRequest request) throws AuthorizationAccessException { + final AuthorizationResult result = baseAuthorizer.authorize(request); + + // audit the authorization request + audit(baseAuthorizer, request, result); + + return result; + } + + @Override + public void initialize(AuthorizerInitializationContext initializationContext) throws SecurityProviderCreationException { + baseAuthorizer.initialize(initializationContext); + } + + @Override + public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException { + baseAuthorizer.onConfigured(configurationContext); + } + + @Override + public void preDestruction() throws SecurityProviderDestructionException { + baseAuthorizer.preDestruction(); + } + } + + private static Authorizer installIntegrityChecks(final Authorizer baseAuthorizer) { + if (baseAuthorizer instanceof ManagedAuthorizer) { + return new ManagedAuthorizerWrapper((ManagedAuthorizer) baseAuthorizer); + } else { + return new AuthorizerWrapper(baseAuthorizer); + } + } + + private static void audit(final Authorizer authorizer, final AuthorizationRequest request, final AuthorizationResult result) { + // audit when... + // 1 - the authorizer supports auditing + // 2 - the request is an access attempt + // 3 - the result is either approved/denied, when resource is not found a subsequent request may be following with the parent resource + if (authorizer instanceof AuthorizationAuditor && request.isAccessAttempt() && !AuthorizationResult.Result.ResourceNotFound.equals(result.getResult())) { + ((AuthorizationAuditor) authorizer).auditAccessAttempt(request, result); + } + } + + /** + * Checks if another policy exists with the same resource and action as the given policy. + * + * @param checkAccessPolicy an access policy being checked + * @return true if another access policy exists with the same resource and action, false otherwise + */ + private static boolean policyExists(final AccessPolicyProvider accessPolicyProvider, final AccessPolicy checkAccessPolicy) { + return policyExists(accessPolicyProvider.getAccessPolicies(), checkAccessPolicy); + } + + /** + * Checks if another policy exists with the same resource and action as the given policy. + * + * @param checkAccessPolicy an access policy being checked + * @return true if another access policy exists with the same resource and action, false otherwise + */ + private static boolean policyExists(final Collection policies, final AccessPolicy checkAccessPolicy) { + for (AccessPolicy accessPolicy : policies) { + if (!accessPolicy.getIdentifier().equals(checkAccessPolicy.getIdentifier()) + && accessPolicy.getResource().equals(checkAccessPolicy.getResource()) + && accessPolicy.getAction().equals(checkAccessPolicy.getAction())) { + return true; + } + } + return false; + } + + + /** + * Checks if another user or group exists with the same identity. + * + * @param userGroupProvider the userGroupProvider to use to lookup the tenant + * @param identifier identity of the tenant + * @param identity identity of the tenant + * @return true if another user exists with the same identity, false otherwise + */ + private static boolean userExists(final UserGroupProvider userGroupProvider, final String identifier, final String identity) { + for (User user : userGroupProvider.getUsers()) { + if (!user.getIdentifier().equals(identifier) + && user.getIdentity().equals(identity)) { + return true; + } + } + + return false; + } + + private static boolean groupExists(final UserGroupProvider userGroupProvider, final String identifier, final String identity) { + for (Group group : userGroupProvider.getGroups()) { + if (!group.getIdentifier().equals(identifier) + && group.getName().equals(identity)) { + return true; + } + } + + return false; + } + + /** + * Check that all users in the group exist. + * + * @param userGroupProvider the userGroupProvider to use to lookup the users + * @param group the group whose users will be checked for existence. + * @return true if another user exists with the same identity, false otherwise + */ + private static boolean allGroupUsersExist(final UserGroupProvider userGroupProvider, final Group group) { + for (String userIdentifier : group.getUsers()) { + User user = userGroupProvider.getUser(userIdentifier); + if (user == null) { + return false; + } + } + + return true; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerFactoryException.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerFactoryException.java new file mode 100644 index 0000000000..a47955545a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerFactoryException.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +public class AuthorizerFactoryException extends RuntimeException { + + public AuthorizerFactoryException(String message) { + super(message); + } + + public AuthorizerFactoryException(String message, Throwable cause) { + super(message, cause); + } + + public AuthorizerFactoryException(Throwable cause) { + super(cause); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/CompositeConfigurableUserGroupProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/CompositeConfigurableUserGroupProvider.java new file mode 100644 index 0000000000..b913acf619 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/CompositeConfigurableUserGroupProvider.java @@ -0,0 +1,232 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException; +import org.apache.nifi.registry.security.authorization.exception.UninheritableAuthorizationsException; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException; +import org.apache.nifi.registry.util.PropertyValue; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; + +public class CompositeConfigurableUserGroupProvider extends CompositeUserGroupProvider implements ConfigurableUserGroupProvider { + + static final String PROP_CONFIGURABLE_USER_GROUP_PROVIDER = "Configurable User Group Provider"; + + private UserGroupProviderLookup userGroupProviderLookup; + private ConfigurableUserGroupProvider configurableUserGroupProvider; + + public CompositeConfigurableUserGroupProvider() { + super(true); + } + + @Override + public void initialize(UserGroupProviderInitializationContext initializationContext) throws SecurityProviderCreationException { + userGroupProviderLookup = initializationContext.getUserGroupProviderLookup(); + + // initialize the CompositeUserGroupProvider + super.initialize(initializationContext); + } + + @Override + public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException { + final PropertyValue configurableUserGroupProviderKey = configurationContext.getProperty(PROP_CONFIGURABLE_USER_GROUP_PROVIDER); + if (!configurableUserGroupProviderKey.isSet()) { + throw new SecurityProviderCreationException("The Configurable User Group Provider must be set."); + } + + final UserGroupProvider userGroupProvider = userGroupProviderLookup.getUserGroupProvider(configurableUserGroupProviderKey.getValue()); + + if (userGroupProvider == null) { + throw new SecurityProviderCreationException(String.format("Unable to locate the Configurable User Group Provider: %s", configurableUserGroupProviderKey)); + } + + if (!(userGroupProvider instanceof ConfigurableUserGroupProvider)) { + throw new SecurityProviderCreationException(String.format("The Configurable User Group Provider is not configurable: %s", configurableUserGroupProviderKey)); + } + + // Ensure that the ConfigurableUserGroupProvider is not also listed as one of the providers for the CompositeUserGroupProvider + for (Map.Entry entry : configurationContext.getProperties().entrySet()) { + Matcher matcher = USER_GROUP_PROVIDER_PATTERN.matcher(entry.getKey()); + if (matcher.matches() && !StringUtils.isBlank(entry.getValue())) { + final String userGroupProviderKey = entry.getValue(); + + if (userGroupProviderKey.equals(configurableUserGroupProviderKey.getValue())) { + throw new SecurityProviderCreationException(String.format("Duplicate provider in Composite Configurable User Group Provider configuration: %s", userGroupProviderKey)); + } + } + } + + configurableUserGroupProvider = (ConfigurableUserGroupProvider) userGroupProvider; + + // configure the CompositeUserGroupProvider + super.onConfigured(configurationContext); + } + + @Override + public String getFingerprint() throws AuthorizationAccessException { + return configurableUserGroupProvider.getFingerprint(); + } + + @Override + public void inheritFingerprint(String fingerprint) throws AuthorizationAccessException { + configurableUserGroupProvider.inheritFingerprint(fingerprint); + } + + @Override + public void checkInheritability(String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException { + configurableUserGroupProvider.checkInheritability(proposedFingerprint); + } + + @Override + public User addUser(User user) throws AuthorizationAccessException { + return configurableUserGroupProvider.addUser(user); + } + + @Override + public boolean isConfigurable(User user) { + return configurableUserGroupProvider.isConfigurable(user); + } + + @Override + public User updateUser(User user) throws AuthorizationAccessException { + return configurableUserGroupProvider.updateUser(user); + } + + @Override + public User deleteUser(User user) throws AuthorizationAccessException { + return configurableUserGroupProvider.deleteUser(user); + } + + @Override + public Group addGroup(Group group) throws AuthorizationAccessException { + return configurableUserGroupProvider.addGroup(group); + } + + @Override + public boolean isConfigurable(Group group) { + return configurableUserGroupProvider.isConfigurable(group); + } + + @Override + public Group updateGroup(Group group) throws AuthorizationAccessException { + return configurableUserGroupProvider.updateGroup(group); + } + + @Override + public Group deleteGroup(Group group) throws AuthorizationAccessException { + return configurableUserGroupProvider.deleteGroup(group); + } + + @Override + public Set getUsers() throws AuthorizationAccessException { + final Set users = new HashSet<>(configurableUserGroupProvider.getUsers()); + users.addAll(super.getUsers()); + return users; + } + + @Override + public User getUser(String identifier) throws AuthorizationAccessException { + User user = configurableUserGroupProvider.getUser(identifier); + + if (user == null) { + user = super.getUser(identifier); + } + + return user; + } + + @Override + public User getUserByIdentity(String identity) throws AuthorizationAccessException { + User user = configurableUserGroupProvider.getUserByIdentity(identity); + + if (user == null) { + user = super.getUserByIdentity(identity); + } + + return user; + } + + @Override + public Set getGroups() throws AuthorizationAccessException { + final Set groups = new HashSet<>(configurableUserGroupProvider.getGroups()); + groups.addAll(super.getGroups()); + return groups; + } + + @Override + public Group getGroup(String identifier) throws AuthorizationAccessException { + Group group = configurableUserGroupProvider.getGroup(identifier); + + if (group == null) { + group = super.getGroup(identifier); + } + + return group; + } + + @Override + public UserAndGroups getUserAndGroups(String identity) throws AuthorizationAccessException { + + final CompositeUserAndGroups combinedResult; + + // First, lookup user and groups by identity and combine data from all providers + UserAndGroups configurableProviderResult = configurableUserGroupProvider.getUserAndGroups(identity); + UserAndGroups compositeProvidersResult = super.getUserAndGroups(identity); + + if (configurableProviderResult.getUser() != null && compositeProvidersResult.getUser() != null) { + throw new IllegalStateException("Multiple UserGroupProviders claim to provide user " + identity); + + } else if (configurableProviderResult.getUser() != null) { + combinedResult = new CompositeUserAndGroups(configurableProviderResult.getUser(), configurableProviderResult.getGroups()); + combinedResult.addAllGroups(compositeProvidersResult.getGroups()); + + } else if (compositeProvidersResult.getUser() != null) { + combinedResult = new CompositeUserAndGroups(compositeProvidersResult.getUser(), compositeProvidersResult.getGroups()); + combinedResult.addAllGroups(configurableProviderResult.getGroups()); + + } else { + return UserAndGroups.EMPTY; + } + + // Second, lookup groups containing the user identifier + String userIdentifier = combinedResult.getUser().getIdentifier(); + for (final Group group : configurableUserGroupProvider.getGroups()) { + if (group.getUsers() != null && group.getUsers().contains(userIdentifier)) { + combinedResult.addGroup(group); + } + } + for (final Group group : super.getGroups()) { + if (group.getUsers() != null && group.getUsers().contains(userIdentifier)) { + combinedResult.addGroup(group); + } + } + + return combinedResult; + } + + @Override + public void preDestruction() throws SecurityProviderDestructionException { + super.preDestruction(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/CompositeUserAndGroups.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/CompositeUserAndGroups.java new file mode 100644 index 0000000000..5665122f10 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/CompositeUserAndGroups.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +import java.util.HashSet; +import java.util.Set; + +public class CompositeUserAndGroups implements UserAndGroups { + + private User user; + private Set groups; + + public CompositeUserAndGroups() { + this.user = null; + this.groups = null; + } + + public CompositeUserAndGroups(User user, Set groups) { + this.user = user; + setGroups(groups); + } + + @Override + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + @Override + public Set getGroups() { + return groups; + } + + public void setGroups(Set groups) { + // copy the collection so that if we add to this collection it does not modify other references + if (groups != null) { + this.groups = new HashSet<>(groups); + } else { + this.groups = null; + } + } + + public void addAllGroups(Set groups) { + if (groups != null) { + if (this.groups == null) { + this.groups = new HashSet<>(); + } + this.groups.addAll(groups); + } + } + + public void addGroup(Group group) { + if (group != null) { + if (this.groups == null) { + this.groups = new HashSet<>(); + } + this.groups.add(group); + } + } + + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/CompositeUserGroupProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/CompositeUserGroupProvider.java new file mode 100644 index 0000000000..3d84c14b9f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/CompositeUserGroupProvider.java @@ -0,0 +1,207 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class CompositeUserGroupProvider implements UserGroupProvider { + + private static final Logger logger = LoggerFactory.getLogger(CompositeUserGroupProvider.class); + + static final String PROP_USER_GROUP_PROVIDER_PREFIX = "User Group Provider "; + static final Pattern USER_GROUP_PROVIDER_PATTERN = Pattern.compile(PROP_USER_GROUP_PROVIDER_PREFIX + "\\S+"); + + private final boolean allowEmptyProviderList; + + private UserGroupProviderLookup userGroupProviderLookup; + private List userGroupProviders = new ArrayList<>(); // order matters + + public CompositeUserGroupProvider() { + this(false); + } + + public CompositeUserGroupProvider(boolean allowEmptyProviderList) { + this.allowEmptyProviderList = allowEmptyProviderList; + } + + @Override + public void initialize(UserGroupProviderInitializationContext initializationContext) throws SecurityProviderCreationException { + userGroupProviderLookup = initializationContext.getUserGroupProviderLookup(); + } + + @Override + public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException { + for (Map.Entry entry : configurationContext.getProperties().entrySet()) { + Matcher matcher = USER_GROUP_PROVIDER_PATTERN.matcher(entry.getKey()); + if (matcher.matches() && !StringUtils.isBlank(entry.getValue())) { + final String userGroupProviderKey = entry.getValue(); + final UserGroupProvider userGroupProvider = userGroupProviderLookup.getUserGroupProvider(userGroupProviderKey); + + if (userGroupProvider == null) { + throw new SecurityProviderCreationException(String.format("Unable to locate the configured User Group Provider: %s", userGroupProviderKey)); + } + + if (userGroupProviders.contains(userGroupProvider)) { + throw new SecurityProviderCreationException(String.format("Duplicate provider in Composite User Group Provider configuration: %s", userGroupProviderKey)); + } + + userGroupProviders.add(userGroupProvider); + } + } + + if (!allowEmptyProviderList && userGroupProviders.isEmpty()) { + throw new SecurityProviderCreationException("At least one User Group Provider must be configured."); + } + } + + @Override + public Set getUsers() throws AuthorizationAccessException { + final Set users = new HashSet<>(); + + for (final UserGroupProvider userGroupProvider : userGroupProviders) { + users.addAll(userGroupProvider.getUsers()); + } + + return users; + } + + @Override + public User getUser(String identifier) throws AuthorizationAccessException { + User user = null; + + for (final UserGroupProvider userGroupProvider : userGroupProviders) { + user = userGroupProvider.getUser(identifier); + + if (user != null) { + break; + } + } + + return user; + } + + @Override + public User getUserByIdentity(String identity) throws AuthorizationAccessException { + User user = null; + + for (final UserGroupProvider userGroupProvider : userGroupProviders) { + user = userGroupProvider.getUserByIdentity(identity); + + if (user != null) { + break; + } + } + + return user; + } + + @Override + public Set getGroups() throws AuthorizationAccessException { + final Set groups = new HashSet<>(); + + for (final UserGroupProvider userGroupProvider : userGroupProviders) { + groups.addAll(userGroupProvider.getGroups()); + } + + return groups; + } + + @Override + public Group getGroup(String identifier) throws AuthorizationAccessException { + Group group = null; + + for (final UserGroupProvider userGroupProvider : userGroupProviders) { + group = userGroupProvider.getGroup(identifier); + + if (group != null) { + break; + } + } + + return group; + } + + @Override + public UserAndGroups getUserAndGroups(String identity) throws AuthorizationAccessException { + + // This method builds a UserAndGroups response by combining the data from all providers using a two-pass approach + + CompositeUserAndGroups compositeUserAndGroups = new CompositeUserAndGroups(); + + // First pass - call getUserAndGroups(identity) on all providers, aggregate the responses, check for multiple + // user identity matches, which should not happen as identities should by globally unique. + String providerClassForUser = ""; + for (final UserGroupProvider userGroupProvider : userGroupProviders) { + UserAndGroups userAndGroups = userGroupProvider.getUserAndGroups(identity); + + if (userAndGroups.getUser() != null) { + // is this the first match on the user? + if(compositeUserAndGroups.getUser() == null) { + compositeUserAndGroups.setUser(userAndGroups.getUser()); + providerClassForUser = userGroupProvider.getClass().getName(); + } else { + logger.warn("Multiple UserGroupProviders are claiming to provide user '{}': [{} and {}] ", + identity, + userAndGroups.getUser(), + providerClassForUser, userGroupProvider.getClass().getName()); + throw new IllegalStateException("Multiple UserGroupProviders are claiming to provide user " + identity); + } + } + + if (userAndGroups.getGroups() != null) { + compositeUserAndGroups.addAllGroups(userAndGroups.getGroups()); + } + } + + if (compositeUserAndGroups.getUser() == null) { + logger.debug("No user found for identity {}", identity); + return UserAndGroups.EMPTY; + } + + // Second pass - Now that we've matched a user, call getGroups() on all providers, and + // check all groups to see if they contain the user identifier corresponding to the identity. + // This is necessary because a provider might only know about a group<->userIdentifier mapping + // without knowing the user identifier. + String userIdentifier = compositeUserAndGroups.getUser().getIdentifier(); + for (final UserGroupProvider userGroupProvider : userGroupProviders) { + for (final Group group : userGroupProvider.getGroups()) { + if (group.getUsers() != null && group.getUsers().contains(userIdentifier)) { + compositeUserAndGroups.addGroup(group); + } + } + } + + return compositeUserAndGroups; + } + + @Override + public void preDestruction() throws SecurityProviderDestructionException { + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizableLookup.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizableLookup.java new file mode 100644 index 0000000000..6f68ebef20 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizableLookup.java @@ -0,0 +1,282 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.exception.ResourceNotFoundException; +import org.apache.nifi.registry.security.authorization.resource.Authorizable; +import org.apache.nifi.registry.security.authorization.resource.InheritingAuthorizable; +import org.apache.nifi.registry.security.authorization.resource.ProxyChainAuthorizable; +import org.apache.nifi.registry.security.authorization.resource.PublicCheckingAuthorizable; +import org.apache.nifi.registry.security.authorization.resource.ResourceFactory; +import org.apache.nifi.registry.security.authorization.resource.ResourceType; +import org.apache.nifi.registry.service.RegistryService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Objects; + +@Component +public class StandardAuthorizableLookup implements AuthorizableLookup { + + private static final Logger logger = LoggerFactory.getLogger(StandardAuthorizableLookup.class); + + private static final Authorizable TENANTS_AUTHORIZABLE = new Authorizable() { + @Override + public Authorizable getParentAuthorizable() { + return null; + } + + @Override + public Resource getResource() { + return ResourceFactory.getTenantsResource(); + } + }; + + private static final Authorizable POLICIES_AUTHORIZABLE = new Authorizable() { + @Override + public Authorizable getParentAuthorizable() { + return null; + } + + @Override + public Resource getResource() { + return ResourceFactory.getPoliciesResource(); + } + }; + + private static final Authorizable BUCKETS_AUTHORIZABLE = new Authorizable() { + @Override + public Authorizable getParentAuthorizable() { + return null; + } + + @Override + public Resource getResource() { + return ResourceFactory.getBucketsResource(); + } + }; + + private static final Authorizable PROXY_AUTHORIZABLE = new Authorizable() { + @Override + public Authorizable getParentAuthorizable() { + return null; + } + + @Override + public Resource getResource() { + return ResourceFactory.getProxyResource(); + } + }; + + private static final Authorizable ACTUATOR_AUTHORIZABLE = new Authorizable() { + @Override + public Authorizable getParentAuthorizable() { + return null; + } + + @Override + public Resource getResource() { + return ResourceFactory.getActuatorResource(); + } + }; + + private static final Authorizable SWAGGER_AUTHORIZABLE = new Authorizable() { + @Override + public Authorizable getParentAuthorizable() { + return null; + } + + @Override + public Resource getResource() { + return ResourceFactory.getSwaggerResource(); + } + }; + + private final RegistryService registryService; + + @Autowired + public StandardAuthorizableLookup(final RegistryService registryService) { + this.registryService = Objects.requireNonNull(registryService); + } + + @Override + public Authorizable getActuatorAuthorizable() { + return new ProxyChainAuthorizable(ACTUATOR_AUTHORIZABLE, PROXY_AUTHORIZABLE, this::isPublicAccessAllowed); + } + + @Override + public Authorizable getSwaggerAuthorizable() { + return new ProxyChainAuthorizable(SWAGGER_AUTHORIZABLE, PROXY_AUTHORIZABLE, this::isPublicAccessAllowed); + } + + @Override + public Authorizable getProxyAuthorizable() { + return PROXY_AUTHORIZABLE; + } + + @Override + public Authorizable getTenantsAuthorizable() { + return new ProxyChainAuthorizable(TENANTS_AUTHORIZABLE, PROXY_AUTHORIZABLE, this::isPublicAccessAllowed); + } + + @Override + public Authorizable getPoliciesAuthorizable() { + return new ProxyChainAuthorizable(POLICIES_AUTHORIZABLE, PROXY_AUTHORIZABLE, this::isPublicAccessAllowed); + } + + @Override + public Authorizable getBucketsAuthorizable() { + return new ProxyChainAuthorizable(BUCKETS_AUTHORIZABLE, PROXY_AUTHORIZABLE, this::isPublicAccessAllowed); + } + + @Override + public Authorizable getBucketAuthorizable(String bucketIdentifier) { + // Note - this creates a special Authorizable type that inherits permissions from the parent Authorizable + final Authorizable inheritingAuthorizable = new InheritingAuthorizable() { + + @Override + public Authorizable getParentAuthorizable() { + // Use the unwrapped buckets authorizable here so that we don't reauthorize the proxy chain + return BUCKETS_AUTHORIZABLE; + } + + @Override + public Resource getResource() { + return ResourceFactory.getBucketResource(bucketIdentifier, "Bucket with ID " + bucketIdentifier); + } + + }; + + // Wrap the inheriting Authorizable with logic that first checks if public access is allowed, if not then delegates to the inheriting Authorizable + final Authorizable publicCheckingAuthorizable = new PublicCheckingAuthorizable(inheritingAuthorizable, this::isPublicAccessAllowed); + + // Return ProxyChainAuthorizable -> public checking Authorizable -> inheriting Authorizable + return new ProxyChainAuthorizable(publicCheckingAuthorizable, PROXY_AUTHORIZABLE, this::isPublicAccessAllowed); + } + + @Override + public Authorizable getAuthorizableByResource(String resource) { + ResourceType resourceType = ResourceType.mapFullResourcePathToResourceType(resource); + + if (resourceType == null) { + throw new ResourceNotFoundException("Unrecognized resource: " + resource); + } + + return getAuthorizableByResource(resourceType, resource); + } + + private Authorizable getAuthorizableByResource(final ResourceType resourceType, final String resource) { + Authorizable authorizable = null; + switch (resourceType) { + + /* Access to these resources are always authorized by the top-level resource */ + case Policy: + authorizable = getPoliciesAuthorizable(); + break; + case Tenant: + authorizable = getTenantsAuthorizable(); + break; + case Proxy: + authorizable = getProxyAuthorizable(); + break; + case Actuator: + authorizable = getActuatorAuthorizable(); + break; + case Swagger: + authorizable = getSwaggerAuthorizable(); + break; + + /* Access to buckets can be authorized by the top-level /buckets resource or an individual /buckets/{id} resource */ + case Bucket: + final String childResourceId = StringUtils.substringAfter(resource, resourceType.getValue()); + if (childResourceId.startsWith("/")) { + authorizable = getAuthorizableByChildResource(resourceType, childResourceId); + } else { + authorizable = getBucketsAuthorizable(); + } + } + + if (authorizable == null) { + logger.debug("Could not determine the Authorizable for resource type='{}', path='{}', ", resourceType.getValue(), resource); + throw new IllegalArgumentException("This an unexpected type of authorizable resource: " + resourceType.getValue()); + } + + return authorizable; + } + + private Authorizable getAuthorizableByChildResource(final ResourceType baseResourceType, final String childResourceId) { + Authorizable authorizable; + switch (baseResourceType) { + case Bucket: + String[] childResourcePathParts = childResourceId.split("/"); + if (childResourcePathParts.length >= 1) { + final String bucketId = childResourcePathParts[1]; + authorizable = getBucketAuthorizable(bucketId); + break; + } + default: + throw new IllegalArgumentException("Unexpected lookup for child resource authorizable for base resource type " + baseResourceType.getValue()); + } + + return authorizable; + } + + /** + * Determines if the given Resource is considered public for the action being performed. + * + * @param resource a Resource being authorized + * @param action the action being performed + * @return true if the resource is public for the given action, false otherwise + */ + private boolean isPublicAccessAllowed(final Resource resource, final RequestAction action) { + if (resource == null || action == null) { + return false; + } + + if (action != RequestAction.READ) { + return false; + } + + final String resourceIdentifier = resource.getIdentifier(); + if (resourceIdentifier == null || !resourceIdentifier.startsWith(ResourceType.Bucket.getValue() + "/")) { + return false; + } + + final int lastSlashIndex = resourceIdentifier.lastIndexOf("/"); + if (lastSlashIndex < 0 || lastSlashIndex >= resourceIdentifier.length() - 1) { + return false; + } + + final String bucketId = resourceIdentifier.substring(lastSlashIndex + 1); + try { + final Bucket bucket = registryService.getBucket(bucketId); + return bucket.isAllowPublicRead(); + } catch (ResourceNotFoundException rnfe) { + // if not found then we can't determine public access, so return false to delegate to regular authorizer + logger.debug("Cannot determine public access, bucket not found with id [{}]", new Object[]{bucketId}); + return false; + } catch (Exception e) { + logger.error("Error checking public access to bucket with id [{}]", new Object[]{bucketId}, e); + return false; + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizerConfigurationContext.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizerConfigurationContext.java new file mode 100644 index 0000000000..9d274f70b1 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizerConfigurationContext.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +import org.apache.nifi.registry.util.PropertyValue; +import org.apache.nifi.registry.util.StandardPropertyValue; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * + */ +public class StandardAuthorizerConfigurationContext implements AuthorizerConfigurationContext { + + private final String identifier; + private final Map properties; + + public StandardAuthorizerConfigurationContext(String identifier, Map properties) { + this.identifier = identifier; + this.properties = Collections.unmodifiableMap(new HashMap(properties)); + } + + @Override + public String getIdentifier() { + return identifier; + } + + @Override + public Map getProperties() { + return properties; + } + + @Override + public PropertyValue getProperty(String property) { + return new StandardPropertyValue(properties.get(property)); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizerInitializationContext.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizerInitializationContext.java new file mode 100644 index 0000000000..d643e91eb5 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizerInitializationContext.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +public class StandardAuthorizerInitializationContext implements AuthorizerInitializationContext { + + private final String identifier; + private final UserGroupProviderLookup userGroupProviderLookup; + private final AccessPolicyProviderLookup accessPolicyProviderLookup; + private final AuthorizerLookup authorizerLookup; + + public StandardAuthorizerInitializationContext(String identifier, UserGroupProviderLookup userGroupProviderLookup, + AccessPolicyProviderLookup accessPolicyProviderLookup, AuthorizerLookup authorizerLookup) { + this.identifier = identifier; + this.userGroupProviderLookup = userGroupProviderLookup; + this.accessPolicyProviderLookup = accessPolicyProviderLookup; + this.authorizerLookup = authorizerLookup; + } + + @Override + public String getIdentifier() { + return identifier; + } + + public AuthorizerLookup getAuthorizerLookup() { + return authorizerLookup; + } + + @Override + public AccessPolicyProviderLookup getAccessPolicyProviderLookup() { + return accessPolicyProviderLookup; + } + + @Override + public UserGroupProviderLookup getUserGroupProviderLookup() { + return userGroupProviderLookup; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardManagedAuthorizer.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardManagedAuthorizer.java new file mode 100644 index 0000000000..58bcf55464 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardManagedAuthorizer.java @@ -0,0 +1,264 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException; +import org.apache.nifi.registry.security.authorization.exception.UninheritableAuthorizationsException; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException; +import org.apache.nifi.registry.util.PropertyValue; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.util.Set; + +public class StandardManagedAuthorizer implements ManagedAuthorizer { + + private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance(); + private static final XMLOutputFactory XML_OUTPUT_FACTORY = XMLOutputFactory.newInstance(); + + private static final String USER_GROUP_PROVIDER_ELEMENT = "userGroupProvider"; + private static final String ACCESS_POLICY_PROVIDER_ELEMENT = "accessPolicyProvider"; + + private AccessPolicyProviderLookup accessPolicyProviderLookup; + private AccessPolicyProvider accessPolicyProvider; + private UserGroupProvider userGroupProvider; + + public StandardManagedAuthorizer() {} + + // exposed for testing to inject mocks + public StandardManagedAuthorizer(AccessPolicyProvider accessPolicyProvider, UserGroupProvider userGroupProvider) { + this.accessPolicyProvider = accessPolicyProvider; + this.userGroupProvider = userGroupProvider; + } + + @Override + public void initialize(AuthorizerInitializationContext initializationContext) throws SecurityProviderCreationException { + accessPolicyProviderLookup = initializationContext.getAccessPolicyProviderLookup(); + } + + @Override + public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException { + final PropertyValue accessPolicyProviderKey = configurationContext.getProperty("Access Policy Provider"); + if (!accessPolicyProviderKey.isSet()) { + throw new SecurityProviderCreationException("The Access Policy Provider must be set."); + } + + accessPolicyProvider = accessPolicyProviderLookup.getAccessPolicyProvider(accessPolicyProviderKey.getValue()); + + // ensure the desired access policy provider was found + if (accessPolicyProvider == null) { + throw new SecurityProviderCreationException(String.format("Unable to locate configured Access Policy Provider: %s", accessPolicyProviderKey)); + } + + userGroupProvider = accessPolicyProvider.getUserGroupProvider(); + + // ensure the desired access policy provider has a user group provider + if (userGroupProvider == null) { + throw new SecurityProviderCreationException(String.format("Configured Access Policy Provider %s does not contain a User Group Provider", accessPolicyProviderKey)); + } + } + + @Override + public AuthorizationResult authorize(AuthorizationRequest request) throws AuthorizationAccessException { + final String resourceIdentifier = request.getResource().getIdentifier(); + final AccessPolicy policy = accessPolicyProvider.getAccessPolicy(resourceIdentifier, request.getAction()); + if (policy == null) { + return AuthorizationResult.resourceNotFound(); + } + + final UserAndGroups userAndGroups = userGroupProvider.getUserAndGroups(request.getIdentity()); + + final User user = userAndGroups.getUser(); + if (user == null) { + return AuthorizationResult.denied(String.format("Unknown user with identity '%s'.", request.getIdentity())); + } + + final Set userGroups = userAndGroups.getGroups(); + if (policy.getUsers().contains(user.getIdentifier()) || containsGroup(userGroups, policy)) { + return AuthorizationResult.approved(); + } + + return AuthorizationResult.denied(request.getExplanationSupplier().get()); + } + + /** + * Determines if the policy contains one of the user's groups. + * + * @param userGroups the set of the user's groups + * @param policy the policy + * @return true if one of the Groups in userGroups is contained in the policy + */ + private boolean containsGroup(final Set userGroups, final AccessPolicy policy) { + if (userGroups == null || userGroups.isEmpty() || policy.getGroups().isEmpty()) { + return false; + } + + for (Group userGroup : userGroups) { + if (policy.getGroups().contains(userGroup.getIdentifier())) { + return true; + } + } + + return false; + } + + @Override + public String getFingerprint() throws AuthorizationAccessException { + XMLStreamWriter writer = null; + final StringWriter out = new StringWriter(); + try { + writer = XML_OUTPUT_FACTORY.createXMLStreamWriter(out); + writer.writeStartDocument(); + writer.writeStartElement("managedAuthorizations"); + + writer.writeStartElement(ACCESS_POLICY_PROVIDER_ELEMENT); + if (accessPolicyProvider instanceof ConfigurableAccessPolicyProvider) { + writer.writeCharacters(((ConfigurableAccessPolicyProvider) accessPolicyProvider).getFingerprint()); + } + writer.writeEndElement(); + + writer.writeStartElement(USER_GROUP_PROVIDER_ELEMENT); + if (userGroupProvider instanceof ConfigurableUserGroupProvider) { + writer.writeCharacters(((ConfigurableUserGroupProvider) userGroupProvider).getFingerprint()); + } + writer.writeEndElement(); + + writer.writeEndElement(); + writer.writeEndDocument(); + writer.flush(); + } catch (XMLStreamException e) { + throw new AuthorizationAccessException("Unable to generate fingerprint", e); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (XMLStreamException e) { + // nothing to do here + } + } + } + + return out.toString(); + } + + @Override + public void inheritFingerprint(String fingerprint) throws AuthorizationAccessException { + if (StringUtils.isBlank(fingerprint)) { + return; + } + + final FingerprintHolder fingerprintHolder = parseFingerprint(fingerprint); + + if (StringUtils.isNotBlank(fingerprintHolder.getPolicyFingerprint()) && accessPolicyProvider instanceof ConfigurableAccessPolicyProvider) { + ((ConfigurableAccessPolicyProvider) accessPolicyProvider).inheritFingerprint(fingerprintHolder.getPolicyFingerprint()); + } + + if (StringUtils.isNotBlank(fingerprintHolder.getUserGroupFingerprint()) && userGroupProvider instanceof ConfigurableUserGroupProvider) { + ((ConfigurableUserGroupProvider) userGroupProvider).inheritFingerprint(fingerprintHolder.getUserGroupFingerprint()); + } + } + + @Override + public void checkInheritability(String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException { + final FingerprintHolder fingerprintHolder = parseFingerprint(proposedFingerprint); + + if (StringUtils.isNotBlank(fingerprintHolder.getPolicyFingerprint())) { + if (accessPolicyProvider instanceof ConfigurableAccessPolicyProvider) { + ((ConfigurableAccessPolicyProvider) accessPolicyProvider).checkInheritability(fingerprintHolder.getPolicyFingerprint()); + } else { + throw new UninheritableAuthorizationsException("Policy fingerprint is not blank and the configured AccessPolicyProvider does not support fingerprinting."); + } + } + + if (StringUtils.isNotBlank(fingerprintHolder.getUserGroupFingerprint())) { + if (userGroupProvider instanceof ConfigurableUserGroupProvider) { + ((ConfigurableUserGroupProvider) userGroupProvider).checkInheritability(fingerprintHolder.getUserGroupFingerprint()); + } else { + throw new UninheritableAuthorizationsException("User/Group fingerprint is not blank and the configured UserGroupProvider does not support fingerprinting."); + } + } + } + + private final FingerprintHolder parseFingerprint(final String fingerprint) throws AuthorizationAccessException { + final byte[] fingerprintBytes = fingerprint.getBytes(StandardCharsets.UTF_8); + + try (final ByteArrayInputStream in = new ByteArrayInputStream(fingerprintBytes)) { + final DocumentBuilder docBuilder = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder(); + final Document document = docBuilder.parse(in); + final Element rootElement = document.getDocumentElement(); + + final NodeList accessPolicyProviderList = rootElement.getElementsByTagName(ACCESS_POLICY_PROVIDER_ELEMENT); + if (accessPolicyProviderList.getLength() != 1) { + throw new AuthorizationAccessException(String.format("Only one %s element is allowed: %s", ACCESS_POLICY_PROVIDER_ELEMENT, fingerprint)); + } + + final NodeList userGroupProviderList = rootElement.getElementsByTagName(USER_GROUP_PROVIDER_ELEMENT); + if (userGroupProviderList.getLength() != 1) { + throw new AuthorizationAccessException(String.format("Only one %s element is allowed: %s", USER_GROUP_PROVIDER_ELEMENT, fingerprint)); + } + + final Node accessPolicyProvider = accessPolicyProviderList.item(0); + final Node userGroupProvider = userGroupProviderList.item(0); + return new FingerprintHolder(accessPolicyProvider.getTextContent(), userGroupProvider.getTextContent()); + } catch (SAXException | ParserConfigurationException | IOException e) { + throw new AuthorizationAccessException("Unable to parse fingerprint", e); + } + } + + @Override + public AccessPolicyProvider getAccessPolicyProvider() { + return accessPolicyProvider; + } + + @Override + public void preDestruction() throws SecurityProviderDestructionException { + + } + + private static class FingerprintHolder { + private final String policyFingerprint; + private final String userGroupFingerprint; + + public FingerprintHolder(String policyFingerprint, String userGroupFingerprint) { + this.policyFingerprint = policyFingerprint; + this.userGroupFingerprint = userGroupFingerprint; + } + + public String getPolicyFingerprint() { + return policyFingerprint; + } + + public String getUserGroupFingerprint() { + return userGroupFingerprint; + } + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/UntrustedProxyException.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/UntrustedProxyException.java new file mode 100644 index 0000000000..fbf1580748 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/UntrustedProxyException.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +public class UntrustedProxyException extends RuntimeException { + + public UntrustedProxyException(String message) { + super(message); + } + + public UntrustedProxyException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/UsersAndAccessPolicies.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/UsersAndAccessPolicies.java new file mode 100644 index 0000000000..7675f27f2e --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/UsersAndAccessPolicies.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +import java.util.Set; + +/** + * A holder object to provide atomic access to policies for a given resource and users by + * identity. Implementations must ensure consistent access to the data backing this instance. + */ +public interface UsersAndAccessPolicies { + + /** + * Retrieves the set of access policies for a given resource and action. + * + * @param resourceIdentifier the resource identifier to retrieve policies for + * @param action the action to retrieve policies for + * @return the access policy for the given resource and action + */ + AccessPolicy getAccessPolicy(final String resourceIdentifier, final RequestAction action); + + /** + * Retrieves a user by an identity string. + * + * @param identity the identity of the user to retrieve + * @return the user with the given identity + */ + User getUser(final String identity); + + /** + * Retrieves the groups for a given user identity. + * + * @param userIdentity a user identity + * @return the set of groups for the given user identity + */ + Set getGroups(final String userIdentity); + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/database/DatabaseAccessPolicyProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/database/DatabaseAccessPolicyProvider.java new file mode 100644 index 0000000000..2403784263 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/database/DatabaseAccessPolicyProvider.java @@ -0,0 +1,401 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.database; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.nifi.registry.security.authorization.AbstractConfigurableAccessPolicyProvider; +import org.apache.nifi.registry.security.authorization.AccessPolicy; +import org.apache.nifi.registry.security.authorization.AccessPolicyProviderInitializationContext; +import org.apache.nifi.registry.security.authorization.AuthorizerConfigurationContext; +import org.apache.nifi.registry.security.authorization.Group; +import org.apache.nifi.registry.security.authorization.RequestAction; +import org.apache.nifi.registry.security.authorization.User; +import org.apache.nifi.registry.security.authorization.annotation.AuthorizerContext; +import org.apache.nifi.registry.security.authorization.database.entity.DatabaseAccessPolicy; +import org.apache.nifi.registry.security.authorization.database.mapper.DatabaseAccessPolicyRowMapper; +import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException; +import org.apache.nifi.registry.security.authorization.exception.UninheritableAuthorizationsException; +import org.apache.nifi.registry.security.authorization.util.AccessPolicyProviderUtils; +import org.apache.nifi.registry.security.authorization.util.InitialPolicies; +import org.apache.nifi.registry.security.authorization.util.ResourceAndAction; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException; +import org.apache.nifi.registry.security.identity.IdentityMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.util.CollectionUtils; + +import javax.sql.DataSource; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Implementation of {@link org.apache.nifi.registry.security.authorization.ConfigurableAccessPolicyProvider} backed by a relational database. + */ +public class DatabaseAccessPolicyProvider extends AbstractConfigurableAccessPolicyProvider { + + private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseAccessPolicyProvider.class); + + private DataSource dataSource; + private IdentityMapper identityMapper; + + private JdbcTemplate jdbcTemplate; + + @AuthorizerContext + public void setDataSource(final DataSource dataSource) { + this.dataSource = dataSource; + } + + @AuthorizerContext + public void setIdentityMapper(final IdentityMapper identityMapper) { + this.identityMapper = identityMapper; + } + + @Override + protected void doInitialize(AccessPolicyProviderInitializationContext initializationContext) throws SecurityProviderCreationException { + super.doInitialize(initializationContext); + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + @Override + public void doOnConfigured(final AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException { + final String initialAdminIdentity = AccessPolicyProviderUtils.getInitialAdminIdentity(configurationContext, identityMapper); + final Set nifiIdentities = AccessPolicyProviderUtils.getNiFiIdentities(configurationContext, identityMapper); + final String nifiGroupName = AccessPolicyProviderUtils.getNiFiGroupName(configurationContext, identityMapper); + + if (!StringUtils.isBlank(initialAdminIdentity)) { + LOGGER.info("Populating authorizations for Initial Admin: '" + initialAdminIdentity + "'"); + populateInitialAdmin(initialAdminIdentity); + } + + if (!CollectionUtils.isEmpty(nifiIdentities)) { + LOGGER.info("Populating authorizations for NiFi identities: [{}]", StringUtils.join(nifiIdentities, ";")); + populateNiFiIdentities(nifiIdentities); + } + + if (!StringUtils.isBlank(nifiGroupName)) { + LOGGER.info("Populating authorizations for NiFi Group: '" + nifiGroupName + "'"); + populateNiFiGroup(nifiGroupName); + } + } + + private void populateInitialAdmin(final String initialAdminIdentity) { + final User initialAdmin = getUserGroupProvider().getUserByIdentity(initialAdminIdentity); + if (initialAdmin == null) { + throw new SecurityProviderCreationException("Unable to locate initial admin '" + initialAdminIdentity + "' to seed policies"); + } + + for (final ResourceAndAction resourceAction : InitialPolicies.ADMIN_POLICIES) { + populateInitialPolicy(initialAdmin, resourceAction); + } + } + + private void populateNiFiIdentities(final Set nifiIdentities) { + for (final String nifiIdentity : nifiIdentities) { + final User nifiUser = getUserGroupProvider().getUserByIdentity(nifiIdentity); + if (nifiUser == null) { + throw new SecurityProviderCreationException("Unable to locate NiFi identity '" + nifiIdentity + "' to seed policies."); + } + + for (final ResourceAndAction resourceAction : InitialPolicies.NIFI_POLICIES) { + populateInitialPolicy(nifiUser, resourceAction); + } + } + } + + private void populateNiFiGroup(final String nifiGroupName) { + final Group nifiGroup = AccessPolicyProviderUtils.getGroup(nifiGroupName, getUserGroupProvider()); + + for (final ResourceAndAction resourceAction : InitialPolicies.NIFI_POLICIES) { + populateInitialPolicy(nifiGroup, resourceAction); + } + } + + @Override + public void preDestruction() throws SecurityProviderDestructionException { + + } + + // ---- fingerprinting methods + + @Override + public String getFingerprint() throws AuthorizationAccessException { + throw new UnsupportedOperationException("Fingerprinting is not supported by this provider"); + } + + @Override + public void inheritFingerprint(String fingerprint) throws AuthorizationAccessException { + throw new UnsupportedOperationException("Fingerprinting is not supported by this provider"); + } + + @Override + public void checkInheritability(String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException { + throw new UnsupportedOperationException("Fingerprinting is not supported by this provider"); + } + + // ---- access policy methods + + @Override + public AccessPolicy addAccessPolicy(final AccessPolicy accessPolicy) throws AuthorizationAccessException { + Validate.notNull(accessPolicy); + + // insert to the policy table + final String policySql = "INSERT INTO APP_POLICY(IDENTIFIER, RESOURCE, ACTION) VALUES (?, ?, ?)"; + jdbcTemplate.update(policySql, accessPolicy.getIdentifier(), accessPolicy.getResource(), accessPolicy.getAction().toString()); + + // insert to the policy-user and policy groups table + createPolicyUserAndGroups(accessPolicy); + + return accessPolicy; + } + + @Override + public AccessPolicy updateAccessPolicy(final AccessPolicy accessPolicy) throws AuthorizationAccessException { + Validate.notNull(accessPolicy); + + // determine if policy exists + final DatabaseAccessPolicy existingPolicy = getDatabaseAcessPolicy(accessPolicy.getIdentifier()); + if (existingPolicy == null) { + return null; + } + + // delete any policy-user associations + final String deletePolicyUsersSql = "DELETE FROM APP_POLICY_USER WHERE POLICY_IDENTIFIER = ?"; + jdbcTemplate.update(deletePolicyUsersSql, accessPolicy.getIdentifier()); + + // delete any policy-group associations + final String deletePolicyGroupsSql = "DELETE FROM APP_POLICY_GROUP WHERE POLICY_IDENTIFIER = ?"; + jdbcTemplate.update(deletePolicyGroupsSql, accessPolicy.getIdentifier()); + + + // re-create the associations + createPolicyUserAndGroups(accessPolicy); + + return accessPolicy; + } + + @Override + public Set getAccessPolicies() throws AuthorizationAccessException { + // retrieve all the policies + final String sql = "SELECT * FROM APP_POLICY"; + final List databasePolicies = jdbcTemplate.query(sql, new DatabaseAccessPolicyRowMapper()); + + // retrieve all users in policies, mapped by policy id + final Map> policyToUsers = new HashMap<>(); + jdbcTemplate.query("SELECT * FROM APP_POLICY_USER", (rs) -> { + final String policyIdentifier = rs.getString("POLICY_IDENTIFIER"); + final String userIdentifier = rs.getString("USER_IDENTIFIER"); + + final Set userIdentifiers = policyToUsers.computeIfAbsent(policyIdentifier, (k) -> new HashSet<>()); + userIdentifiers.add(userIdentifier); + }); + + // retrieve all groups in policies, mapped by policy id + final Map> policyToGroups = new HashMap<>(); + jdbcTemplate.query("SELECT * FROM APP_POLICY_GROUP", (rs) -> { + final String policyIdentifier = rs.getString("POLICY_IDENTIFIER"); + final String groupIdentifier = rs.getString("GROUP_IDENTIFIER"); + + final Set groupIdentifiers = policyToGroups.computeIfAbsent(policyIdentifier, (k) -> new HashSet<>()); + groupIdentifiers.add(groupIdentifier); + }); + + // convert the database model to the api model + final Set policies = new HashSet<>(); + + databasePolicies.forEach(p -> { + final Set userIdentifiers = policyToUsers.get(p.getIdentifier()); + final Set groupIdentifiers = policyToGroups.get(p.getIdentifier()); + policies.add(mapTopAccessPolicy(p, userIdentifiers, groupIdentifiers)); + }); + + return policies; + } + + @Override + public AccessPolicy getAccessPolicy(final String identifier) throws AuthorizationAccessException { + Validate.notBlank(identifier); + + final DatabaseAccessPolicy databaseAccessPolicy = getDatabaseAcessPolicy(identifier); + if (databaseAccessPolicy == null) { + return null; + } + + final Set userIdentifiers = getPolicyUsers(identifier); + final Set groupIdentifiers = getPolicyGroups(identifier); + return mapTopAccessPolicy(databaseAccessPolicy, userIdentifiers, groupIdentifiers); + } + + @Override + public AccessPolicy getAccessPolicy(final String resourceIdentifier, RequestAction action) throws AuthorizationAccessException { + Validate.notBlank(resourceIdentifier); + Validate.notNull(action); + + final String policySql = "SELECT * FROM APP_POLICY WHERE RESOURCE = ? AND ACTION = ?"; + final Object[] args = new Object[]{resourceIdentifier, action.toString()}; + final DatabaseAccessPolicy databaseAccessPolicy = queryForObject(policySql, args, new DatabaseAccessPolicyRowMapper()); + if (databaseAccessPolicy == null) { + return null; + } + + final Set userIdentifiers = getPolicyUsers(databaseAccessPolicy.getIdentifier()); + final Set groupIdentifiers = getPolicyGroups(databaseAccessPolicy.getIdentifier()); + return mapTopAccessPolicy(databaseAccessPolicy, userIdentifiers, groupIdentifiers); + } + + @Override + public AccessPolicy deleteAccessPolicy(final AccessPolicy accessPolicy) throws AuthorizationAccessException { + Validate.notNull(accessPolicy); + + final String sql = "DELETE FROM APP_POLICY WHERE IDENTIFIER = ?"; + final int rowsUpdated = jdbcTemplate.update(sql, accessPolicy.getIdentifier()); + if (rowsUpdated <= 0) { + return null; + } + + return accessPolicy; + } + + protected void createPolicyUserAndGroups(final AccessPolicy accessPolicy) { + if (accessPolicy.getUsers() != null) { + for (final String userIdentifier : accessPolicy.getUsers()) { + insertPolicyUser(accessPolicy.getIdentifier(), userIdentifier); + } + } + + if (accessPolicy.getGroups() != null) { + for (final String groupIdentifier : accessPolicy.getGroups()) { + insertPolicyGroup(accessPolicy.getIdentifier(), groupIdentifier); + } + } + } + + protected void insertPolicyGroup(final String policyIdentifier, final String groupIdentifier) { + final String policyGroupSql = "INSERT INTO APP_POLICY_GROUP(POLICY_IDENTIFIER, GROUP_IDENTIFIER) VALUES (?, ?)"; + jdbcTemplate.update(policyGroupSql, policyIdentifier, groupIdentifier); + } + + protected void insertPolicyUser(final String policyIdentifier, final String userIdentifier) { + final String policyUserSql = "INSERT INTO APP_POLICY_USER(POLICY_IDENTIFIER, USER_IDENTIFIER) VALUES (?, ?)"; + jdbcTemplate.update(policyUserSql, policyIdentifier, userIdentifier); + } + + protected DatabaseAccessPolicy getDatabaseAcessPolicy(final String policyIdentifier) { + final String sql = "SELECT * FROM APP_POLICY WHERE IDENTIFIER = ?"; + return queryForObject(sql, new Object[] {policyIdentifier}, new DatabaseAccessPolicyRowMapper()); + } + + protected Set getPolicyUsers(final String policyIdentifier) { + final String sql = "SELECT * FROM APP_POLICY_USER WHERE POLICY_IDENTIFIER = ?"; + + final Set userIdentifiers = new HashSet<>(); + jdbcTemplate.query(sql, new Object[]{policyIdentifier}, (rs) -> { + userIdentifiers.add(rs.getString("USER_IDENTIFIER")); + }); + return userIdentifiers; + } + + protected Set getPolicyGroups(final String policyIdentifier) { + final String sql = "SELECT * FROM APP_POLICY_GROUP WHERE POLICY_IDENTIFIER = ?"; + + final Set groupIdentifiers = new HashSet<>(); + jdbcTemplate.query(sql, new Object[]{policyIdentifier}, (rs) -> { + groupIdentifiers.add(rs.getString("GROUP_IDENTIFIER")); + }); + return groupIdentifiers; + } + + protected AccessPolicy mapTopAccessPolicy(final DatabaseAccessPolicy databaseAccessPolicy, final Set userIdentifiers, final Set groupIdentifiers) { + return new AccessPolicy.Builder() + .identifier(databaseAccessPolicy.getIdentifier()) + .resource(databaseAccessPolicy.getResource()) + .action(RequestAction.valueOfValue(databaseAccessPolicy.getAction())) + .addUsers(userIdentifiers) + .addGroups(groupIdentifiers) + .build(); + } + + protected void populateInitialPolicy(final User initialUser, final ResourceAndAction resourceAndAction) { + final String userIdentifier = initialUser.getIdentifier(); + final String resourceIdentifier = resourceAndAction.getResource().getIdentifier(); + final RequestAction action = resourceAndAction.getAction(); + + final AccessPolicy existingPolicy = getAccessPolicy(resourceIdentifier, action); + if (existingPolicy == null) { + // no policy exists for the given resource and action, so create a new one and add the given user + // we don't need to seed the identifier here since there is only a single external DB + final AccessPolicy accessPolicy = new AccessPolicy.Builder() + .identifierGenerateRandom() + .resource(resourceIdentifier) + .action(action) + .addUser(userIdentifier) + .build(); + + addAccessPolicy(accessPolicy); + } else { + // a policy already exists for the given resource and action, so just associate the user with that policy + if (existingPolicy.getUsers().contains(initialUser.getIdentifier())) { + LOGGER.debug("'{}' is already part of the policy for {} {}", + new Object[]{initialUser.getIdentity(), action.toString(), resourceIdentifier}); + } else { + LOGGER.debug("Adding '{}' to the policy for {} {}", + new Object[]{initialUser.getIdentity(), action.toString(), resourceIdentifier}); + insertPolicyUser(existingPolicy.getIdentifier(), userIdentifier); + } + } + } + + protected void populateInitialPolicy(final Group initialGroup, final ResourceAndAction resourceAndAction) { + final String resourceIdentifier = resourceAndAction.getResource().getIdentifier(); + final RequestAction action = resourceAndAction.getAction(); + + final AccessPolicy existingPolicy = getAccessPolicy(resourceIdentifier, action); + if (existingPolicy == null) { + // no policy exists for the given resource and action, so create a new one and add the given group + // we don't need to seed the identifier here since there is only a single external DB + final AccessPolicy accessPolicy = new AccessPolicy.Builder() + .identifierGenerateRandom() + .resource(resourceIdentifier) + .action(action) + .addGroup(initialGroup.getIdentifier()) + .build(); + + addAccessPolicy(accessPolicy); + } else { + // a policy already exists for the given resource and action, so just associate the group with that policy + insertPolicyGroup(existingPolicy.getIdentifier(), initialGroup.getIdentifier()); + } + } + + //-- util methods + + protected T queryForObject(final String sql, final Object[] args, final RowMapper rowMapper) { + try { + return jdbcTemplate.queryForObject(sql, args, rowMapper); + } catch(final EmptyResultDataAccessException e) { + return null; + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/database/DatabaseUserGroupProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/database/DatabaseUserGroupProvider.java new file mode 100644 index 0000000000..c44a553266 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/database/DatabaseUserGroupProvider.java @@ -0,0 +1,387 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.database; + +import org.apache.commons.lang3.Validate; +import org.apache.nifi.registry.security.authorization.AuthorizerConfigurationContext; +import org.apache.nifi.registry.security.authorization.ConfigurableUserGroupProvider; +import org.apache.nifi.registry.security.authorization.Group; +import org.apache.nifi.registry.security.authorization.User; +import org.apache.nifi.registry.security.authorization.UserAndGroups; +import org.apache.nifi.registry.security.authorization.UserGroupProviderInitializationContext; +import org.apache.nifi.registry.security.authorization.annotation.AuthorizerContext; +import org.apache.nifi.registry.security.authorization.database.entity.DatabaseGroup; +import org.apache.nifi.registry.security.authorization.database.entity.DatabaseUser; +import org.apache.nifi.registry.security.authorization.database.mapper.DatabaseGroupRowMapper; +import org.apache.nifi.registry.security.authorization.database.mapper.DatabaseUserRowMapper; +import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException; +import org.apache.nifi.registry.security.authorization.exception.UninheritableAuthorizationsException; +import org.apache.nifi.registry.security.authorization.util.UserGroupProviderUtils; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException; +import org.apache.nifi.registry.security.identity.IdentityMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; + +import javax.sql.DataSource; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Implementation of {@link org.apache.nifi.registry.security.authorization.ConfigurableUserGroupProvider} backed by a relational database. + */ +public class DatabaseUserGroupProvider implements ConfigurableUserGroupProvider { + + private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseUserGroupProvider.class); + + private DataSource dataSource; + private IdentityMapper identityMapper; + + private JdbcTemplate jdbcTemplate; + + @AuthorizerContext + public void setDataSource(final DataSource dataSource) { + this.dataSource = dataSource; + } + + @AuthorizerContext + public void setIdentityMapper(final IdentityMapper identityMapper) { + this.identityMapper = identityMapper; + } + + @Override + public void initialize(final UserGroupProviderInitializationContext initializationContext) throws SecurityProviderCreationException { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + @Override + public void onConfigured(final AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException { + final Set initialUserIdentities = UserGroupProviderUtils.getInitialUserIdentities(configurationContext, identityMapper); + + for (final String initialUserIdentity : initialUserIdentities) { + final User existingUser = getUserByIdentity(initialUserIdentity); + if (existingUser == null) { + final User initialUser = new User.Builder() + .identifierGenerateFromSeed(initialUserIdentity) + .identity(initialUserIdentity) + .build(); + addUser(initialUser); + LOGGER.info("Created initial user with identity {}", new Object[]{initialUserIdentity}); + } else { + LOGGER.debug("User already exists with identity {}", new Object[]{initialUserIdentity}); + } + } + } + + @Override + public void preDestruction() throws SecurityProviderDestructionException { + + } + + //-- fingerprint methods + + @Override + public String getFingerprint() throws AuthorizationAccessException { + throw new UnsupportedOperationException("Fingerprinting is not supported by this provider"); + } + + @Override + public void inheritFingerprint(final String fingerprint) throws AuthorizationAccessException { + throw new UnsupportedOperationException("Fingerprinting is not supported by this provider"); + } + + @Override + public void checkInheritability(final String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException { + throw new UnsupportedOperationException("Fingerprinting is not supported by this provider"); + } + + //-- User CRUD + + @Override + public User addUser(final User user) throws AuthorizationAccessException { + Validate.notNull(user); + final String sql = "INSERT INTO UGP_USER(IDENTIFIER, IDENTITY) VALUES (?, ?)"; + jdbcTemplate.update(sql, new Object[] {user.getIdentifier(), user.getIdentity()}); + return user; + } + + @Override + public User updateUser(final User user) throws AuthorizationAccessException { + Validate.notNull(user); + + // update the user identity + final String sql = "UPDATE UGP_USER SET IDENTITY = ? WHERE IDENTIFIER = ?"; + final int updated = jdbcTemplate.update(sql, user.getIdentity(), user.getIdentifier()); + + // if no rows were updated then there is no user with the given identifier, so return null + if (updated <= 0) { + return null; + } + + return user; + } + + @Override + public Set getUsers() throws AuthorizationAccessException { + final String sql = "SELECT * FROM UGP_USER"; + final List databaseUsers = jdbcTemplate.query(sql, new DatabaseUserRowMapper()); + + final Set users = new HashSet<>(); + databaseUsers.forEach(u -> { + users.add(mapToUser(u)); + }); + return users; + } + + @Override + public User getUser(final String identifier) throws AuthorizationAccessException { + Validate.notBlank(identifier); + + final DatabaseUser databaseUser = getDatabaseUser(identifier); + if (databaseUser == null) { + return null; + } + + return mapToUser(databaseUser); + } + + @Override + public User getUserByIdentity(final String identity) throws AuthorizationAccessException { + Validate.notBlank(identity); + + final String sql = "SELECT * FROM UGP_USER WHERE IDENTITY = ?"; + final DatabaseUser databaseUser = queryForObject(sql, new Object[] {identity}, new DatabaseUserRowMapper()); + if (databaseUser == null) { + return null; + } + + return mapToUser(databaseUser); + } + + @Override + public UserAndGroups getUserAndGroups(final String userIdentity) throws AuthorizationAccessException { + Validate.notBlank(userIdentity); + + // retrieve the user + final User user = getUserByIdentity(userIdentity); + + // if the user exists, then retrieve the groups for the user + final Set groups; + if (user == null) { + groups = null; + } else { + final String userGroupSql = + "SELECT " + + "G.IDENTIFIER AS IDENTIFIER, " + + "G.IDENTITY AS IDENTITY " + + "FROM " + + "UGP_GROUP AS G, " + + "UGP_USER_GROUP AS UG " + + "WHERE " + + "G.IDENTIFIER = UG.GROUP_IDENTIFIER AND " + + "UG.USER_IDENTIFIER = ?"; + + final Object[] args = {user.getIdentifier()}; + final List databaseGroups = jdbcTemplate.query(userGroupSql, args, new DatabaseGroupRowMapper()); + + groups = new HashSet<>(); + databaseGroups.forEach(g -> { + final Set userIdentifiers = getUserIdentifiers(g.getIdentifier()); + groups.add(mapToGroup(g, userIdentifiers)); + }); + } + + return new UserAndGroups() { + @Override + public User getUser() { + return user; + } + + @Override + public Set getGroups() { + return groups; + } + }; + } + + @Override + public User deleteUser(final User user) throws AuthorizationAccessException { + Validate.notNull(user); + + final String deleteFromUserGroupSql = "DELETE FROM UGP_USER_GROUP WHERE USER_IDENTIFIER = ?"; + jdbcTemplate.update(deleteFromUserGroupSql, user.getIdentifier()); + + final String deleteFromUserSql = "DELETE FROM UGP_USER WHERE IDENTIFIER = ?"; + final int rowsDeletedFromUser = jdbcTemplate.update(deleteFromUserSql, user.getIdentifier()); + if (rowsDeletedFromUser <= 0) { + return null; + } + + return user; + } + + private DatabaseUser getDatabaseUser(final String userIdentifier) { + final String sql = "SELECT * FROM UGP_USER WHERE IDENTIFIER = ?"; + return queryForObject(sql, new Object[] {userIdentifier}, new DatabaseUserRowMapper()); + } + + private User mapToUser(final DatabaseUser databaseUser) { + return new User.Builder() + .identifier(databaseUser.getIdentifier()) + .identity(databaseUser.getIdentity()) + .build(); + } + + //-- Group CRUD + + @Override + public Group addGroup(final Group group) throws AuthorizationAccessException { + Validate.notNull(group); + + // insert to the group table... + final String groupSql = "INSERT INTO UGP_GROUP(IDENTIFIER, IDENTITY) VALUES (?, ?)"; + jdbcTemplate.update(groupSql, group.getIdentifier(), group.getName()); + + // insert to the user-group table... + createUserGroups(group); + + return group; + } + + @Override + public Group updateGroup(final Group group) throws AuthorizationAccessException { + Validate.notNull(group); + + // update the group identity + final String updateGroupSql = "UPDATE UGP_GROUP SET IDENTITY = ? WHERE IDENTIFIER = ?"; + final int updated = jdbcTemplate.update(updateGroupSql, group.getName(), group.getIdentifier()); + + // if no rows were updated then a group does not exist for the given identifier, so return null + if (updated <= 0) { + return null; + } + + // delete any user-group associations + final String deleteUserGroups = "DELETE FROM UGP_USER_GROUP WHERE GROUP_IDENTIFIER = ?"; + jdbcTemplate.update(deleteUserGroups, group.getIdentifier()); + + // re-create any user-group associations + createUserGroups(group); + + return group; + } + + @Override + public Set getGroups() throws AuthorizationAccessException { + // retrieve all the groups + final String sql = "SELECT * FROM UGP_GROUP"; + final List databaseGroups = jdbcTemplate.query(sql, new DatabaseGroupRowMapper()); + + // retrieve all the users in the groups, mapped by group id + final Map> groupToUsers = new HashMap<>(); + jdbcTemplate.query("SELECT * FROM UGP_USER_GROUP", (rs) -> { + final String groupIdentifier = rs.getString("GROUP_IDENTIFIER"); + final String userIdentifier = rs.getString("USER_IDENTIFIER"); + + final Set userIdentifiers = groupToUsers.computeIfAbsent(groupIdentifier, (k) -> new HashSet<>()); + userIdentifiers.add(userIdentifier); + }); + + // convert from database model to api model + final Set groups = new HashSet<>(); + databaseGroups.forEach(g -> { + groups.add(mapToGroup(g, groupToUsers.get(g.getIdentifier()))); + }); + return groups; + } + + @Override + public Group getGroup(final String groupIdentifier) throws AuthorizationAccessException { + Validate.notBlank(groupIdentifier); + + final DatabaseGroup databaseGroup = getDatabaseGroup(groupIdentifier); + if (databaseGroup == null) { + return null; + } + + final Set userIdentifiers = getUserIdentifiers(groupIdentifier); + return mapToGroup(databaseGroup, userIdentifiers); + } + + @Override + public Group deleteGroup(final Group group) throws AuthorizationAccessException { + Validate.notNull(group); + + final String sql = "DELETE FROM UGP_GROUP WHERE IDENTIFIER = ?"; + final int rowsUpdated = jdbcTemplate.update(sql, group.getIdentifier()); + if (rowsUpdated <= 0) { + return null; + } + + return group; + } + + private void createUserGroups(final Group group) { + if (group.getUsers() != null) { + for (final String userIdentifier : group.getUsers()) { + final String userGroupSql = "INSERT INTO UGP_USER_GROUP (USER_IDENTIFIER, GROUP_IDENTIFIER) VALUES (?, ?)"; + jdbcTemplate.update(userGroupSql, userIdentifier, group.getIdentifier()); + } + } + } + + private DatabaseGroup getDatabaseGroup(final String groupIdentifier) { + final String sql = "SELECT * FROM UGP_GROUP WHERE IDENTIFIER = ?"; + return queryForObject(sql, new Object[] {groupIdentifier}, new DatabaseGroupRowMapper()); + } + + private Set getUserIdentifiers(final String groupIdentifier) { + final String sql = "SELECT * FROM UGP_USER_GROUP WHERE GROUP_IDENTIFIER = ?"; + + final Set userIdentifiers = new HashSet<>(); + jdbcTemplate.query(sql, new Object[]{groupIdentifier}, (rs) -> { + userIdentifiers.add(rs.getString("USER_IDENTIFIER")); + }); + + return userIdentifiers; + } + + private Group mapToGroup(final DatabaseGroup databaseGroup, final Set userIdentifiers) { + return new Group.Builder() + .identifier(databaseGroup.getIdentifier()) + .name(databaseGroup.getIdentity()) + .addUsers(userIdentifiers == null ? Collections.emptySet() : userIdentifiers) + .build(); + } + + //-- util methods + + private T queryForObject(final String sql, final Object[] args, final RowMapper rowMapper) { + try { + return jdbcTemplate.queryForObject(sql, args, rowMapper); + } catch(final EmptyResultDataAccessException e) { + return null; + } + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/database/entity/DatabaseAccessPolicy.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/database/entity/DatabaseAccessPolicy.java new file mode 100644 index 0000000000..310d18ba0e --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/database/entity/DatabaseAccessPolicy.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.database.entity; + +public class DatabaseAccessPolicy { + + private String identifier; + + private String resource; + + private String action; + + public String getIdentifier() { + return identifier; + } + + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public String getAction() { + return action; + } + + public void setAction(String action) { + this.action = action; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/database/entity/DatabaseGroup.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/database/entity/DatabaseGroup.java new file mode 100644 index 0000000000..ccdc892c0b --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/database/entity/DatabaseGroup.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.database.entity; + +public class DatabaseGroup { + + private String identifier; + + private String identity; + + public String getIdentifier() { + return identifier; + } + + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + + public String getIdentity() { + return identity; + } + + public void setIdentity(String identity) { + this.identity = identity; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/database/entity/DatabaseUser.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/database/entity/DatabaseUser.java new file mode 100644 index 0000000000..cca3ab9452 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/database/entity/DatabaseUser.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.database.entity; + +public class DatabaseUser { + + private String identifier; + + private String identity; + + public String getIdentifier() { + return identifier; + } + + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + + public String getIdentity() { + return identity; + } + + public void setIdentity(String identity) { + this.identity = identity; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/database/mapper/DatabaseAccessPolicyRowMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/database/mapper/DatabaseAccessPolicyRowMapper.java new file mode 100644 index 0000000000..d795f597e8 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/database/mapper/DatabaseAccessPolicyRowMapper.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.database.mapper; + +import org.apache.nifi.registry.security.authorization.database.entity.DatabaseAccessPolicy; +import org.springframework.jdbc.core.RowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class DatabaseAccessPolicyRowMapper implements RowMapper { + + @Override + public DatabaseAccessPolicy mapRow(final ResultSet rs, final int i) throws SQLException { + final DatabaseAccessPolicy policy = new DatabaseAccessPolicy(); + policy.setIdentifier(rs.getString("IDENTIFIER")); + policy.setResource(rs.getString("RESOURCE")); + policy.setAction(rs.getString("ACTION")); + return policy; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/database/mapper/DatabaseGroupRowMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/database/mapper/DatabaseGroupRowMapper.java new file mode 100644 index 0000000000..4fa473e6a1 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/database/mapper/DatabaseGroupRowMapper.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.database.mapper; + +import org.apache.nifi.registry.security.authorization.database.entity.DatabaseGroup; +import org.springframework.jdbc.core.RowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class DatabaseGroupRowMapper implements RowMapper { + + @Override + public DatabaseGroup mapRow(final ResultSet rs, final int rowNum) throws SQLException { + final DatabaseGroup group = new DatabaseGroup(); + group.setIdentifier(rs.getString("IDENTIFIER")); + group.setIdentity(rs.getString("IDENTITY")); + return group; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/database/mapper/DatabaseUserRowMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/database/mapper/DatabaseUserRowMapper.java new file mode 100644 index 0000000000..532937cf66 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/database/mapper/DatabaseUserRowMapper.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.database.mapper; + +import org.apache.nifi.registry.security.authorization.database.entity.DatabaseUser; +import org.springframework.jdbc.core.RowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class DatabaseUserRowMapper implements RowMapper { + + @Override + public DatabaseUser mapRow(final ResultSet rs, final int rowNum) throws SQLException { + final DatabaseUser user = new DatabaseUser(); + user.setIdentifier(rs.getString("IDENTIFIER")); + user.setIdentity(rs.getString("IDENTITY")); + return user; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/AuthorizationsHolder.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/AuthorizationsHolder.java new file mode 100644 index 0000000000..6e84f492cf --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/AuthorizationsHolder.java @@ -0,0 +1,187 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.file; + + +import org.apache.nifi.registry.security.authorization.file.generated.Authorizations; +import org.apache.nifi.registry.security.authorization.file.generated.Policies; +import org.apache.nifi.registry.security.authorization.AccessPolicy; +import org.apache.nifi.registry.security.authorization.RequestAction; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * A holder to provide atomic access to data structures. + */ +public class AuthorizationsHolder { + + private final Authorizations authorizations; + + private final Set allPolicies; + private final Map> policiesByResource; + private final Map policiesById; + + /** + * Creates a new holder and populates all convenience authorizations data structures. + * + * @param authorizations the current authorizations instance + */ + public AuthorizationsHolder(final Authorizations authorizations) { + this.authorizations = authorizations; + + // load all access policies + final Policies policies = authorizations.getPolicies(); + final Set allPolicies = Collections.unmodifiableSet(createAccessPolicies(policies)); + + // create a convenience map from resource id to policies + final Map> policiesByResourceMap = Collections.unmodifiableMap(createResourcePolicyMap(allPolicies)); + + // create a convenience map from policy id to policy + final Map policiesByIdMap = Collections.unmodifiableMap(createPoliciesByIdMap(allPolicies)); + + // set all the holders + this.allPolicies = allPolicies; + this.policiesByResource = policiesByResourceMap; + this.policiesById = policiesByIdMap; + } + + /** + * Creates AccessPolicies from the JAXB Policies. + * + * @param policies the JAXB Policies element + * @return a set of AccessPolicies corresponding to the provided Resources + */ + private Set createAccessPolicies(org.apache.nifi.registry.security.authorization.file.generated.Policies policies) { + Set allPolicies = new HashSet<>(); + if (policies == null || policies.getPolicy() == null) { + return allPolicies; + } + + // load the new authorizations + for (final org.apache.nifi.registry.security.authorization.file.generated.Policy policy : policies.getPolicy()) { + final String policyIdentifier = policy.getIdentifier(); + final String resourceIdentifier = policy.getResource(); + + // start a new builder and set the policy and resource identifiers + final AccessPolicy.Builder builder = new AccessPolicy.Builder() + .identifier(policyIdentifier) + .resource(resourceIdentifier); + + // add each user identifier + for (org.apache.nifi.registry.security.authorization.file.generated.Policy.User user : policy.getUser()) { + builder.addUser(user.getIdentifier()); + } + + // add each group identifier + for (org.apache.nifi.registry.security.authorization.file.generated.Policy.Group group : policy.getGroup()) { + builder.addGroup(group.getIdentifier()); + } + + // add the appropriate request actions + final String authorizationCode = policy.getAction(); + if (authorizationCode.equals(FileAccessPolicyProvider.READ_CODE)) { + builder.action(RequestAction.READ); + } else if (authorizationCode.equals(FileAccessPolicyProvider.WRITE_CODE)){ + builder.action(RequestAction.WRITE); + } else if (authorizationCode.equals(FileAccessPolicyProvider.DELETE_CODE)){ + builder.action(RequestAction.DELETE); + } else { + throw new IllegalStateException("Unknown Policy Action: " + authorizationCode); + } + + // build the policy and add it to the map + allPolicies.add(builder.build()); + } + + return allPolicies; + } + + /** + * Creates a map from resource identifier to the set of policies for the given resource. + * + * @param allPolicies the set of all policies + * @return a map from resource identifier to policies + */ + private Map> createResourcePolicyMap(final Set allPolicies) { + Map> resourcePolicies = new HashMap<>(); + + for (AccessPolicy policy : allPolicies) { + Set policies = resourcePolicies.get(policy.getResource()); + if (policies == null) { + policies = new HashSet<>(); + resourcePolicies.put(policy.getResource(), policies); + } + policies.add(policy); + } + + return resourcePolicies; + } + + /** + * Creates a Map from policy identifier to AccessPolicy. + * + * @param policies the set of all access policies + * @return the Map from policy identifier to AccessPolicy + */ + private Map createPoliciesByIdMap(final Set policies) { + Map policyMap = new HashMap<>(); + for (AccessPolicy policy : policies) { + policyMap.put(policy.getIdentifier(), policy); + } + return policyMap; + } + + public Authorizations getAuthorizations() { + return authorizations; + } + + public Set getAllPolicies() { + return allPolicies; + } + + public Map> getPoliciesByResource() { + return policiesByResource; + } + + public Map getPoliciesById() { + return policiesById; + } + + public AccessPolicy getAccessPolicy(final String resourceIdentifier, final RequestAction action) { + if (resourceIdentifier == null) { + throw new IllegalArgumentException("Resource Identifier cannot be null"); + } + + final Set resourcePolicies = policiesByResource.get(resourceIdentifier); + if (resourcePolicies == null) { + return null; + } + + for (AccessPolicy accessPolicy : resourcePolicies) { + if (accessPolicy.getAction() == action) { + return accessPolicy; + } + } + + return null; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileAccessPolicyProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileAccessPolicyProvider.java new file mode 100644 index 0000000000..31e77a1835 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileAccessPolicyProvider.java @@ -0,0 +1,670 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.file; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.security.authorization.AbstractConfigurableAccessPolicyProvider; +import org.apache.nifi.registry.security.authorization.AccessPolicy; +import org.apache.nifi.registry.security.authorization.AccessPolicyProviderInitializationContext; +import org.apache.nifi.registry.security.authorization.AuthorizerConfigurationContext; +import org.apache.nifi.registry.security.authorization.Group; +import org.apache.nifi.registry.security.authorization.RequestAction; +import org.apache.nifi.registry.security.authorization.User; +import org.apache.nifi.registry.security.authorization.annotation.AuthorizerContext; +import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException; +import org.apache.nifi.registry.security.authorization.exception.UninheritableAuthorizationsException; +import org.apache.nifi.registry.security.authorization.file.generated.Authorizations; +import org.apache.nifi.registry.security.authorization.file.generated.Policies; +import org.apache.nifi.registry.security.authorization.file.generated.Policy; +import org.apache.nifi.registry.security.authorization.util.AccessPolicyProviderUtils; +import org.apache.nifi.registry.security.authorization.util.InitialPolicies; +import org.apache.nifi.registry.security.authorization.util.ResourceAndAction; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException; +import org.apache.nifi.registry.security.identity.IdentityMapper; +import org.apache.nifi.registry.util.PropertyValue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import javax.xml.XMLConstants; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBElement; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; + +public class FileAccessPolicyProvider extends AbstractConfigurableAccessPolicyProvider { + + private static final Logger logger = LoggerFactory.getLogger(FileAccessPolicyProvider.class); + + private static final String AUTHORIZATIONS_XSD = "/authorizations.xsd"; + private static final String JAXB_AUTHORIZATIONS_PATH = "org.apache.nifi.registry.security.authorization.file.generated"; + + private static final JAXBContext JAXB_AUTHORIZATIONS_CONTEXT = initializeJaxbContext(JAXB_AUTHORIZATIONS_PATH); + + /** + * Load the JAXBContext. + */ + private static JAXBContext initializeJaxbContext(final String contextPath) { + try { + return JAXBContext.newInstance(contextPath, FileAuthorizer.class.getClassLoader()); + } catch (JAXBException e) { + throw new RuntimeException("Unable to create JAXBContext."); + } + } + + private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance(); + private static final XMLOutputFactory XML_OUTPUT_FACTORY = XMLOutputFactory.newInstance(); + + private static final String POLICY_ELEMENT = "policy"; + private static final String POLICY_USER_ELEMENT = "policyUser"; + private static final String POLICY_GROUP_ELEMENT = "policyGroup"; + private static final String IDENTIFIER_ATTR = "identifier"; + private static final String RESOURCE_ATTR = "resource"; + private static final String ACTIONS_ATTR = "actions"; + + /* These codes must match the enumeration values set in authorizations.xsd */ + static final String READ_CODE = "R"; + static final String WRITE_CODE = "W"; + static final String DELETE_CODE = "D"; + + static final String PROP_AUTHORIZATIONS_FILE = "Authorizations File"; + + private IdentityMapper identityMapper; + private Schema authorizationsSchema; + private File authorizationsFile; + private String initialAdminIdentity; + private Set nifiIdentities; + private String nifiGroupName; + + private final AtomicReference authorizationsHolder = new AtomicReference<>(); + + @Override + public void doInitialize(final AccessPolicyProviderInitializationContext initializationContext) throws SecurityProviderCreationException { + try { + final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + authorizationsSchema = schemaFactory.newSchema(FileAuthorizer.class.getResource(AUTHORIZATIONS_XSD)); + } catch (Exception e) { + throw new SecurityProviderCreationException(e); + } + } + + @Override + public void doOnConfigured(final AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException { + try { + final PropertyValue authorizationsPath = configurationContext.getProperty(PROP_AUTHORIZATIONS_FILE); + if (StringUtils.isBlank(authorizationsPath.getValue())) { + throw new SecurityProviderCreationException("The authorizations file must be specified."); + } + + // get the authorizations file and ensure it exists + authorizationsFile = new File(authorizationsPath.getValue()); + if (!authorizationsFile.exists()) { + logger.info("Creating new authorizations file at {}", new Object[] {authorizationsFile.getAbsolutePath()}); + saveAuthorizations(new Authorizations()); + } + + // get the value of the initial admin identity + initialAdminIdentity = AccessPolicyProviderUtils.getInitialAdminIdentity(configurationContext, identityMapper); + + // extract any nifi identities + nifiIdentities = AccessPolicyProviderUtils.getNiFiIdentities(configurationContext, identityMapper); + + // extract the group for nifi identities, if one exists + nifiGroupName = AccessPolicyProviderUtils.getNiFiGroupName(configurationContext, identityMapper); + + // load the authorizations + load(); + + logger.info(String.format("Authorizations file loaded at %s", new Date().toString())); + } catch (JAXBException | SAXException e) { + throw new SecurityProviderCreationException(e); + } + } + + @Override + public Set getAccessPolicies() throws AuthorizationAccessException { + return authorizationsHolder.get().getAllPolicies(); + } + + @Override + public synchronized AccessPolicy addAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException { + if (accessPolicy == null) { + throw new IllegalArgumentException("AccessPolicy cannot be null"); + } + + // create the new JAXB Policy + final Policy policy = createJAXBPolicy(accessPolicy); + + // add the new Policy to the top-level list of policies + final AuthorizationsHolder holder = authorizationsHolder.get(); + final Authorizations authorizations = holder.getAuthorizations(); + authorizations.getPolicies().getPolicy().add(policy); + + saveAndRefreshHolder(authorizations); + + return authorizationsHolder.get().getPoliciesById().get(accessPolicy.getIdentifier()); + } + + @Override + public AccessPolicy getAccessPolicy(String identifier) throws AuthorizationAccessException { + if (identifier == null) { + return null; + } + + final AuthorizationsHolder holder = authorizationsHolder.get(); + return holder.getPoliciesById().get(identifier); + } + + @Override + public AccessPolicy getAccessPolicy(String resourceIdentifier, RequestAction action) throws AuthorizationAccessException { + return authorizationsHolder.get().getAccessPolicy(resourceIdentifier, action); + } + + @Override + public synchronized AccessPolicy updateAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException { + if (accessPolicy == null) { + throw new IllegalArgumentException("AccessPolicy cannot be null"); + } + + final AuthorizationsHolder holder = this.authorizationsHolder.get(); + final Authorizations authorizations = holder.getAuthorizations(); + + // try to find an existing Authorization that matches the policy id + Policy updatePolicy = null; + for (Policy policy : authorizations.getPolicies().getPolicy()) { + if (policy.getIdentifier().equals(accessPolicy.getIdentifier())) { + updatePolicy = policy; + break; + } + } + + // no matching Policy so return null + if (updatePolicy == null) { + return null; + } + + // update the Policy, save, reload, and return + transferUsersAndGroups(accessPolicy, updatePolicy); + saveAndRefreshHolder(authorizations); + + return this.authorizationsHolder.get().getPoliciesById().get(accessPolicy.getIdentifier()); + } + + @Override + public synchronized AccessPolicy deleteAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException { + if (accessPolicy == null) { + throw new IllegalArgumentException("AccessPolicy cannot be null"); + } + + return deleteAccessPolicy(accessPolicy.getIdentifier()); + } + + private synchronized AccessPolicy deleteAccessPolicy(String accessPolicyIdentifer) throws AuthorizationAccessException { + if (accessPolicyIdentifer == null) { + throw new IllegalArgumentException("Access policy identifier cannot be null"); + } + + final AuthorizationsHolder holder = this.authorizationsHolder.get(); + AccessPolicy deletedPolicy = holder.getPoliciesById().get(accessPolicyIdentifer); + if (deletedPolicy == null) { + return null; + } + + // find the matching Policy and remove it + final Authorizations authorizations = holder.getAuthorizations(); + Iterator policyIter = authorizations.getPolicies().getPolicy().iterator(); + while (policyIter.hasNext()) { + final Policy policy = policyIter.next(); + if (policy.getIdentifier().equals(accessPolicyIdentifer)) { + policyIter.remove(); + break; + } + } + + saveAndRefreshHolder(authorizations); + return deletedPolicy; + } + + AuthorizationsHolder getAuthorizationsHolder() { + return authorizationsHolder.get(); + } + + @AuthorizerContext + public void setIdentityMapper(final IdentityMapper identityMapper) { + this.identityMapper = identityMapper; + } + + @Override + public synchronized void inheritFingerprint(String fingerprint) throws AuthorizationAccessException { + parsePolicies(fingerprint).forEach(policy -> addAccessPolicy(policy)); + } + + @Override + public void checkInheritability(String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException { + try { + // ensure we can understand the proposed fingerprint + parsePolicies(proposedFingerprint); + } catch (final AuthorizationAccessException e) { + throw new UninheritableAuthorizationsException("Unable to parse the proposed fingerprint: " + e); + } + + // ensure we are in a proper state to inherit the fingerprint + if (!getAccessPolicies().isEmpty()) { + throw new UninheritableAuthorizationsException("Proposed fingerprint is not inheritable because the current access policies is not empty."); + } + } + + @Override + public String getFingerprint() throws AuthorizationAccessException { + final List policies = new ArrayList<>(getAccessPolicies()); + Collections.sort(policies, Comparator.comparing(AccessPolicy::getIdentifier)); + + XMLStreamWriter writer = null; + final StringWriter out = new StringWriter(); + try { + writer = XML_OUTPUT_FACTORY.createXMLStreamWriter(out); + writer.writeStartDocument(); + writer.writeStartElement("accessPolicies"); + + for (AccessPolicy policy : policies) { + writePolicy(writer, policy); + } + + writer.writeEndElement(); + writer.writeEndDocument(); + writer.flush(); + } catch (XMLStreamException e) { + throw new AuthorizationAccessException("Unable to generate fingerprint", e); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (XMLStreamException e) { + // nothing to do here + } + } + } + + return out.toString(); + } + + private List parsePolicies(final String fingerprint) { + final List policies = new ArrayList<>(); + + final byte[] fingerprintBytes = fingerprint.getBytes(StandardCharsets.UTF_8); + try (final ByteArrayInputStream in = new ByteArrayInputStream(fingerprintBytes)) { + final DocumentBuilder docBuilder = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder(); + final Document document = docBuilder.parse(in); + final Element rootElement = document.getDocumentElement(); + + // parse all the policies and add them to the current access policy provider + NodeList policyNodes = rootElement.getElementsByTagName(POLICY_ELEMENT); + for (int i = 0; i < policyNodes.getLength(); i++) { + Node policyNode = policyNodes.item(i); + policies.add(parsePolicy((Element) policyNode)); + } + } catch (SAXException | ParserConfigurationException | IOException e) { + throw new AuthorizationAccessException("Unable to parse fingerprint", e); + } + + return policies; + } + + private AccessPolicy parsePolicy(final Element element) { + final AccessPolicy.Builder builder = new AccessPolicy.Builder() + .identifier(element.getAttribute(IDENTIFIER_ATTR)) + .resource(element.getAttribute(RESOURCE_ATTR)); + + final String actions = element.getAttribute(ACTIONS_ATTR); + if (actions.equals(RequestAction.READ.name())) { + builder.action(RequestAction.READ); + } else if (actions.equals(RequestAction.WRITE.name())) { + builder.action(RequestAction.WRITE); + } else if (actions.equals(RequestAction.DELETE.name())) { + builder.action(RequestAction.DELETE); + } else { + throw new IllegalStateException("Unknown Policy Action: " + actions); + } + + NodeList policyUsers = element.getElementsByTagName(POLICY_USER_ELEMENT); + for (int i=0; i < policyUsers.getLength(); i++) { + Element policyUserNode = (Element) policyUsers.item(i); + builder.addUser(policyUserNode.getAttribute(IDENTIFIER_ATTR)); + } + + NodeList policyGroups = element.getElementsByTagName(POLICY_GROUP_ELEMENT); + for (int i=0; i < policyGroups.getLength(); i++) { + Element policyGroupNode = (Element) policyGroups.item(i); + builder.addGroup(policyGroupNode.getAttribute(IDENTIFIER_ATTR)); + } + + return builder.build(); + } + + private void writePolicy(final XMLStreamWriter writer, final AccessPolicy policy) throws XMLStreamException { + // sort the users for the policy + List policyUsers = new ArrayList<>(policy.getUsers()); + Collections.sort(policyUsers); + + // sort the groups for this policy + List policyGroups = new ArrayList<>(policy.getGroups()); + Collections.sort(policyGroups); + + writer.writeStartElement(POLICY_ELEMENT); + writer.writeAttribute(IDENTIFIER_ATTR, policy.getIdentifier()); + writer.writeAttribute(RESOURCE_ATTR, policy.getResource()); + writer.writeAttribute(ACTIONS_ATTR, policy.getAction().name()); + + for (String policyUser : policyUsers) { + writer.writeStartElement(POLICY_USER_ELEMENT); + writer.writeAttribute(IDENTIFIER_ATTR, policyUser); + writer.writeEndElement(); + } + + for (String policyGroup : policyGroups) { + writer.writeStartElement(POLICY_GROUP_ELEMENT); + writer.writeAttribute(IDENTIFIER_ATTR, policyGroup); + writer.writeEndElement(); + } + + writer.writeEndElement(); + } + + /** + * Loads the authorizations file and populates the AuthorizationsHolder, only called during start-up. + * + * @throws JAXBException Unable to reload the authorized users file + */ + private synchronized void load() throws JAXBException, SAXException { + // attempt to unmarshal + final Authorizations authorizations = unmarshallAuthorizations(); + if (authorizations.getPolicies() == null) { + authorizations.setPolicies(new Policies()); + } + + final AuthorizationsHolder authorizationsHolder = new AuthorizationsHolder(authorizations); + final boolean emptyAuthorizations = authorizationsHolder.getAllPolicies().isEmpty(); + final boolean hasInitialAdminIdentity = (initialAdminIdentity != null && !StringUtils.isBlank(initialAdminIdentity)); + final boolean hasNiFiIdentities = (nifiIdentities != null && !nifiIdentities.isEmpty()); + + // if we are starting fresh then we might need to populate an initial admin + if (emptyAuthorizations) { + if (hasInitialAdminIdentity) { + logger.info("Populating authorizations for Initial Admin: '" + initialAdminIdentity + "'"); + populateInitialAdmin(authorizations); + } + + if (hasNiFiIdentities) { + logger.info("Populating authorizations for NiFi identities: [{}]", StringUtils.join(nifiIdentities, ";")); + populateNiFiIdentities(authorizations); + } + + if (!StringUtils.isEmpty(nifiGroupName)) { + logger.info("Populating authorizations for NiFi identity group: [{}]", nifiGroupName); + populateNiFiGroup(authorizations); + } + + saveAndRefreshHolder(authorizations); + } else { + this.authorizationsHolder.set(authorizationsHolder); + } + } + + private void saveAuthorizations(final Authorizations authorizations) throws JAXBException { + final Marshaller marshaller = JAXB_AUTHORIZATIONS_CONTEXT.createMarshaller(); + marshaller.setSchema(authorizationsSchema); + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); + marshaller.marshal(authorizations, authorizationsFile); + } + + private Authorizations unmarshallAuthorizations() throws JAXBException { + final Unmarshaller unmarshaller = JAXB_AUTHORIZATIONS_CONTEXT.createUnmarshaller(); + unmarshaller.setSchema(authorizationsSchema); + + final JAXBElement element = unmarshaller.unmarshal(new StreamSource(authorizationsFile), Authorizations.class); + return element.getValue(); + } + + /** + * Creates the initial admin user and sets policies managing buckets, users, and policies. + */ + private void populateInitialAdmin(final Authorizations authorizations) { + final User initialAdmin = getUserGroupProvider().getUserByIdentity(initialAdminIdentity); + if (initialAdmin == null) { + throw new SecurityProviderCreationException("Unable to locate initial admin " + initialAdminIdentity + " to seed policies"); + } + + for (final ResourceAndAction resourceAction : InitialPolicies.ADMIN_POLICIES) { + final String resource = resourceAction.getResource().getIdentifier(); + final String actionCode = getActionCode(resourceAction.getAction()); + addUserToAccessPolicy(authorizations, resource, initialAdmin.getIdentifier(), actionCode); + } + } + + /** + * Creates a user for each NiFi client and gives each one write permission to /proxy. + * + * @param authorizations the overall authorizations + */ + private void populateNiFiIdentities(final Authorizations authorizations) { + for (String nifiIdentity : nifiIdentities) { + final User nifiUser = getUserGroupProvider().getUserByIdentity(nifiIdentity); + if (nifiUser == null) { + throw new SecurityProviderCreationException("Unable to locate NiFi identities'" + nifiIdentity + "' to seed policies."); + } + + // grant access to the resources needed for initial nifi identities + for (final ResourceAndAction resourceAction : InitialPolicies.NIFI_POLICIES) { + final String resource = resourceAction.getResource().getIdentifier(); + final String actionCode = getActionCode(resourceAction.getAction()); + addUserToAccessPolicy(authorizations, resource, nifiUser.getIdentifier(), actionCode); + } + } + } + + /** + * Populates the authorizations for the NiFi Group. + * + * @param authorizations the overall authorizations + */ + private void populateNiFiGroup(final Authorizations authorizations) { + final Group nifiGroup = AccessPolicyProviderUtils.getGroup(nifiGroupName, getUserGroupProvider()); + + // grant access to the resources needed for initial nifi-proxy identities + for (final ResourceAndAction resourceAction : InitialPolicies.NIFI_POLICIES) { + final String resource = resourceAction.getResource().getIdentifier(); + final String actionCode = getActionCode(resourceAction.getAction()); + addGroupToAccessPolicy(authorizations, resource, nifiGroup.getIdentifier(), actionCode); + } + } + + private void addGroupToAccessPolicy(final Authorizations authorizations, final String resource, final String groupIdentifier, final String actionCode) { + Optional policyOptional = authorizations.getPolicies().getPolicy().stream() + .filter(policy -> policy.getResource().equals(resource)) + .filter(policy -> policy.getAction().equals(actionCode)) + .findAny(); + if (policyOptional.isPresent()) { + Policy policy = policyOptional.get(); + Policy.Group group = new Policy.Group(); + group.setIdentifier(groupIdentifier); + policy.getGroup().add(group); + } else { + AccessPolicy.Builder accessPolicyBuilder = + new AccessPolicy.Builder() + .identifierGenerateFromSeed(resource + actionCode) + .resource(resource) + .addGroup(groupIdentifier) + .action(getAction(actionCode)); + + authorizations.getPolicies().getPolicy().add(createJAXBPolicy(accessPolicyBuilder.build())); + } + } + + /** + * Creates and adds an access policy for the given resource, identity, and actions to the specified authorizations. + * + * @param authorizations the Authorizations instance to add the policy to + * @param resource the resource for the policy + * @param userIdentifier the identifier for the user to add to the policy + * @param actionCode the action for the policy + */ + private void addUserToAccessPolicy(final Authorizations authorizations, final String resource, final String userIdentifier, final String actionCode) { + // first try to find an existing policy for the given resource and action + Policy foundPolicy = null; + for (Policy policy : authorizations.getPolicies().getPolicy()) { + if (policy.getResource().equals(resource) && policy.getAction().equals(actionCode)) { + foundPolicy = policy; + break; + } + } + + if (foundPolicy == null) { + // if we didn't find an existing policy create a new one + final String uuidSeed = resource + actionCode; + + final AccessPolicy.Builder builder = new AccessPolicy.Builder() + .identifierGenerateFromSeed(uuidSeed) + .resource(resource) + .addUser(userIdentifier) + .action(getAction(actionCode)); + + final AccessPolicy accessPolicy = builder.build(); + final Policy jaxbPolicy = createJAXBPolicy(accessPolicy); + authorizations.getPolicies().getPolicy().add(jaxbPolicy); + } else { + // otherwise add the user to the existing policy + Policy.User policyUser = new Policy.User(); + policyUser.setIdentifier(userIdentifier); + foundPolicy.getUser().add(policyUser); + } + } + + private Policy createJAXBPolicy(final AccessPolicy accessPolicy) { + final Policy policy = new Policy(); + policy.setIdentifier(accessPolicy.getIdentifier()); + policy.setResource(accessPolicy.getResource()); + policy.setAction(getActionCode(accessPolicy.getAction())); + transferUsersAndGroups(accessPolicy, policy); + return policy; + } + + private String getActionCode(final RequestAction action) { + switch (action) { + case READ: + return READ_CODE; + case WRITE: + return WRITE_CODE; + case DELETE: + return DELETE_CODE; + default: + throw new IllegalStateException("Unknown action: " + action); + } + } + + private RequestAction getAction(final String actionCode) { + switch (actionCode) { + case READ_CODE: + return RequestAction.READ; + case WRITE_CODE: + return RequestAction.WRITE; + case DELETE_CODE: + return RequestAction.DELETE; + default: + throw new IllegalStateException("Unknown action: " + actionCode); + } + } + + /** + * Sets the given Policy to the state of the provided AccessPolicy. Users and Groups will be cleared and + * set to match the AccessPolicy, the resource and action will be set to match the AccessPolicy. + * + * Does not set the identifier. + * + * @param accessPolicy the AccessPolicy to transfer state from + * @param policy the Policy to transfer state to + */ + private void transferUsersAndGroups(AccessPolicy accessPolicy, Policy policy) { + // add users to the policy + policy.getUser().clear(); + for (String userIdentifier : accessPolicy.getUsers()) { + Policy.User policyUser = new Policy.User(); + policyUser.setIdentifier(userIdentifier); + policy.getUser().add(policyUser); + } + + // add groups to the policy + policy.getGroup().clear(); + for (String groupIdentifier : accessPolicy.getGroups()) { + Policy.Group policyGroup = new Policy.Group(); + policyGroup.setIdentifier(groupIdentifier); + policy.getGroup().add(policyGroup); + } + } + + /** + * Saves the Authorizations instance by marshalling to a file, then re-populates the + * in-memory data structures and sets the new holder. + * + * Synchronized to ensure only one thread writes the file at a time. + * + * @param authorizations the authorizations to save and populate from + * @throws AuthorizationAccessException if an error occurs saving the authorizations + */ + private synchronized void saveAndRefreshHolder(final Authorizations authorizations) throws AuthorizationAccessException { + try { + saveAuthorizations(authorizations); + + this.authorizationsHolder.set(new AuthorizationsHolder(authorizations)); + } catch (JAXBException e) { + throw new AuthorizationAccessException("Unable to save Authorizations", e); + } + } + + @Override + public void preDestruction() throws SecurityProviderDestructionException { + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileAuthorizer.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileAuthorizer.java new file mode 100644 index 0000000000..314a92264b --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileAuthorizer.java @@ -0,0 +1,275 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.file; + +import org.apache.nifi.registry.security.authorization.AbstractPolicyBasedAuthorizer; +import org.apache.nifi.registry.security.authorization.AccessPolicy; +import org.apache.nifi.registry.security.authorization.AccessPolicyProviderInitializationContext; +import org.apache.nifi.registry.security.authorization.AccessPolicyProviderLookup; +import org.apache.nifi.registry.security.authorization.AuthorizerConfigurationContext; +import org.apache.nifi.registry.security.authorization.AuthorizerInitializationContext; +import org.apache.nifi.registry.security.authorization.Group; +import org.apache.nifi.registry.security.authorization.RequestAction; +import org.apache.nifi.registry.security.authorization.StandardAuthorizerConfigurationContext; +import org.apache.nifi.registry.security.authorization.User; +import org.apache.nifi.registry.security.authorization.UserGroupProviderInitializationContext; +import org.apache.nifi.registry.security.authorization.UserGroupProviderLookup; +import org.apache.nifi.registry.security.authorization.UsersAndAccessPolicies; +import org.apache.nifi.registry.security.authorization.annotation.AuthorizerContext; +import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException; +import org.apache.nifi.registry.security.authorization.util.AccessPolicyProviderUtils; +import org.apache.nifi.registry.security.authorization.util.UserGroupProviderUtils; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.identity.IdentityMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; + +/** + * Provides authorizes requests to resources using policies persisted in a file. + */ +public class FileAuthorizer extends AbstractPolicyBasedAuthorizer { + + private static final Logger logger = LoggerFactory.getLogger(FileAuthorizer.class); + + private static final String FILE_USER_GROUP_PROVIDER_ID = "file-user-group-provider"; + private static final String FILE_ACCESS_POLICY_PROVIDER_ID = "file-access-policy-provider"; + + static final String PROP_LEGACY_AUTHORIZED_USERS_FILE = "Legacy Authorized Users File"; + + private FileUserGroupProvider userGroupProvider = new FileUserGroupProvider(); + private FileAccessPolicyProvider accessPolicyProvider = new FileAccessPolicyProvider(); + + @Override + public void initialize(final AuthorizerInitializationContext initializationContext) throws SecurityProviderCreationException { + // initialize the user group provider + userGroupProvider.initialize(new UserGroupProviderInitializationContext() { + @Override + public String getIdentifier() { + return FILE_USER_GROUP_PROVIDER_ID; + } + + @Override + public UserGroupProviderLookup getUserGroupProviderLookup() { + return (identifier) -> null; + } + }); + + // initialize the access policy provider + accessPolicyProvider.initialize(new AccessPolicyProviderInitializationContext() { + @Override + public String getIdentifier() { + return FILE_ACCESS_POLICY_PROVIDER_ID; + } + + @Override + public UserGroupProviderLookup getUserGroupProviderLookup() { + return (identifier) -> { + if (FILE_USER_GROUP_PROVIDER_ID.equals(identifier)) { + return userGroupProvider; + } + + return null; + }; + } + + @Override + public AccessPolicyProviderLookup getAccessPolicyProviderLookup() { + return (identifier) -> null; + } + }); + } + + @Override + public void doOnConfigured(final AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException { + final Map configurationProperties = configurationContext.getProperties(); + + // relay the relevant config + final Map userGroupProperties = new HashMap<>(); + if (configurationProperties.containsKey(FileUserGroupProvider.PROP_TENANTS_FILE)) { + userGroupProperties.put(FileUserGroupProvider.PROP_TENANTS_FILE, configurationProperties.get(FileUserGroupProvider.PROP_TENANTS_FILE)); + } + if (configurationProperties.containsKey(FileAuthorizer.PROP_LEGACY_AUTHORIZED_USERS_FILE)) { + userGroupProperties.put(FileAuthorizer.PROP_LEGACY_AUTHORIZED_USERS_FILE, configurationProperties.get(FileAuthorizer.PROP_LEGACY_AUTHORIZED_USERS_FILE)); + } + + // relay the relevant config + final Map accessPolicyProperties = new HashMap<>(); + accessPolicyProperties.put(FileAccessPolicyProvider.PROP_USER_GROUP_PROVIDER, FILE_USER_GROUP_PROVIDER_ID); + if (configurationProperties.containsKey(FileAccessPolicyProvider.PROP_AUTHORIZATIONS_FILE)) { + accessPolicyProperties.put(FileAccessPolicyProvider.PROP_AUTHORIZATIONS_FILE, configurationProperties.get(FileAccessPolicyProvider.PROP_AUTHORIZATIONS_FILE)); + } + if (configurationProperties.containsKey(AccessPolicyProviderUtils.PROP_INITIAL_ADMIN_IDENTITY)) { + accessPolicyProperties.put(AccessPolicyProviderUtils.PROP_INITIAL_ADMIN_IDENTITY, configurationProperties.get(AccessPolicyProviderUtils.PROP_INITIAL_ADMIN_IDENTITY)); + } + if (configurationProperties.containsKey(FileAuthorizer.PROP_LEGACY_AUTHORIZED_USERS_FILE)) { + accessPolicyProperties.put(FileAuthorizer.PROP_LEGACY_AUTHORIZED_USERS_FILE, configurationProperties.get(FileAuthorizer.PROP_LEGACY_AUTHORIZED_USERS_FILE)); + } + + // ensure all nifi identities are seeded into the user provider + configurationProperties.forEach((property, value) -> { + final Matcher matcher = AccessPolicyProviderUtils.NIFI_IDENTITY_PATTERN.matcher(property); + if (matcher.matches()) { + accessPolicyProperties.put(property, value); + userGroupProperties.put(property.replace(AccessPolicyProviderUtils.PROP_NIFI_IDENTITY_PREFIX, UserGroupProviderUtils.PROP_INITIAL_USER_IDENTITY_PREFIX), value); + } + }); + + // ensure the initial admin is seeded into the user provider if appropriate + if (configurationProperties.containsKey(AccessPolicyProviderUtils.PROP_INITIAL_ADMIN_IDENTITY)) { + int i = 0; + while (true) { + final String key = UserGroupProviderUtils.PROP_INITIAL_USER_IDENTITY_PREFIX + i++; + if (!userGroupProperties.containsKey(key)) { + userGroupProperties.put(key, configurationProperties.get(AccessPolicyProviderUtils.PROP_INITIAL_ADMIN_IDENTITY)); + break; + } + } + } + + // configure the user group provider + userGroupProvider.onConfigured(new StandardAuthorizerConfigurationContext(FILE_USER_GROUP_PROVIDER_ID, userGroupProperties)); + + // configure the access policy provider + accessPolicyProvider.onConfigured(new StandardAuthorizerConfigurationContext(FILE_USER_GROUP_PROVIDER_ID, accessPolicyProperties)); + } + + @Override + public void preDestruction() { + + } + + // ------------------ Groups ------------------ + + @Override + public synchronized Group doAddGroup(Group group) throws AuthorizationAccessException { + return userGroupProvider.addGroup(group); + } + + @Override + public Group getGroup(String identifier) throws AuthorizationAccessException { + return userGroupProvider.getGroup(identifier); + } + + @Override + public synchronized Group doUpdateGroup(Group group) throws AuthorizationAccessException { + return userGroupProvider.updateGroup(group); + } + + @Override + public synchronized Group deleteGroup(Group group) throws AuthorizationAccessException { + return userGroupProvider.deleteGroup(group); + } + + @Override + public Set getGroups() throws AuthorizationAccessException { + return userGroupProvider.getGroups(); + } + + // ------------------ Users ------------------ + + @Override + public synchronized User doAddUser(final User user) throws AuthorizationAccessException { + return userGroupProvider.addUser(user); + } + + @Override + public User getUser(final String identifier) throws AuthorizationAccessException { + return userGroupProvider.getUser(identifier); + } + + @Override + public User getUserByIdentity(final String identity) throws AuthorizationAccessException { + return userGroupProvider.getUserByIdentity(identity); + } + + @Override + public synchronized User doUpdateUser(final User user) throws AuthorizationAccessException { + return userGroupProvider.updateUser(user); + } + + @Override + public synchronized User deleteUser(final User user) throws AuthorizationAccessException { + return userGroupProvider.deleteUser(user); + } + + @Override + public Set getUsers() throws AuthorizationAccessException { + return userGroupProvider.getUsers(); + } + + // ------------------ AccessPolicies ------------------ + + @Override + public synchronized AccessPolicy doAddAccessPolicy(final AccessPolicy accessPolicy) throws AuthorizationAccessException { + return accessPolicyProvider.addAccessPolicy(accessPolicy); + } + + @Override + public AccessPolicy getAccessPolicy(final String identifier) throws AuthorizationAccessException { + return accessPolicyProvider.getAccessPolicy(identifier); + } + + @Override + public synchronized AccessPolicy updateAccessPolicy(final AccessPolicy accessPolicy) throws AuthorizationAccessException { + return accessPolicyProvider.updateAccessPolicy(accessPolicy); + } + + @Override + public synchronized AccessPolicy deleteAccessPolicy(final AccessPolicy accessPolicy) throws AuthorizationAccessException { + return accessPolicyProvider.deleteAccessPolicy(accessPolicy); + } + + @Override + public Set getAccessPolicies() throws AuthorizationAccessException { + return accessPolicyProvider.getAccessPolicies(); + } + + @AuthorizerContext + public void setIdentityMapper(final IdentityMapper identityMapper) { + userGroupProvider.setIdentityMapper(identityMapper); + accessPolicyProvider.setIdentityMapper(identityMapper); + } + + @Override + public synchronized UsersAndAccessPolicies getUsersAndAccessPolicies() throws AuthorizationAccessException { + final AuthorizationsHolder authorizationsHolder = accessPolicyProvider.getAuthorizationsHolder(); + final UserGroupHolder userGroupHolder = userGroupProvider.getUserGroupHolder(); + + return new UsersAndAccessPolicies() { + @Override + public AccessPolicy getAccessPolicy(String resourceIdentifier, RequestAction action) { + return authorizationsHolder.getAccessPolicy(resourceIdentifier, action); + } + + @Override + public User getUser(String identity) { + return userGroupHolder.getUser(identity); + } + + @Override + public Set getGroups(String userIdentity) { + return userGroupHolder.getGroups(userIdentity); + } + }; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileUserGroupProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileUserGroupProvider.java new file mode 100644 index 0000000000..ba7ca9c5fb --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileUserGroupProvider.java @@ -0,0 +1,716 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.file; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.security.authorization.AuthorizerConfigurationContext; +import org.apache.nifi.registry.security.authorization.ConfigurableUserGroupProvider; +import org.apache.nifi.registry.security.authorization.Group; +import org.apache.nifi.registry.security.authorization.User; +import org.apache.nifi.registry.security.authorization.UserAndGroups; +import org.apache.nifi.registry.security.authorization.UserGroupProviderInitializationContext; +import org.apache.nifi.registry.security.authorization.annotation.AuthorizerContext; +import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException; +import org.apache.nifi.registry.security.authorization.exception.UninheritableAuthorizationsException; +import org.apache.nifi.registry.security.authorization.file.tenants.generated.Groups; +import org.apache.nifi.registry.security.authorization.file.tenants.generated.Tenants; +import org.apache.nifi.registry.security.authorization.file.tenants.generated.Users; +import org.apache.nifi.registry.security.authorization.util.UserGroupProviderUtils; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException; +import org.apache.nifi.registry.security.identity.IdentityMapper; +import org.apache.nifi.registry.util.PropertyValue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import javax.xml.XMLConstants; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBElement; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; + +public class FileUserGroupProvider implements ConfigurableUserGroupProvider { + + private static final Logger logger = LoggerFactory.getLogger(FileUserGroupProvider.class); + + private static final String TENANTS_XSD = "/tenants.xsd"; + private static final String JAXB_TENANTS_PATH = "org.apache.nifi.registry.security.authorization.file.tenants.generated"; + + private static final JAXBContext JAXB_TENANTS_CONTEXT = initializeJaxbContext(JAXB_TENANTS_PATH); + + /** + * Load the JAXBContext. + */ + private static JAXBContext initializeJaxbContext(final String contextPath) { + try { + return JAXBContext.newInstance(contextPath, FileAuthorizer.class.getClassLoader()); + //return JAXBContext.newInstance(contextPath); + } catch (JAXBException e) { + throw new RuntimeException("Unable to create JAXBContext: " + e); + } + } + + private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance(); + private static final XMLOutputFactory XML_OUTPUT_FACTORY = XMLOutputFactory.newInstance(); + + private static final String USER_ELEMENT = "user"; + private static final String GROUP_USER_ELEMENT = "groupUser"; + private static final String GROUP_ELEMENT = "group"; + private static final String IDENTIFIER_ATTR = "identifier"; + private static final String IDENTITY_ATTR = "identity"; + private static final String NAME_ATTR = "name"; + + static final String PROP_TENANTS_FILE = "Users File"; + + private Schema tenantsSchema; + private File tenantsFile; + private Set initialUserIdentities; + private IdentityMapper identityMapper; + + private final AtomicReference userGroupHolder = new AtomicReference<>(); + + @Override + public void initialize(UserGroupProviderInitializationContext initializationContext) throws SecurityProviderCreationException { + try { + final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + tenantsSchema = schemaFactory.newSchema(FileAuthorizer.class.getResource(TENANTS_XSD)); + } catch (Exception e) { + throw new SecurityProviderCreationException(e); + } + } + + @Override + public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException { + try { + final PropertyValue tenantsPath = configurationContext.getProperty(PROP_TENANTS_FILE); + if (StringUtils.isBlank(tenantsPath.getValue())) { + throw new SecurityProviderCreationException("The users file must be specified."); + } + + // get the tenants file and ensure it exists + tenantsFile = new File(tenantsPath.getValue()); + if (!tenantsFile.exists()) { + logger.info("Creating new users file at {}", new Object[] {tenantsFile.getAbsolutePath()}); + saveTenants(new Tenants()); + } + + // extract any nifi identities + initialUserIdentities = UserGroupProviderUtils.getInitialUserIdentities(configurationContext, identityMapper); + + load(); + + logger.info(String.format("Users/Groups file loaded at %s", new Date().toString())); + } catch (SecurityProviderCreationException | JAXBException | IllegalStateException | SAXException e) { + throw new SecurityProviderCreationException(e); + } + } + + @Override + public Set getUsers() throws AuthorizationAccessException { + return userGroupHolder.get().getAllUsers(); + } + + @Override + public synchronized User addUser(User user) throws AuthorizationAccessException { + if (user == null) { + throw new IllegalArgumentException("User cannot be null"); + } + + final org.apache.nifi.registry.security.authorization.file.tenants.generated.User jaxbUser = createJAXBUser(user); + + final UserGroupHolder holder = userGroupHolder.get(); + final Tenants tenants = holder.getTenants(); + tenants.getUsers().getUser().add(jaxbUser); + + saveAndRefreshHolder(tenants); + + return userGroupHolder.get().getUsersById().get(user.getIdentifier()); + } + + @Override + public User getUser(String identifier) throws AuthorizationAccessException { + if (identifier == null) { + return null; + } + + final UserGroupHolder holder = userGroupHolder.get(); + return holder.getUsersById().get(identifier); + } + + @Override + public synchronized User updateUser(User user) throws AuthorizationAccessException { + if (user == null) { + throw new IllegalArgumentException("User cannot be null"); + } + + final UserGroupHolder holder = userGroupHolder.get(); + final Tenants tenants = holder.getTenants(); + + final List users = tenants.getUsers().getUser(); + + // fine the User that needs to be updated + org.apache.nifi.registry.security.authorization.file.tenants.generated.User updateUser = null; + for (org.apache.nifi.registry.security.authorization.file.tenants.generated.User jaxbUser : users) { + if (user.getIdentifier().equals(jaxbUser.getIdentifier())) { + updateUser = jaxbUser; + break; + } + } + + // if user wasn't found return null, otherwise update the user and save changes + if (updateUser == null) { + return null; + } else { + updateUser.setIdentity(user.getIdentity()); + saveAndRefreshHolder(tenants); + + return userGroupHolder.get().getUsersById().get(user.getIdentifier()); + } + } + + @Override + public User getUserByIdentity(String identity) throws AuthorizationAccessException { + if (identity == null) { + return null; + } + + final UserGroupHolder holder = userGroupHolder.get(); + return holder.getUsersByIdentity().get(identity); + } + + @Override + public synchronized User deleteUser(User user) throws AuthorizationAccessException { + if (user == null) { + throw new IllegalArgumentException("User cannot be null"); + } + + return deleteUser(user.getIdentifier()); + } + + private synchronized User deleteUser(String userIdentifier) throws AuthorizationAccessException { + if (userIdentifier == null) { + throw new IllegalArgumentException("User identifier cannot be null"); + } + + final UserGroupHolder holder = userGroupHolder.get(); + final User deletedUser = holder.getUsersById().get(userIdentifier); + if (deletedUser == null) { + return null; + } + + // for each group iterate over the user references and remove the user reference if it matches the user being deleted + final Tenants tenants = holder.getTenants(); + for (org.apache.nifi.registry.security.authorization.file.tenants.generated.Group group : tenants.getGroups().getGroup()) { + Iterator groupUserIter = group.getUser().iterator(); + while (groupUserIter.hasNext()) { + org.apache.nifi.registry.security.authorization.file.tenants.generated.Group.User groupUser = groupUserIter.next(); + if (groupUser.getIdentifier().equals(userIdentifier)) { + groupUserIter.remove(); + break; + } + } + } + + // remove the actual user + Iterator iter = tenants.getUsers().getUser().iterator(); + while (iter.hasNext()) { + org.apache.nifi.registry.security.authorization.file.tenants.generated.User jaxbUser = iter.next(); + if (userIdentifier.equals(jaxbUser.getIdentifier())) { + iter.remove(); + break; + } + } + + saveAndRefreshHolder(tenants); + return deletedUser; + } + + @Override + public Set getGroups() throws AuthorizationAccessException { + return userGroupHolder.get().getAllGroups(); + } + + @Override + public synchronized Group addGroup(Group group) throws AuthorizationAccessException { + if (group == null) { + throw new IllegalArgumentException("Group cannot be null"); + } + + final UserGroupHolder holder = userGroupHolder.get(); + final Tenants tenants = holder.getTenants(); + + // create a new JAXB Group based on the incoming Group + final org.apache.nifi.registry.security.authorization.file.tenants.generated.Group jaxbGroup = + new org.apache.nifi.registry.security.authorization.file.tenants.generated.Group(); + jaxbGroup.setIdentifier(group.getIdentifier()); + jaxbGroup.setName(group.getName()); + + // add each user to the group + for (String groupUser : group.getUsers()) { + org.apache.nifi.registry.security.authorization.file.tenants.generated.Group.User jaxbGroupUser = + new org.apache.nifi.registry.security.authorization.file.tenants.generated.Group.User(); + jaxbGroupUser.setIdentifier(groupUser); + jaxbGroup.getUser().add(jaxbGroupUser); + } + + tenants.getGroups().getGroup().add(jaxbGroup); + saveAndRefreshHolder(tenants); + + return userGroupHolder.get().getGroupsById().get(group.getIdentifier()); + } + + @Override + public Group getGroup(String identifier) throws AuthorizationAccessException { + if (identifier == null) { + return null; + } + return userGroupHolder.get().getGroupsById().get(identifier); + } + + @Override + public UserAndGroups getUserAndGroups(final String identity) throws AuthorizationAccessException { + final UserGroupHolder holder = userGroupHolder.get(); + final User user = holder.getUser(identity); + final Set groups = holder.getGroups(identity); + + return new UserAndGroups() { + @Override + public User getUser() { + return user; + } + + @Override + public Set getGroups() { + return groups; + } + }; + } + + @Override + public synchronized Group updateGroup(Group group) throws AuthorizationAccessException { + if (group == null) { + throw new IllegalArgumentException("Group cannot be null"); + } + + final UserGroupHolder holder = userGroupHolder.get(); + final Tenants tenants = holder.getTenants(); + + // find the group that needs to be update + org.apache.nifi.registry.security.authorization.file.tenants.generated.Group updateGroup = null; + for (org.apache.nifi.registry.security.authorization.file.tenants.generated.Group jaxbGroup : tenants.getGroups().getGroup()) { + if (jaxbGroup.getIdentifier().equals(group.getIdentifier())) { + updateGroup = jaxbGroup; + break; + } + } + + // if the group wasn't found return null, otherwise update the group and save changes + if (updateGroup == null) { + return null; + } + + // reset the list of users and add each user to the group + updateGroup.getUser().clear(); + for (String groupUser : group.getUsers()) { + org.apache.nifi.registry.security.authorization.file.tenants.generated.Group.User jaxbGroupUser = + new org.apache.nifi.registry.security.authorization.file.tenants.generated.Group.User(); + jaxbGroupUser.setIdentifier(groupUser); + updateGroup.getUser().add(jaxbGroupUser); + } + + updateGroup.setName(group.getName()); + saveAndRefreshHolder(tenants); + + return userGroupHolder.get().getGroupsById().get(group.getIdentifier()); + } + + @Override + public synchronized Group deleteGroup(Group group) throws AuthorizationAccessException { + if (group == null) { + throw new IllegalArgumentException("Group cannot be null"); + } + + return deleteGroup(group.getIdentifier()); + } + + private synchronized Group deleteGroup(String groupIdentifier) throws AuthorizationAccessException { + if (groupIdentifier == null) { + throw new IllegalArgumentException("Group identifier cannot be null"); + } + + final UserGroupHolder holder = userGroupHolder.get(); + final Group deletedGroup = holder.getGroupsById().get(groupIdentifier); + if (deletedGroup == null) { + return null; + } + + // now remove the actual group from the top-level list of groups + final Tenants tenants = holder.getTenants(); + Iterator iter = tenants.getGroups().getGroup().iterator(); + while (iter.hasNext()) { + org.apache.nifi.registry.security.authorization.file.tenants.generated.Group jaxbGroup = iter.next(); + if (groupIdentifier.equals(jaxbGroup.getIdentifier())) { + iter.remove(); + break; + } + } + + saveAndRefreshHolder(tenants); + return deletedGroup; + } + + UserGroupHolder getUserGroupHolder() { + return userGroupHolder.get(); + } + + @AuthorizerContext + public void setIdentityMapper(final IdentityMapper identityMapper) { + this.identityMapper = identityMapper; + } + + @Override + public synchronized void inheritFingerprint(String fingerprint) throws AuthorizationAccessException { + final UsersAndGroups usersAndGroups = parseUsersAndGroups(fingerprint); + usersAndGroups.getUsers().forEach(user -> addUser(user)); + usersAndGroups.getGroups().forEach(group -> addGroup(group)); + } + + @Override + public void checkInheritability(String proposedFingerprint) throws AuthorizationAccessException { + try { + // ensure we understand the proposed fingerprint + parseUsersAndGroups(proposedFingerprint); + } catch (final AuthorizationAccessException e) { + throw new UninheritableAuthorizationsException("Unable to parse the proposed fingerprint: " + e); + } + + final UserGroupHolder usersAndGroups = userGroupHolder.get(); + + // ensure we are in a proper state to inherit the fingerprint + if (!usersAndGroups.getAllUsers().isEmpty() || !usersAndGroups.getAllGroups().isEmpty()) { + throw new UninheritableAuthorizationsException("Proposed fingerprint is not inheritable because the current users and groups is not empty."); + } + } + + @Override + public String getFingerprint() throws AuthorizationAccessException { + final UserGroupHolder usersAndGroups = userGroupHolder.get(); + + final List users = new ArrayList<>(usersAndGroups.getAllUsers()); + Collections.sort(users, Comparator.comparing(User::getIdentifier)); + + final List groups = new ArrayList<>(usersAndGroups.getAllGroups()); + Collections.sort(groups, Comparator.comparing(Group::getIdentifier)); + + XMLStreamWriter writer = null; + final StringWriter out = new StringWriter(); + try { + writer = XML_OUTPUT_FACTORY.createXMLStreamWriter(out); + writer.writeStartDocument(); + writer.writeStartElement("tenants"); + + for (User user : users) { + writeUser(writer, user); + } + for (Group group : groups) { + writeGroup(writer, group); + } + + writer.writeEndElement(); + writer.writeEndDocument(); + writer.flush(); + } catch (XMLStreamException e) { + throw new AuthorizationAccessException("Unable to generate fingerprint", e); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (XMLStreamException e) { + // nothing to do here + } + } + } + + return out.toString(); + } + + private UsersAndGroups parseUsersAndGroups(final String fingerprint) { + final List users = new ArrayList<>(); + final List groups = new ArrayList<>(); + + final byte[] fingerprintBytes = fingerprint.getBytes(StandardCharsets.UTF_8); + try (final ByteArrayInputStream in = new ByteArrayInputStream(fingerprintBytes)) { + final DocumentBuilder docBuilder = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder(); + final Document document = docBuilder.parse(in); + final Element rootElement = document.getDocumentElement(); + + // parse all the users and add them to the current user group provider + NodeList userNodes = rootElement.getElementsByTagName(USER_ELEMENT); + for (int i=0; i < userNodes.getLength(); i++) { + Node userNode = userNodes.item(i); + users.add(parseUser((Element) userNode)); + } + + // parse all the groups and add them to the current user group provider + NodeList groupNodes = rootElement.getElementsByTagName(GROUP_ELEMENT); + for (int i=0; i < groupNodes.getLength(); i++) { + Node groupNode = groupNodes.item(i); + groups.add(parseGroup((Element) groupNode)); + } + } catch (SAXException | ParserConfigurationException | IOException e) { + throw new AuthorizationAccessException("Unable to parse fingerprint", e); + } + + return new UsersAndGroups(users, groups); + } + + private User parseUser(final Element element) { + final User.Builder builder = new User.Builder() + .identifier(element.getAttribute(IDENTIFIER_ATTR)) + .identity(element.getAttribute(IDENTITY_ATTR)); + + return builder.build(); + } + + private Group parseGroup(final Element element) { + final Group.Builder builder = new Group.Builder() + .identifier(element.getAttribute(IDENTIFIER_ATTR)) + .name(element.getAttribute(NAME_ATTR)); + + NodeList groupUsers = element.getElementsByTagName(GROUP_USER_ELEMENT); + for (int i=0; i < groupUsers.getLength(); i++) { + Element groupUserNode = (Element) groupUsers.item(i); + builder.addUser(groupUserNode.getAttribute(IDENTIFIER_ATTR)); + } + + return builder.build(); + } + + private void writeUser(final XMLStreamWriter writer, final User user) throws XMLStreamException { + writer.writeStartElement(USER_ELEMENT); + writer.writeAttribute(IDENTIFIER_ATTR, user.getIdentifier()); + writer.writeAttribute(IDENTITY_ATTR, user.getIdentity()); + writer.writeEndElement(); + } + + private void writeGroup(final XMLStreamWriter writer, final Group group) throws XMLStreamException { + List users = new ArrayList<>(group.getUsers()); + Collections.sort(users); + + writer.writeStartElement(GROUP_ELEMENT); + writer.writeAttribute(IDENTIFIER_ATTR, group.getIdentifier()); + writer.writeAttribute(NAME_ATTR, group.getName()); + + for (String user : users) { + writer.writeStartElement(GROUP_USER_ELEMENT); + writer.writeAttribute(IDENTIFIER_ATTR, user); + writer.writeEndElement(); + } + + writer.writeEndElement(); + } + + private org.apache.nifi.registry.security.authorization.file.tenants.generated.User createJAXBUser(User user) { + final org.apache.nifi.registry.security.authorization.file.tenants.generated.User jaxbUser = + new org.apache.nifi.registry.security.authorization.file.tenants.generated.User(); + jaxbUser.setIdentifier(user.getIdentifier()); + jaxbUser.setIdentity(user.getIdentity()); + return jaxbUser; + } + + /** + * Loads the authorizations file and populates the AuthorizationsHolder, only called during start-up. + * + * @throws JAXBException Unable to reload the authorized users file + * @throws IllegalStateException Unable to sync file with restore + * @throws SAXException Unable to unmarshall tenants + */ + private synchronized void load() throws JAXBException, IllegalStateException, SAXException { + final Tenants tenants = unmarshallTenants(); + if (tenants.getUsers() == null) { + tenants.setUsers(new Users()); + } + if (tenants.getGroups() == null) { + tenants.setGroups(new Groups()); + } + + final UserGroupHolder userGroupHolder = new UserGroupHolder(tenants); + final boolean emptyTenants = userGroupHolder.getAllUsers().isEmpty() && userGroupHolder.getAllGroups().isEmpty(); + + if (emptyTenants) { + + populateInitialUsers(tenants); + + // save any changes that were made and repopulate the holder + saveAndRefreshHolder(tenants); + } else { + this.userGroupHolder.set(userGroupHolder); + } + } + + private void saveTenants(final Tenants tenants) throws JAXBException { + final Marshaller marshaller = JAXB_TENANTS_CONTEXT.createMarshaller(); + marshaller.setSchema(tenantsSchema); + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); + marshaller.marshal(tenants, tenantsFile); + } + + private Tenants unmarshallTenants() throws JAXBException { + final Unmarshaller unmarshaller = JAXB_TENANTS_CONTEXT.createUnmarshaller(); + unmarshaller.setSchema(tenantsSchema); + + final JAXBElement element = unmarshaller.unmarshal(new StreamSource(tenantsFile), Tenants.class); + return element.getValue(); + } + + private void populateInitialUsers(final Tenants tenants) { + for (String initialUserIdentity : initialUserIdentities) { + getOrCreateUser(tenants, initialUserIdentity); + } + } + + /** + * Finds the User with the given identity, or creates a new one and adds it to the Tenants. + * + * @param tenants the Tenants reference + * @param userIdentity the user identity to find or create + * @return the User from Tenants with the given identity, or a new instance that was added to Tenants + */ + private org.apache.nifi.registry.security.authorization.file.tenants.generated.User getOrCreateUser(final Tenants tenants, final String userIdentity) { + if (StringUtils.isBlank(userIdentity)) { + return null; + } + + org.apache.nifi.registry.security.authorization.file.tenants.generated.User foundUser = null; + for (org.apache.nifi.registry.security.authorization.file.tenants.generated.User user : tenants.getUsers().getUser()) { + if (user.getIdentity().equals(userIdentity)) { + foundUser = user; + break; + } + } + + if (foundUser == null) { + final User newUser = new User.Builder().identifierGenerateFromSeed(userIdentity).identity(userIdentity).build(); + foundUser = new org.apache.nifi.registry.security.authorization.file.tenants.generated.User(); + foundUser.setIdentifier(newUser.getIdentifier()); + foundUser.setIdentity(newUser.getIdentity()); + tenants.getUsers().getUser().add(foundUser); + } + + return foundUser; + } + + /** + * Finds the Group with the given name, or creates a new one and adds it to Tenants. + * + * @param tenants the Tenants reference + * @param groupName the name of the group to look for + * @return the Group from Tenants with the given name, or a new instance that was added to Tenants + */ + private org.apache.nifi.registry.security.authorization.file.tenants.generated.Group getOrCreateGroup(final Tenants tenants, final String groupName) { + if (StringUtils.isBlank(groupName)) { + return null; + } + + org.apache.nifi.registry.security.authorization.file.tenants.generated.Group foundGroup = null; + for (org.apache.nifi.registry.security.authorization.file.tenants.generated.Group group : tenants.getGroups().getGroup()) { + if (group.getName().equals(groupName)) { + foundGroup = group; + break; + } + } + + if (foundGroup == null) { + final Group newGroup = new Group.Builder().identifierGenerateFromSeed(groupName).name(groupName).build(); + foundGroup = new org.apache.nifi.registry.security.authorization.file.tenants.generated.Group(); + foundGroup.setIdentifier(newGroup.getIdentifier()); + foundGroup.setName(newGroup.getName()); + tenants.getGroups().getGroup().add(foundGroup); + } + + return foundGroup; + } + + /** + * Saves the Authorizations instance by marshalling to a file, then re-populates the + * in-memory data structures and sets the new holder. + * + * Synchronized to ensure only one thread writes the file at a time. + * + * @param tenants the tenants to save and populate from + * @throws AuthorizationAccessException if an error occurs saving the authorizations + */ + private synchronized void saveAndRefreshHolder(final Tenants tenants) throws AuthorizationAccessException { + try { + saveTenants(tenants); + + this.userGroupHolder.set(new UserGroupHolder(tenants)); + } catch (JAXBException e) { + throw new AuthorizationAccessException("Unable to save Authorizations", e); + } + } + + @Override + public void preDestruction() throws SecurityProviderDestructionException { + } + + private static class UsersAndGroups { + final List users; + final List groups; + + public UsersAndGroups(List users, List groups) { + this.users = users; + this.groups = groups; + } + + public List getUsers() { + return users; + } + + public List getGroups() { + return groups; + } + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/UserGroupHolder.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/UserGroupHolder.java new file mode 100644 index 0000000000..9828a45551 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/UserGroupHolder.java @@ -0,0 +1,241 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.file; + + +import org.apache.nifi.registry.security.authorization.file.tenants.generated.Groups; +import org.apache.nifi.registry.security.authorization.file.tenants.generated.Tenants; +import org.apache.nifi.registry.security.authorization.file.tenants.generated.Users; +import org.apache.nifi.registry.security.authorization.Group; +import org.apache.nifi.registry.security.authorization.User; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * A holder to provide atomic access to user group data structures. + */ +public class UserGroupHolder { + + private final Tenants tenants; + + private final Set allUsers; + private final Map usersById; + private final Map usersByIdentity; + + private final Set allGroups; + private final Map groupsById; + private final Map> groupsByUserIdentity; + + /** + * Creates a new holder and populates all convenience data structures. + * + * @param tenants the current tenants instance + */ + public UserGroupHolder(final Tenants tenants) { + this.tenants = tenants; + + // load all users + final Users users = tenants.getUsers(); + final Set allUsers = Collections.unmodifiableSet(createUsers(users)); + + // load all groups + final Groups groups = tenants.getGroups(); + final Set allGroups = Collections.unmodifiableSet(createGroups(groups, users)); + + // create a convenience map to retrieve a user by id + final Map userByIdMap = Collections.unmodifiableMap(createUserByIdMap(allUsers)); + + // create a convenience map to retrieve a user by identity + final Map userByIdentityMap = Collections.unmodifiableMap(createUserByIdentityMap(allUsers)); + + // create a convenience map to retrieve a group by id + final Map groupByIdMap = Collections.unmodifiableMap(createGroupByIdMap(allGroups)); + + // create a convenience map to retrieve the groups for a user identity + final Map> groupsByUserIdentityMap = Collections.unmodifiableMap(createGroupsByUserIdentityMap(allGroups, allUsers)); + + // set all the holders + this.allUsers = allUsers; + this.allGroups = allGroups; + this.usersById = userByIdMap; + this.usersByIdentity = userByIdentityMap; + this.groupsById = groupByIdMap; + this.groupsByUserIdentity = groupsByUserIdentityMap; + } + + /** + * Creates a set of Users from the JAXB Users. + * + * @param users the JAXB Users + * @return a set of API Users matching the provided JAXB Users + */ + private Set createUsers(Users users) { + Set allUsers = new HashSet<>(); + if (users == null || users.getUser() == null) { + return allUsers; + } + + for (org.apache.nifi.registry.security.authorization.file.tenants.generated.User user : users.getUser()) { + final User.Builder builder = new User.Builder() + .identity(user.getIdentity()) + .identifier(user.getIdentifier()); + + allUsers.add(builder.build()); + } + + return allUsers; + } + + /** + * Creates a set of Groups from the JAXB Groups. + * + * @param groups the JAXB Groups + * @return a set of API Groups matching the provided JAXB Groups + */ + private Set createGroups(Groups groups, + Users users) { + Set allGroups = new HashSet<>(); + if (groups == null || groups.getGroup() == null) { + return allGroups; + } + + for (org.apache.nifi.registry.security.authorization.file.tenants.generated.Group group : groups.getGroup()) { + final Group.Builder builder = new Group.Builder() + .identifier(group.getIdentifier()) + .name(group.getName()); + + for (org.apache.nifi.registry.security.authorization.file.tenants.generated.Group.User groupUser : group.getUser()) { + builder.addUser(groupUser.getIdentifier()); + } + + allGroups.add(builder.build()); + } + + return allGroups; + } + + /** + * Creates a Map from user identifier to User. + * + * @param users the set of all users + * @return the Map from user identifier to User + */ + private Map createUserByIdMap(final Set users) { + Map usersMap = new HashMap<>(); + for (User user : users) { + usersMap.put(user.getIdentifier(), user); + } + return usersMap; + } + + /** + * Creates a Map from user identity to User. + * + * @param users the set of all users + * @return the Map from user identity to User + */ + private Map createUserByIdentityMap(final Set users) { + Map usersMap = new HashMap<>(); + for (User user : users) { + usersMap.put(user.getIdentity(), user); + } + return usersMap; + } + + /** + * Creates a Map from group identifier to Group. + * + * @param groups the set of all groups + * @return the Map from group identifier to Group + */ + private Map createGroupByIdMap(final Set groups) { + Map groupsMap = new HashMap<>(); + for (Group group : groups) { + groupsMap.put(group.getIdentifier(), group); + } + return groupsMap; + } + + /** + * Creates a Map from user identity to the set of Groups for that identity. + * + * @param groups all groups + * @param users all users + * @return a Map from User identity to the set of Groups for that identity + */ + private Map> createGroupsByUserIdentityMap(final Set groups, final Set users) { + Map> groupsByUserIdentity = new HashMap<>(); + + for (User user : users) { + Set userGroups = new HashSet<>(); + for (Group group : groups) { + for (String groupUser : group.getUsers()) { + if (groupUser.equals(user.getIdentifier())) { + userGroups.add(group); + } + } + } + + groupsByUserIdentity.put(user.getIdentity(), userGroups); + } + + return groupsByUserIdentity; + } + + public Tenants getTenants() { + return tenants; + } + + public Set getAllUsers() { + return allUsers; + } + + public Map getUsersById() { + return usersById; + } + + public Map getUsersByIdentity() { + return usersByIdentity; + } + + public Set getAllGroups() { + return allGroups; + } + + public Map getGroupsById() { + return groupsById; + } + + public User getUser(String identity) { + if (identity == null) { + throw new IllegalArgumentException("Identity cannot be null"); + } + return usersByIdentity.get(identity); + } + + public Set getGroups(String userIdentity) { + if (userIdentity == null) { + throw new IllegalArgumentException("User Identity cannot be null"); + } + return groupsByUserIdentity.get(userIdentity); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/Authorizable.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/Authorizable.java new file mode 100644 index 0000000000..c461965089 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/Authorizable.java @@ -0,0 +1,300 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.resource; + +import org.apache.nifi.registry.security.authorization.AuthorizationAuditor; +import org.apache.nifi.registry.security.authorization.AuthorizationRequest; +import org.apache.nifi.registry.security.authorization.AuthorizationResult; +import org.apache.nifi.registry.security.authorization.AuthorizationResult.Result; +import org.apache.nifi.registry.security.authorization.Authorizer; +import org.apache.nifi.registry.security.authorization.RequestAction; +import org.apache.nifi.registry.security.authorization.Resource; +import org.apache.nifi.registry.security.authorization.UserContextKeys; +import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException; +import org.apache.nifi.registry.security.authorization.user.NiFiUser; + +import java.util.HashMap; +import java.util.Map; + +public interface Authorizable { + + /** + * The parent for this Authorizable. May be null. + * + * @return the parent authorizable or null + */ + Authorizable getParentAuthorizable(); + + /** + * The Resource for this Authorizable. + * + * @return the resource + */ + Resource getResource(); + + /** + * The originally requested resource for this Authorizable. Because policies are inherited, if a resource + * does not have a policy, this Authorizable may represent a parent resource and this method will return + * the originally requested resource. + * + * @return the originally requested resource + */ + default Resource getRequestedResource() { + return getResource(); + } + + /** + * Returns whether the current user is authorized for the specified action on the specified resource. This + * method does not imply the user is directly attempting to access the specified resource. If the user is + * attempting a direct access use Authorizable.authorize(). + * + * @param authorizer authorizer + * @param action action + * @return is authorized + */ + default boolean isAuthorized(Authorizer authorizer, RequestAction action, NiFiUser user) { + return Result.Approved.equals(checkAuthorization(authorizer, action, user).getResult()); + } + + /** + * Returns the result of an authorization request for the specified user for the specified action on the specified + * resource. This method does not imply the user is directly attempting to access the specified resource. If the user is + * attempting a direct access use Authorizable.authorize(). + * + * @param authorizer authorizer + * @param action action + * @param user user + * @return is authorized + */ + default AuthorizationResult checkAuthorization(Authorizer authorizer, RequestAction action, NiFiUser user, Map resourceContext) { + if (user == null) { + return AuthorizationResult.denied("Unknown user."); + } + + final Map userContext; + if (user.getClientAddress() != null && !user.getClientAddress().trim().isEmpty()) { + userContext = new HashMap<>(); + userContext.put(UserContextKeys.CLIENT_ADDRESS.name(), user.getClientAddress()); + } else { + userContext = null; + } + + final Resource resource = getResource(); + final Resource requestedResource = getRequestedResource(); + final AuthorizationRequest request = new AuthorizationRequest.Builder() + .identity(user.getIdentity()) + .groups(user.getGroups()) + .anonymous(user.isAnonymous()) + .accessAttempt(false) + .action(action) + .resource(resource) + .requestedResource(requestedResource) + .resourceContext(resourceContext) + .userContext(userContext) + .explanationSupplier(() -> { + // build the safe explanation + final StringBuilder safeDescription = new StringBuilder("Unable to "); + + if (RequestAction.READ.equals(action)) { + safeDescription.append("view "); + } else { + safeDescription.append("modify "); // covers write or delete + } + safeDescription.append(resource.getSafeDescription()).append("."); + + return safeDescription.toString(); + }) + .build(); + + // perform the authorization + final AuthorizationResult result = authorizer.authorize(request); + + // verify the results + if (Result.ResourceNotFound.equals(result.getResult())) { + final Authorizable parent = getParentAuthorizable(); + if (parent == null) { + return AuthorizationResult.denied("No applicable policies could be found."); + } else { + // create a custom authorizable to override the safe description but still defer to the parent authorizable + final Authorizable parentProxy = new Authorizable() { + @Override + public Authorizable getParentAuthorizable() { + return parent.getParentAuthorizable(); + } + + @Override + public Resource getRequestedResource() { + return requestedResource; + } + + @Override + public Resource getResource() { + final Resource parentResource = parent.getResource(); + return new Resource() { + @Override + public String getIdentifier() { + return parentResource.getIdentifier(); + } + + @Override + public String getName() { + return parentResource.getName(); + } + + @Override + public String getSafeDescription() { + return resource.getSafeDescription(); + } + }; + } + }; + return parentProxy.checkAuthorization(authorizer, action, user, resourceContext); + } + } else { + return result; + } + } + + /** + * Returns the result of an authorization request for the specified user for the specified action on the specified + * resource. This method does not imply the user is directly attempting to access the specified resource. If the user is + * attempting a direct access use Authorizable.authorize(). + * + * @param authorizer authorizer + * @param action action + * @param user user + * @return is authorized + */ + default AuthorizationResult checkAuthorization(Authorizer authorizer, RequestAction action, NiFiUser user) { + return checkAuthorization(authorizer, action, user, null); + } + + /** + * Authorizes the current user for the specified action on the specified resource. This method does imply the user is + * directly accessing the specified resource. + * + * @param authorizer authorizer + * @param action action + * @param user user + * @param resourceContext resource context + */ + default void authorize(Authorizer authorizer, RequestAction action, NiFiUser user, Map resourceContext) throws AccessDeniedException { + if (user == null) { + throw new AccessDeniedException("Unknown user."); + } + + final Map userContext; + if (user.getClientAddress() != null && !user.getClientAddress().trim().isEmpty()) { + userContext = new HashMap<>(); + userContext.put(UserContextKeys.CLIENT_ADDRESS.name(), user.getClientAddress()); + } else { + userContext = null; + } + + final Resource resource = getResource(); + final Resource requestedResource = getRequestedResource(); + final AuthorizationRequest request = new AuthorizationRequest.Builder() + .identity(user.getIdentity()) + .groups(user.getGroups()) + .anonymous(user.isAnonymous()) + .accessAttempt(true) + .action(action) + .resource(resource) + .requestedResource(requestedResource) + .resourceContext(resourceContext) + .userContext(userContext) + .explanationSupplier(() -> { + // build the safe explanation + final StringBuilder safeDescription = new StringBuilder("Unable to "); + + if (RequestAction.READ.equals(action)) { + safeDescription.append("view "); + } else { + safeDescription.append("modify "); + } + safeDescription.append(resource.getSafeDescription()).append("."); + + return safeDescription.toString(); + }) + .build(); + + final AuthorizationResult result = authorizer.authorize(request); + if (Result.ResourceNotFound.equals(result.getResult())) { + final Authorizable parent = getParentAuthorizable(); + if (parent == null) { + final AuthorizationResult failure = AuthorizationResult.denied("No applicable policies could be found."); + + // audit authorization request + if (authorizer instanceof AuthorizationAuditor) { + ((AuthorizationAuditor) authorizer).auditAccessAttempt(request, failure); + } + + // denied + throw new AccessDeniedException(failure.getExplanation()); + } else { + // create a custom authorizable to override the safe description but still defer to the parent authorizable + final Authorizable parentProxy = new Authorizable() { + @Override + public Authorizable getParentAuthorizable() { + return parent.getParentAuthorizable(); + } + + @Override + public Resource getRequestedResource() { + return requestedResource; + } + + @Override + public Resource getResource() { + final Resource parentResource = parent.getResource(); + return new Resource() { + @Override + public String getIdentifier() { + return parentResource.getIdentifier(); + } + + @Override + public String getName() { + return parentResource.getName(); + } + + @Override + public String getSafeDescription() { + return resource.getSafeDescription(); + } + }; + } + }; + parentProxy.authorize(authorizer, action, user, resourceContext); + } + } else if (Result.Denied.equals(result.getResult())) { + throw new AccessDeniedException(result.getExplanation()); + } + } + + /** + * Authorizes the current user for the specified action on the specified resource. This method does imply the user is + * directly accessing the specified resource. + * + * @param authorizer authorizer + * @param action action + * @param user user + */ + default void authorize(Authorizer authorizer, RequestAction action, NiFiUser user) throws AccessDeniedException { + authorize(authorizer, action, user, null); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/InheritingAuthorizable.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/InheritingAuthorizable.java new file mode 100644 index 0000000000..b0292296c3 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/InheritingAuthorizable.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.resource; + +import org.apache.nifi.registry.security.authorization.AuthorizationResult; +import org.apache.nifi.registry.security.authorization.AuthorizationResult.Result; +import org.apache.nifi.registry.security.authorization.Authorizer; +import org.apache.nifi.registry.security.authorization.RequestAction; +import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException; +import org.apache.nifi.registry.security.authorization.user.NiFiUser; + +import java.util.Map; + +public interface InheritingAuthorizable extends Authorizable { + + /** + * Returns the result of an authorization request for the specified user for the specified action on the specified + * resource. This method does not imply the user is directly attempting to access the specified resource. If the user is + * attempting a direct access use Authorizable.authorize(). + * + * @param authorizer authorizer + * @param action action + * @param user user + * @return is authorized + */ + default AuthorizationResult checkAuthorization(Authorizer authorizer, RequestAction action, NiFiUser user, Map resourceContext) { + if (user == null) { + throw new AccessDeniedException("Unknown user."); + } + + final AuthorizationResult resourceResult = Authorizable.super.checkAuthorization(authorizer, action, user, resourceContext); + + // if we're denied from the resource try inheriting + if (Result.Denied.equals(resourceResult.getResult()) && getParentAuthorizable() != null) { + return getParentAuthorizable().checkAuthorization(authorizer, action, user, resourceContext); + } else { + return resourceResult; + } + } + + /** + * Authorizes the current user for the specified action on the specified resource. If the current user is + * not in the access policy for the specified resource, the parent authorizable resource will be checked, recursively + * + * @param authorizer authorizer + * @param action action + * @param user user + * @param resourceContext resource context + */ + default void authorize(Authorizer authorizer, RequestAction action, NiFiUser user, Map resourceContext) throws AccessDeniedException { + if (user == null) { + throw new AccessDeniedException("Unknown user."); + } + + try { + Authorizable.super.authorize(authorizer, action, user, resourceContext); + } catch (final AccessDeniedException resourceDenied) { + // if we're denied from the resource try inheriting + try { + if (getParentAuthorizable() != null) { + getParentAuthorizable().authorize(authorizer, action, user, resourceContext); + } else { + throw resourceDenied; + } + } catch (final AccessDeniedException policiesDenied) { + throw resourceDenied; + } + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/ProxyChainAuthorizable.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/ProxyChainAuthorizable.java new file mode 100644 index 0000000000..aec8d76d97 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/ProxyChainAuthorizable.java @@ -0,0 +1,145 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.resource; + +import org.apache.nifi.registry.security.authorization.AuthorizationResult; +import org.apache.nifi.registry.security.authorization.Authorizer; +import org.apache.nifi.registry.security.authorization.RequestAction; +import org.apache.nifi.registry.security.authorization.Resource; +import org.apache.nifi.registry.security.authorization.UntrustedProxyException; +import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException; +import org.apache.nifi.registry.security.authorization.user.NiFiUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.Objects; +import java.util.function.BiFunction; + +/** + * Authorizable that wraps another Authorizable and applies logic for authorizing the proxy chain, unless the resource + * allows public access, which then skips authorizing the proxy chain. + */ +public class ProxyChainAuthorizable implements Authorizable { + + private static final Logger LOGGER = LoggerFactory.getLogger(ProxyChainAuthorizable.class); + + private final Authorizable wrappedAuthorizable; + private final Authorizable proxyAuthorizable; + private final BiFunction publicResourceCheck; + + public ProxyChainAuthorizable(final Authorizable wrappedAuthorizable, + final Authorizable proxyAuthorizable, + final BiFunction publicResourceCheck) { + this.wrappedAuthorizable = Objects.requireNonNull(wrappedAuthorizable); + this.proxyAuthorizable = Objects.requireNonNull(proxyAuthorizable); + this.publicResourceCheck = Objects.requireNonNull(publicResourceCheck); + } + + @Override + public Authorizable getParentAuthorizable() { + if (wrappedAuthorizable.getParentAuthorizable() == null) { + return null; + } else { + final Authorizable parentAuthorizable = wrappedAuthorizable.getParentAuthorizable(); + return new ProxyChainAuthorizable(parentAuthorizable, proxyAuthorizable, publicResourceCheck); + } + } + + @Override + public Resource getResource() { + return wrappedAuthorizable.getResource(); + } + + @Override + public AuthorizationResult checkAuthorization(final Authorizer authorizer, final RequestAction action, final NiFiUser user, + final Map resourceContext) { + final Resource requestResource = wrappedAuthorizable.getRequestedResource(); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Requested resource is {}", new Object[]{requestResource.getIdentifier()}); + } + + // if public access is allowed then we want to skip proxy authorization so just return + final Boolean isPublicAccessAllowed = publicResourceCheck.apply(requestResource, action); + if (isPublicAccessAllowed) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Proxy chain will not be checked, public access is allowed for {} on {}", + new Object[]{action.toString(), requestResource.getIdentifier()}); + } + return AuthorizationResult.approved(); + } + + // otherwise public access is not allowed so check the proxy chain for the given action + NiFiUser proxyUser = user.getChain(); + while (proxyUser != null) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Checking proxy [{}] for {}", new Object[]{proxyUser, action}); + } + + // if the proxy is denied then break out of the loop and return a denied result + final AuthorizationResult proxyAuthorizationResult = proxyAuthorizable.checkAuthorization(authorizer, action, proxyUser); + if (proxyAuthorizationResult.getResult() == AuthorizationResult.Result.Denied) { + final String deniedMessage = String.format("Untrusted proxy [%s] for %s operation.", proxyUser.getIdentity(), action.toString()); + return AuthorizationResult.denied(deniedMessage); + } + + proxyUser = proxyUser.getChain(); + } + + // at this point the proxy chain was approved so continue to check the original Authorizable + return wrappedAuthorizable.checkAuthorization(authorizer, action, user, resourceContext); + } + + @Override + public void authorize(final Authorizer authorizer, final RequestAction action, final NiFiUser user, + final Map resourceContext) throws AccessDeniedException { + final Resource requestResource = wrappedAuthorizable.getRequestedResource(); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Requested resource is {}", new Object[]{requestResource.getIdentifier()}); + } + + // if public access is allowed then we want to skip proxy authorization so just return + final Boolean isPublicAccessAllowed = publicResourceCheck.apply(requestResource, action); + if (isPublicAccessAllowed) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Proxy chain will not be authorized, public access is allowed for {} on {}", + new Object[]{action.toString(), requestResource.getIdentifier()}); + } + return; + } + + // otherwise public access is not allowed so authorize proxy chain for the given action + NiFiUser proxyUser = user.getChain(); + while (proxyUser != null) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Authorizing proxy [{}] for {}", new Object[]{proxyUser, action}); + } + + try { + proxyAuthorizable.authorize(authorizer, action, proxyUser); + } catch (final AccessDeniedException e) { + final String actionString = action.toString(); + throw new UntrustedProxyException(String.format("Untrusted proxy [%s] for %s operation.", proxyUser.getIdentity(), actionString)); + } + proxyUser = proxyUser.getChain(); + } + + // at this point the proxy chain was authorized so continue to authorize the original Authorizable + wrappedAuthorizable.authorize(authorizer, action, user, resourceContext); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/PublicCheckingAuthorizable.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/PublicCheckingAuthorizable.java new file mode 100644 index 0000000000..7cacb598fe --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/PublicCheckingAuthorizable.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.resource; + +import org.apache.nifi.registry.security.authorization.AuthorizationResult; +import org.apache.nifi.registry.security.authorization.Authorizer; +import org.apache.nifi.registry.security.authorization.RequestAction; +import org.apache.nifi.registry.security.authorization.Resource; +import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException; +import org.apache.nifi.registry.security.authorization.user.NiFiUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.Objects; +import java.util.function.BiFunction; + +/** + * Authorizable that first checks if public access is allowed for the resource and action. If it is then it short-circuits + * and returns approved, otherwise it continues and delegates to the wrapped Authorizable. + */ +public class PublicCheckingAuthorizable implements Authorizable { + + private static final Logger LOGGER = LoggerFactory.getLogger(PublicCheckingAuthorizable.class); + + private final Authorizable wrappedAuthorizable; + private final BiFunction publicResourceCheck; + + public PublicCheckingAuthorizable(final Authorizable wrappedAuthorizable, + final BiFunction publicResourceCheck) { + this.wrappedAuthorizable = Objects.requireNonNull(wrappedAuthorizable); + this.publicResourceCheck = Objects.requireNonNull(publicResourceCheck); + } + + @Override + public Authorizable getParentAuthorizable() { + return wrappedAuthorizable.getParentAuthorizable(); + } + + @Override + public Resource getResource() { + return wrappedAuthorizable.getResource(); + } + + @Override + public AuthorizationResult checkAuthorization(final Authorizer authorizer, final RequestAction action, final NiFiUser user, + final Map resourceContext) { + final Resource resource = getResource(); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Requested resource is {}", new Object[]{resource.getIdentifier()}); + } + + // if public access is allowed then return approved + final Boolean isPublicAccessAllowed = publicResourceCheck.apply(resource, action); + if(isPublicAccessAllowed) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Public access is allowed for {}", new Object[]{resource.getIdentifier()}); + } + return AuthorizationResult.approved(); + } + + // otherwise delegate to the original inheriting authorizable + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Delegating to inheriting authorizable for {}", new Object[]{resource.getIdentifier()}); + } + return wrappedAuthorizable.checkAuthorization(authorizer, action, user, resourceContext); + } + + @Override + public void authorize(final Authorizer authorizer, final RequestAction action, final NiFiUser user, + final Map resourceContext) throws AccessDeniedException { + final Resource resource = getResource(); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Requested resource is {}", new Object[]{resource.getIdentifier()}); + } + + // if public access is allowed then skip authorization and return + final Boolean isPublicAccessAllowed = publicResourceCheck.apply(resource, action); + if(isPublicAccessAllowed) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Public access is allowed for {}", new Object[]{resource.getIdentifier()}); + } + return; + } + + // otherwise delegate to the original authorizable + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Delegating to inheriting authorizable for {}", new Object[]{resource.getIdentifier()}); + } + + wrappedAuthorizable.authorize(authorizer, action, user, resourceContext); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/ResourceFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/ResourceFactory.java new file mode 100644 index 0000000000..c605d4a6d8 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/ResourceFactory.java @@ -0,0 +1,235 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.resource; + +import org.apache.nifi.registry.security.authorization.Resource; + +import java.util.Objects; + +public final class ResourceFactory { + + private final static Resource BUCKETS_RESOURCE = new Resource() { + @Override + public String getIdentifier() { + return ResourceType.Bucket.getValue(); + } + + @Override + public String getName() { + return "Buckets"; + } + + @Override + public String getSafeDescription() { + return "buckets"; + } + }; + + private final static Resource PROXY_RESOURCE = new Resource() { + @Override + public String getIdentifier() { + return ResourceType.Proxy.getValue(); + } + + @Override + public String getName() { + return "Proxy User Requests"; + } + + @Override + public String getSafeDescription() { + return "proxy requests on behalf of users"; + } + }; + + private final static Resource TENANTS_RESOURCE = new Resource() { + @Override + public String getIdentifier() { + return ResourceType.Tenant.getValue(); + } + + @Override + public String getName() { + return "Tenants"; + } + + @Override + public String getSafeDescription() { + return "users/user groups"; + } + }; + + private final static Resource POLICIES_RESOURCE = new Resource() { + + @Override + public String getIdentifier() { + return ResourceType.Policy.getValue(); + } + + @Override + public String getName() { + return "Access Policies"; + } + + @Override + public String getSafeDescription() { + return "policies"; + } + }; + + private final static Resource ACTUATOR_RESOURCE = new Resource() { + @Override + public String getIdentifier() { + return ResourceType.Actuator.getValue(); + } + + @Override + public String getName() { + return "Actuator"; + } + + @Override + public String getSafeDescription() { + return "actuator"; + } + }; + + private final static Resource SWAGGER_RESOURCE = new Resource() { + @Override + public String getIdentifier() { + return ResourceType.Swagger.getValue(); + } + + @Override + public String getName() { + return "Swagger"; + } + + @Override + public String getSafeDescription() { + return "swagger"; + } + }; + + /** + * Gets the Resource for actuator system management endpoints. + * + * @return The resource for actuator system management endpoints. + */ + public static Resource getActuatorResource() { + return ACTUATOR_RESOURCE; + } + + /** + * Gets the Resource for swagger UI static resources. + * + * @return The resource for swagger UI static resources. + */ + public static Resource getSwaggerResource() { + return SWAGGER_RESOURCE; + } + + /** + * Gets the Resource for proxying a user request. + * + * @return The resource for proxying a user request + */ + public static Resource getProxyResource() { + return PROXY_RESOURCE; + } + + /** + * Gets the Resource for accessing Tenants which includes creating, modifying, and deleting Users and UserGroups. + * + * @return The Resource for accessing Tenants + */ + public static Resource getTenantsResource() { + return TENANTS_RESOURCE; + } + + /** + * Gets the {@link Resource} for accessing access policies. + * @return The policies resource + */ + public static Resource getPoliciesResource() { + return POLICIES_RESOURCE; + } + + /** + * Gets the {@link Resource} for accessing buckets. + * @return The buckets resource + */ + public static Resource getBucketsResource() { + return BUCKETS_RESOURCE; + } + + /** + * Gets the {@link Resource} for accessing buckets. + * @return The buckets resource + */ + public static Resource getBucketResource(String bucketIdentifier, String bucketName) { + return getChildResource(ResourceType.Bucket, bucketIdentifier, bucketName); + } + + /** + * Get a Resource object for any object that has a base type and an identifier, ie: + * /buckets/{uuid} + * + * @param parentResourceType - Required, the base resource type + * @param childIdentifier - Required, the identity of this sub resource + * @param name - Optional, the name of the subresource + * @return A resource for this object + */ + private static Resource getChildResource(final ResourceType parentResourceType, final String childIdentifier, final String name) { + Objects.requireNonNull(parentResourceType, "The base resource type must be specified."); + Objects.requireNonNull(childIdentifier, "The child identifier identifier must be specified."); + + return new Resource() { + @Override + public String getIdentifier() { + return String.format("%s/%s", parentResourceType.getValue(), childIdentifier); + } + + @Override + public String getName() { + return name; + } + + @Override + public String getSafeDescription() { + final StringBuilder safeDescription = new StringBuilder(); + switch (parentResourceType) { + case Bucket: + safeDescription.append("Bucket"); + break; + default: + safeDescription.append("Unknown resource type"); + break; + } + safeDescription.append(" with ID "); + safeDescription.append(childIdentifier); + return safeDescription.toString(); + } + }; + + } + + /** + * Prevent outside instantiation. + */ + private ResourceFactory() {} +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/ResourceType.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/ResourceType.java new file mode 100644 index 0000000000..0b77cd20a2 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/ResourceType.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.resource; + +public enum ResourceType { + Bucket("/buckets"), + Policy("/policies"), + Proxy("/proxy"), + Tenant("/tenants"), + Actuator("/actuator"), + Swagger("/swagger"); + + final String value; + + private ResourceType(final String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static ResourceType valueOfValue(final String rawValue) { + ResourceType type = null; + + for (final ResourceType rt : values()) { + if (rt.getValue().equals(rawValue)) { + type = rt; + break; + } + } + + if (type == null) { + throw new IllegalArgumentException("Unknown resource type value " + rawValue); + } + + return type; + } + + /** + * Map an arbitrary resource path to its base resource type. The base resource type is + * what the resource path starts with. + * + * The resourcePath arg is expected to be a string of the format: + * + * {ResourceTypeValue}/arbitrary/sub-resource/path + * + * For example: + * /buckets -> ResourceType.Bucket + * /buckets/bucketId -> ResourceType.Bucket + * /policies/read/buckets -> ResourceType.Policy + * + * @param resourcePath the path component of a URI (not including the context path) + * @return the base resource type + */ + public static ResourceType mapFullResourcePathToResourceType(final String resourcePath) { + if (resourcePath == null) { + throw new IllegalArgumentException("Resource path must not be null"); + } + + ResourceType type = null; + + for (final ResourceType rt : values()) { + final String rtValue = rt.getValue(); + if(resourcePath.equals(rtValue) || resourcePath.startsWith(rtValue + "/")) { + type = rt; + break; + } + } + + return type; + } +} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/NssShellCommands.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/NssShellCommands.java new file mode 100644 index 0000000000..eef58b0558 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/NssShellCommands.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.shell; + +/** + * Provides shell commands to read users and groups on NSS-enabled systems. + * + * See `man 5 nsswitch.conf` for more info. + */ +class NssShellCommands implements ShellCommandsProvider { + /** + * @return Shell command string that will return a list of users. + */ + public String getUsersList() { + return "getent passwd | cut -f 1,3,4 -d ':'"; + } + + /** + * @return Shell command string that will return a list of groups. + */ + public String getGroupsList() { + return "getent group | cut -f 1,3 -d ':'"; + } + + /** + * @param groupName name of group. + * @return Shell command string that will return a list of users for a group. + */ + public String getGroupMembers(String groupName) { + return String.format("getent group %s | cut -f 4 -d ':'", groupName); + } + + /** + * Gets the command for reading a single user by id. + * + * When executed, this command should output a single line, in the format used by `getUsersList`. + * + * @param userId name of user. + * @return Shell command string that will read a single user. + */ + @Override + public String getUserById(String userId) { + return String.format("getent passwd %s | cut -f 1,3,4 -d ':'", userId); + } + + /** + * This method reuses `getUserById` because the getent command is the same for + * both uid and username. + * + * @param userName name of user. + * @return Shell command string that will read a single user. + */ + public String getUserByName(String userName) { + return getUserById(userName); + } + + /** + * This method supports gid or group name because getent does. + * + * @param groupId name of group. + * @return Shell command string that will read a single group. + */ + public String getGroupById(String groupId) { + return String.format("getent group %s | cut -f 1,3,4 -d ':'", groupId); + } + + /** + * This gives exit code 0 on all tested distributions. + * + * @return Shell command string that will exit normally (0) on a suitable system. + */ + public String getSystemCheck() { + return "getent --version"; + } +} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/OsxShellCommands.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/OsxShellCommands.java new file mode 100644 index 0000000000..059166235a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/OsxShellCommands.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.shell; + +/** + * Provides shell commands to read users and groups on Mac OSX systems. + * + * See `man dscl` for more info. + */ +class OsxShellCommands implements ShellCommandsProvider { + /** + * @return Shell command string that will return a list of users. + */ + public String getUsersList() { + return "dscl . -readall /Users UniqueID PrimaryGroupID | awk 'BEGIN { OFS = \":\"; ORS=\"\\n\"; i=0;} /RecordName: / {name = $2;i = 0;}" + + "/PrimaryGroupID: / {gid = $2;} /^ / {if (i == 0) { i++; name = $1;}} /UniqueID: / {uid = $2;print name, uid, gid;}' | grep -v ^_"; + } + + /** + * @return Shell command string that will return a list of groups. + */ + public String getGroupsList() { + return "dscl . -list /Groups PrimaryGroupID | grep -v '^_' | sed 's/ \\{1,\\}/:/g'"; + } + + /** + * + * @param groupName name of group. + * @return Shell command string that will return a list of users for a group. + */ + public String getGroupMembers(String groupName) { + return String.format("dscl . -read /Groups/%s GroupMembership | cut -f 2- -d ' ' | sed 's/\\ /,/g'", groupName); + } + + /** + * @param userId name of user. + * @return Shell command string that will read a single user. + */ + @Override + public String getUserById(String userId) { + return String.format("id -P %s | cut -f 1,3,4 -d ':'", userId); + } + + /** + * @param userName name of user. + * @return Shell command string that will read a single user. + */ + public String getUserByName(String userName) { + return getUserById(userName); // 'id' command works for both uid/username + } + + /** + * @param groupId name of group. + * @return Shell command string that will read a single group. + */ + public String getGroupById(String groupId) { + return String.format(" dscl . -read /Groups/`dscl . -search /Groups gid %s | head -n 1 | cut -f 1` RecordName PrimaryGroupID | awk 'BEGIN { OFS = \":\"; ORS=\"\\n\"; i=0;} " + + "/RecordName: / {name = $2;i = 1;}/PrimaryGroupID: / {gid = $2;}; {if (i==1) {print name,gid,\"\"}}'", groupId); + } + + /** + * @return Shell command string that will exit normally (0) on a suitable system. + */ + public String getSystemCheck() { + return "which dscl"; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/RemoteShellCommands.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/RemoteShellCommands.java new file mode 100644 index 0000000000..f622409bc7 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/RemoteShellCommands.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.shell; + +class RemoteShellCommands implements ShellCommandsProvider { + // Carefully crafted command replacement string: + private final static String remoteCommand = "ssh " + + "-o 'StrictHostKeyChecking no' " + + "-o 'PasswordAuthentication no' " + + "-o \"RemoteCommand %s\" " + + "-i %s -p %s -l root %s"; + + private ShellCommandsProvider innerProvider; + private String privateKeyPath; + private String remoteHost; + private Integer remotePort; + + private RemoteShellCommands() { + } + + public static ShellCommandsProvider wrapOtherProvider(ShellCommandsProvider otherProvider, String keyPath, String host, Integer port) { + RemoteShellCommands remote = new RemoteShellCommands(); + + remote.innerProvider = otherProvider; + remote.privateKeyPath = keyPath; + remote.remoteHost = host; + remote.remotePort = port; + + return remote; + } + + public String getUsersList() { + return String.format(remoteCommand, innerProvider.getUsersList(), privateKeyPath, remotePort, remoteHost); + } + + public String getGroupsList() { + return String.format(remoteCommand, innerProvider.getGroupsList(), privateKeyPath, remotePort, remoteHost); + } + + public String getGroupMembers(String groupName) { + return String.format(remoteCommand, innerProvider.getGroupMembers(groupName), privateKeyPath, remotePort, remoteHost); + } + + public String getUserById(String userId) { + return String.format(remoteCommand, innerProvider.getUserById(userId), privateKeyPath, remotePort, remoteHost); + } + + public String getUserByName(String userName) { + return String.format(remoteCommand, innerProvider.getUserByName(userName), privateKeyPath, remotePort, remoteHost); + } + + public String getGroupById(String groupId) { + return String.format(remoteCommand, innerProvider.getGroupById(groupId), privateKeyPath, remotePort, remoteHost); + } + + public String getSystemCheck() { + return String.format(remoteCommand, innerProvider.getSystemCheck(), privateKeyPath, remotePort, remoteHost); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/ShellCommandsProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/ShellCommandsProvider.java new file mode 100644 index 0000000000..ce3e6a4d38 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/ShellCommandsProvider.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.shell; + +/** + * Common interface for shell command strings to read users and groups. + * + */ +interface ShellCommandsProvider { + /** + * Gets the command for listing users. + * + * When executed, this command should output one record per line in this format: + * + * `username:user-id:primary-group-id` + * + * @return Shell command string that will return a list of users. + */ + String getUsersList(); + + /** + * Gets the command for listing groups. + * + * When executed, this command should output one record per line in this format: + * + * `group-name:group-id` + * + * @return Shell command string that will return a list of groups. + */ + String getGroupsList(); + + /** + * Gets the command for listing the members of a group. + * + * When executed, this command should output one line in this format: + * + * `user-name-1,user-name-2,user-name-n` + * + * @param groupName name of group. + * @return Shell command string that will return a list of users for a group. + */ + String getGroupMembers(String groupName); + + /** + * Gets the command for reading a single user by id. Implementations may return null if reading a single + * user by id is not supported. + * + * When executed, this command should output a single line, in the format used by `getUsersList`. + * + * @param userId name of user. + * @return Shell command string that will read a single user. + */ + String getUserById(String userId); + + /** + * Gets the command for reading a single user. Implementations may return null if reading a single user by + * username is not supported. + * + * When executed, this command should output a single line, in the format used by `getUsersList`. + * + * @param userName name of user. + * @return Shell command string that will read a single user. + */ + String getUserByName(String userName); + + /** + * Gets the command for reading a single group. Implementations may return null if reading a single group + * by name is not supported. + * + * When executed, this command should output a single line, in the format used by `getGroupsList`. + * + * @param groupId name of group. + * @return Shell command string that will read a single group. + */ + String getGroupById(String groupId); + + /** + * Gets the command for checking the suitability of the host system. + * + * The command is expected to exit with status 0 (zero) to indicate success, and any other status + * to indicate failure. + * + * @return Shell command string that will exit normally (0) on a suitable system. + */ + String getSystemCheck(); +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/ShellRunner.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/ShellRunner.java new file mode 100644 index 0000000000..de38b631f6 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/ShellRunner.java @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.shell; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +public class ShellRunner { + private final static Logger logger = LoggerFactory.getLogger(ShellRunner.class); + + static String SHELL = "sh"; + static String OPTS = "-c"; + + private final int timeoutSeconds; + private final ExecutorService executor; + + public ShellRunner(final int timeoutSeconds) { + this.timeoutSeconds = timeoutSeconds; + this.executor = Executors.newFixedThreadPool(1, new ThreadFactory() { + @Override + public Thread newThread(final Runnable r) { + final Thread t = Executors.defaultThreadFactory().newThread(r); + t.setName("ShellRunner"); + t.setDaemon(true); + return t; + } + }); + } + + public List runShell(String command) throws IOException { + return runShell(command, ""); + } + + public List runShell(String command, String description) throws IOException { + final ProcessBuilder builder = new ProcessBuilder(SHELL, OPTS, command); + builder.redirectErrorStream(true); + + final List builderCommand = builder.command(); + logger.debug("Run Command '{}': {}", new Object[]{description, builderCommand}); + + final Process proc = builder.start(); + + final List lines = new ArrayList<>(); + executor.submit(() -> { + try { + try (final Reader stdin = new InputStreamReader(proc.getInputStream()); + final BufferedReader reader = new BufferedReader(stdin)) { + logger.trace("Reading process input stream..."); + + String line; + int lineCount = 0; + while ((line = reader.readLine()) != null) { + if (logger.isTraceEnabled()) { + logger.trace((++lineCount) + " - " + line); + } + lines.add(line.trim()); + } + + logger.trace("Finished reading process input stream"); + } + } catch (IOException e) { + logger.error(e.getMessage(), e); + } + }); + + boolean completed; + try { + completed = proc.waitFor(timeoutSeconds, TimeUnit.SECONDS); + } catch (InterruptedException irexc) { + throw new IOException(irexc.getMessage(), irexc.getCause()); + } + + if (!completed) { + logger.debug("Process did not complete in allotted time, attempting to forcibly destroy process..."); + try { + proc.destroyForcibly(); + } catch (Exception e) { + logger.debug("Process failed to destroy: " + e.getMessage(), e); + } + throw new IllegalStateException("Shell command '" + command + "' did not complete during the allotted time period"); + } + + if (proc.exitValue() != 0) { + throw new IOException("Process exited with non-zero value: " + proc.exitValue()); + } + + return lines; + } + + public void shutdown() { + executor.shutdown(); + try { + if (!executor.awaitTermination(5000L, TimeUnit.MILLISECONDS)) { + logger.info("Failed to stop ShellRunner executor in 5 seconds. Terminating"); + executor.shutdownNow(); + } + } catch (InterruptedException ie) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/ShellUserGroupProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/ShellUserGroupProvider.java new file mode 100644 index 0000000000..4e201d2fe3 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/shell/ShellUserGroupProvider.java @@ -0,0 +1,719 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.shell; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.security.authorization.AuthorizerConfigurationContext; +import org.apache.nifi.registry.security.authorization.Group; +import org.apache.nifi.registry.security.authorization.User; +import org.apache.nifi.registry.security.authorization.UserAndGroups; +import org.apache.nifi.registry.security.authorization.UserGroupProvider; +import org.apache.nifi.registry.security.authorization.UserGroupProviderInitializationContext; +import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException; +import org.apache.nifi.registry.util.FormatUtils; +import org.apache.nifi.registry.util.PropertyValue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/* + * ShellUserGroupProvider implements UserGroupProvider by way of shell commands. + */ +public class ShellUserGroupProvider implements UserGroupProvider { + private final static Logger logger = LoggerFactory.getLogger(ShellUserGroupProvider.class); + + private final static String OS_TYPE_ERROR = "Unsupported operating system."; + private final static String SYS_CHECK_ERROR = "System check failed - cannot provide users and groups."; + private final static Map usersById = new HashMap<>(); // id == identifier + private final static Map usersByName = new HashMap<>(); // name == identity + private final static Map groupsById = new HashMap<>(); + + public static final String REFRESH_DELAY_PROPERTY = "Refresh Delay"; + private static final long MINIMUM_SYNC_INTERVAL_MILLISECONDS = 10_000; + + public static final String EXCLUDE_USER_PROPERTY = "Exclude Users"; + public static final String EXCLUDE_GROUP_PROPERTY = "Exclude Groups"; + + public static final String COMMAND_TIMEOUT_PROPERTY = "Command Timeout"; + + private static final String DEFAULT_COMMAND_TIMEOUT = "60 seconds"; + + private long fixedDelay; + private Pattern excludeUsers; + private Pattern excludeGroups; + private int timeoutSeconds; + + // Our scheduler has one thread for users, one for groups: + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2); + + // Commands selected during initialization: + private ShellCommandsProvider selectedShellCommands; + + private ShellRunner shellRunner; + + // Start of the UserGroupProvider implementation. Javadoc strings + // copied from the interface definition for reference. + + /** + * Retrieves all users. Must be non null + * + * @return a list of users + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + @Override + public Set getUsers() throws AuthorizationAccessException { + synchronized (usersById) { + logger.debug("getUsers has user set of size: " + usersById.size()); + return new HashSet<>(usersById.values()); + } + } + + /** + * Retrieves the user with the given identifier. + * + * @param identifier the id of the user to retrieve + * @return the user with the given id, or null if no matching user was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + @Override + public User getUser(String identifier) throws AuthorizationAccessException { + User user; + + synchronized (usersById) { + user = usersById.get(identifier); + } + + if (user == null) { + logger.debug("getUser (by id) user not found: " + identifier); + } else { + logger.debug("getUser (by id) found user: " + user + " for id: " + identifier); + } + return user; + } + + /** + * Retrieves the user with the given identity. + * + * @param identity the identity of the user to retrieve + * @return the user with the given identity, or null if no matching user was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + @Override + public User getUserByIdentity(String identity) throws AuthorizationAccessException { + User user; + + synchronized (usersByName) { + user = usersByName.get(identity); + } + + if (user == null) { + refreshOneUser(selectedShellCommands.getUserByName(identity), "Get Single User by Name"); + user = usersByName.get(identity); + } + + if (user == null) { + logger.debug("getUser (by name) user not found: " + identity); + } else { + logger.debug("getUser (by name) found user: " + user.getIdentity() + " for name: " + identity); + } + return user; + } + + /** + * Retrieves all groups. Must be non null + * + * @return a list of groups + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + @Override + public Set getGroups() throws AuthorizationAccessException { + synchronized (groupsById) { + logger.debug("getGroups has group set of size: " + groupsById.size()); + return new HashSet<>(groupsById.values()); + } + } + + /** + * Retrieves a Group by Id. + * + * @param identifier the identifier of the Group to retrieve + * @return the Group with the given identifier, or null if no matching group was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + @Override + public Group getGroup(String identifier) throws AuthorizationAccessException { + Group group; + + synchronized (groupsById) { + group = groupsById.get(identifier); + } + + if (group == null) { + refreshOneGroup(selectedShellCommands.getGroupById(identifier), "Get Single Group by Id"); + group = groupsById.get(identifier); + } + + if (group == null) { + logger.debug("getGroup (by id) group not found: " + identifier); + } else { + logger.debug("getGroup (by id) found group: " + group.getName() + " for id: " + identifier); + } + return group; + + } + + /** + * Gets a user and their groups. + * + * @return the UserAndGroups for the specified identity + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + @Override + public UserAndGroups getUserAndGroups(String identity) throws AuthorizationAccessException { + User user = getUserByIdentity(identity); + logger.debug("Retrieved user {} for identity {}", new Object[]{user, identity}); + + Set groups = new HashSet<>(); + if (user != null) { + for (Group g : getGroups()) { + if (g.getUsers().contains(user.getIdentifier())) { + logger.debug("User {} belongs to group {}", new Object[]{user.getIdentity(), g.getName()}); + groups.add(g); + } + } + } + + if (groups.isEmpty()) { + logger.debug("User {} belongs to no groups", user); + } + + return new UserAndGroups() { + @Override + public User getUser() { + return user; + } + + @Override + public Set getGroups() { + return groups; + } + }; + } + + /** + * Called immediately after instance creation for implementers to perform additional setup + * + * @param initializationContext in which to initialize + */ + @Override + public void initialize(UserGroupProviderInitializationContext initializationContext) throws SecurityProviderCreationException { + } + + /** + * Called to configure the Authorizer. + * + * @param configurationContext at the time of configuration + * @throws SecurityProviderCreationException for any issues configuring the provider + */ + @Override + public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException { + logger.info("Configuring ShellUserGroupProvider"); + + fixedDelay = getDelayProperty(configurationContext, REFRESH_DELAY_PROPERTY, "5 mins"); + timeoutSeconds = getTimeoutProperty(configurationContext, COMMAND_TIMEOUT_PROPERTY, DEFAULT_COMMAND_TIMEOUT); + shellRunner = new ShellRunner(timeoutSeconds); + logger.debug("Configured ShellRunner with command timeout of '{}' seconds", new Object[]{timeoutSeconds}); + + + // Our next init step is to select the command set based on the operating system name: + ShellCommandsProvider commands = getCommandsProvider(); + + if (commands == null) { + commands = getCommandsProviderFromName(null); + setCommandsProvider(commands); + } + + // Our next init step is to run the system check from that command set to determine if the other commands + // will work on this host or not. + try { + shellRunner.runShell(commands.getSystemCheck()); + } catch (final Exception e) { + logger.error("initialize exception: " + e + " system check command: " + commands.getSystemCheck()); + throw new SecurityProviderCreationException(SYS_CHECK_ERROR, e); + } + + // The next step is to add the user and group exclude regexes: + try { + excludeGroups = Pattern.compile(getProperty(configurationContext, EXCLUDE_GROUP_PROPERTY, "")); + excludeUsers = Pattern.compile(getProperty(configurationContext, EXCLUDE_USER_PROPERTY, "")); + } catch (final PatternSyntaxException e) { + throw new SecurityProviderCreationException(e); + } + + // With our command set selected, and our system check passed, we can pull in the users and groups: + refreshUsersAndGroups(); + + // And finally, our last init step is to fire off the refresh thread: + scheduler.scheduleWithFixedDelay(() -> { + try { + refreshUsersAndGroups(); + }catch (final Throwable t) { + logger.error("", t); + } + }, fixedDelay, fixedDelay, TimeUnit.MILLISECONDS); + + logger.info("Completed configuration of ShellUserGroupProvider"); + } + + private static ShellCommandsProvider getCommandsProviderFromName(String osName) { + if (osName == null) { + osName = System.getProperty("os.name"); + } + + ShellCommandsProvider commands; + if (osName.startsWith("Linux")) { + logger.debug("Selected Linux command set."); + commands = new NssShellCommands(); + } else if (osName.startsWith("Mac OS X")) { + logger.debug("Selected OSX command set."); + commands = new OsxShellCommands(); + } else { + throw new SecurityProviderCreationException(OS_TYPE_ERROR); + } + return commands; + } + + private String getProperty(AuthorizerConfigurationContext authContext, String propertyName, String defaultValue) { + final PropertyValue property = authContext.getProperty(propertyName); + final String value; + + if (property != null && property.isSet()) { + value = property.getValue(); + } else { + value = defaultValue; + } + return value; + + } + + private long getDelayProperty(AuthorizerConfigurationContext authContext, String propertyName, String defaultValue) { + final PropertyValue intervalProperty = authContext.getProperty(propertyName); + final String propertyValue; + final long syncInterval; + + if (intervalProperty.isSet()) { + propertyValue = intervalProperty.getValue(); + } else { + propertyValue = defaultValue; + } + + try { + syncInterval = Math.round(FormatUtils.getPreciseTimeDuration(propertyValue, TimeUnit.MILLISECONDS)); + } catch (final IllegalArgumentException ignored) { + throw new SecurityProviderCreationException(String.format("The %s '%s' is not a valid time interval.", propertyName, propertyValue)); + } + + if (syncInterval < MINIMUM_SYNC_INTERVAL_MILLISECONDS) { + throw new SecurityProviderCreationException(String.format("The %s '%s' is below the minimum value of '%d ms'", propertyName, propertyValue, MINIMUM_SYNC_INTERVAL_MILLISECONDS)); + } + return syncInterval; + } + + private int getTimeoutProperty(AuthorizerConfigurationContext authContext, String propertyName, String defaultValue) { + final PropertyValue timeoutProperty = authContext.getProperty(propertyName); + + final String propertyValue; + if (timeoutProperty.isSet()) { + propertyValue = timeoutProperty.getValue(); + } else { + propertyValue = defaultValue; + } + + final long timeoutValue; + try { + timeoutValue = Math.round(FormatUtils.getPreciseTimeDuration(propertyValue, TimeUnit.SECONDS)); + } catch (final IllegalArgumentException ignored) { + throw new SecurityProviderCreationException(String.format("The %s '%s' is not a valid time interval.", propertyName, propertyValue)); + } + + return Math.toIntExact(timeoutValue); + } + + /** + * Called immediately before instance destruction for implementers to release resources. + * + * @throws SecurityProviderDestructionException If pre-destruction fails. + */ + @Override + public void preDestruction() throws SecurityProviderDestructionException { + try { + scheduler.shutdownNow(); + } catch (final Exception e) { + logger.warn("Error shutting down refresh scheduler: " + e.getMessage(), e); + } + try { + shellRunner.shutdown(); + } catch (final Exception e) { + logger.warn("Error shutting down ShellRunner: " + e.getMessage(), e); + } + } + + public ShellCommandsProvider getCommandsProvider() { + return selectedShellCommands; + } + + public void setCommandsProvider(ShellCommandsProvider commandsProvider) { + selectedShellCommands = commandsProvider; + } + + /** + * Refresh a single user. + * + * @param command Shell command to read a single user. Pre-formatted by caller. + * @param description Shell command description. + */ + private void refreshOneUser(String command, String description) { + if (command != null) { + Map idToUser = new HashMap<>(); + Map usernameToUser = new HashMap<>(); + Map gidToUser = new HashMap<>(); + List userLines; + + try { + userLines = shellRunner.runShell(command, description); + rebuildUsers(userLines, idToUser, usernameToUser, gidToUser); + } catch (final IOException ioexc) { + logger.error("refreshOneUser shell exception: " + ioexc); + } + + if (idToUser.size() > 0) { + synchronized (usersById) { + usersById.putAll(idToUser); + } + } + + if (usernameToUser.size() > 0) { + synchronized (usersByName) { + usersByName.putAll(usernameToUser); + } + } + } else { + logger.info("Get Single User not supported on this system."); + } + } + + /** + * Refresh a single group. + * + * @param command Shell command to read a single group. Pre-formatted by caller. + * @param description Shell command description. + */ + private void refreshOneGroup(String command, String description) { + if (command != null) { + Map gidToGroup = new HashMap<>(); + List groupLines; + + try { + groupLines = shellRunner.runShell(command, description); + rebuildGroups(groupLines, gidToGroup); + } catch (final IOException ioexc) { + logger.error("refreshOneGroup shell exception: " + ioexc); + } + + if (gidToGroup.size() > 0) { + synchronized (groupsById) { + groupsById.putAll(gidToGroup); + } + } + } else { + logger.info("Get Single Group not supported on this system."); + } + } + + /** + * This is our entry point for user and group refresh. This method runs the top-level + * `getUserList()` and `getGroupsList()` shell commands, then passes those results to the + * other methods for record parse, extract, and object construction. + */ + private void refreshUsersAndGroups() { + final long startTime = System.currentTimeMillis(); + + Map uidToUser = new HashMap<>(); + Map usernameToUser = new HashMap<>(); + Map gidToUser = new HashMap<>(); + Map gidToGroup = new HashMap<>(); + + List userLines; + List groupLines; + + try { + userLines = shellRunner.runShell(selectedShellCommands.getUsersList(), "Get Users List"); + groupLines = shellRunner.runShell(selectedShellCommands.getGroupsList(), "Get Groups List"); + } catch (final IOException ioexc) { + logger.error("refreshUsersAndGroups shell exception: " + ioexc); + return; + } + + rebuildUsers(userLines, uidToUser, usernameToUser, gidToUser); + rebuildGroups(groupLines, gidToGroup); + reconcilePrimaryGroups(gidToUser, gidToGroup); + + synchronized (usersById) { + usersById.clear(); + usersById.putAll(uidToUser); + + if (logger.isTraceEnabled()) { + logger.trace("=== Users by id..."); + Set sortedUsers = new TreeSet<>(Comparator.comparing(User::getIdentity)); + sortedUsers.addAll(usersById.values()); + sortedUsers.forEach(u -> logger.trace("=== " + u.toString())); + } + } + + synchronized (usersByName) { + usersByName.clear(); + usersByName.putAll(usernameToUser); + logger.debug("users now size: " + usersByName.size()); + } + + synchronized (groupsById) { + groupsById.clear(); + groupsById.putAll(gidToGroup); + logger.debug("groups now size: " + groupsById.size()); + + if (logger.isTraceEnabled()) { + logger.trace("=== Groups by id..."); + Set sortedGroups = new TreeSet<>(Comparator.comparing(Group::getName)); + sortedGroups.addAll(groupsById.values()); + sortedGroups.forEach(g -> logger.trace("=== " + g.toString())); + } + } + + final long endTime = System.currentTimeMillis(); + logger.info("Refreshed users and groups, took {} seconds", (endTime - startTime) / 1000); + } + + /** + * This method parses the output of the `getUsersList()` shell command, where we expect the output + * to look like `user-name:user-id:primary-group-id`. + *

+ * This method splits each output line on the ":" and attempts to build a User object + * from the resulting name, uid, and primary gid. Unusable records are logged. + */ + private void rebuildUsers(List userLines, Map idToUser, Map usernameToUser, Map gidToUser) { + userLines.forEach(line -> { + logger.trace("Processing user: {}", new Object[]{line}); + + String[] record = line.split(":"); + if (record.length > 2) { + String userIdentity = record[0], userIdentifier = record[1], primaryGroupIdentifier = record[2]; + + if (!StringUtils.isBlank(userIdentifier) && !StringUtils.isBlank(userIdentity) && !excludeUsers.matcher(userIdentity).matches()) { + User user = new User.Builder() + .identity(userIdentity) + .identifierGenerateFromSeed(getUserIdentifierSeed(userIdentity)) + .build(); + idToUser.put(user.getIdentifier(), user); + usernameToUser.put(userIdentity, user); + logger.debug("Refreshed user {}", new Object[]{user}); + + if (!StringUtils.isBlank(primaryGroupIdentifier)) { + // create a temporary group to deterministically generate the group id and associate this user + Group group = new Group.Builder() + .name(primaryGroupIdentifier) + .identifierGenerateFromSeed(getGroupIdentifierSeed(primaryGroupIdentifier)) + .build(); + gidToUser.put(group.getIdentifier(), user); + logger.debug("Associated primary group {} with user {}", new Object[]{group.getIdentifier(), userIdentity}); + } else { + logger.warn("Null or empty primary group id for: " + userIdentity); + } + + } else { + logger.warn("Null, empty, or skipped user name: " + userIdentity + " or id: " + userIdentifier); + } + } else { + logger.warn("Unexpected record format. Expected 3 or more colon separated values per line."); + } + }); + } + + /** + * This method parses the output of the `getGroupsList()` shell command, where we expect the output + * to look like `group-name:group-id`. + *

+ * This method splits each output line on the ":" and attempts to build a Group object + * from the resulting name and gid. Unusable records are logged. + *

+ * This command also runs the `getGroupMembers(username)` command once per group. The expected output + * of that command should look like `group-name-1,group-name-2`. + */ + private void rebuildGroups(List groupLines, Map groupsById) { + groupLines.forEach(line -> { + logger.trace("Processing group: {}", new Object[]{line}); + + String[] record = line.split(":"); + if (record.length > 1) { + Set users = new HashSet<>(); + String groupName = record[0], groupIdentifier = record[1]; + + try { + String groupMembersCommand = selectedShellCommands.getGroupMembers(groupName); + List memberLines = shellRunner.runShell(groupMembersCommand); + // Use the first line only, and log if the line count isn't exactly one: + if (!memberLines.isEmpty()) { + String memberLine = memberLines.get(0); + if (!StringUtils.isBlank(memberLine)) { + String[] members = memberLine.split(","); + for (String userIdentity : members) { + if (!StringUtils.isBlank(userIdentity)) { + User tempUser = new User.Builder() + .identity(userIdentity) + .identifierGenerateFromSeed(getUserIdentifierSeed(userIdentity)) + .build(); + users.add(tempUser.getIdentifier()); + logger.debug("Added temp user {} for group {}", new Object[]{tempUser, groupName}); + } + } + } else { + logger.debug("list membership returned no members"); + } + } else { + logger.debug("list membership returned zero lines."); + } + if (memberLines.size() > 1) { + logger.error("list membership returned too many lines, only used the first."); + } + + } catch (final IOException ioexc) { + logger.error("list membership shell exception: " + ioexc); + } + + if (!StringUtils.isBlank(groupIdentifier) && !StringUtils.isBlank(groupName) && !excludeGroups.matcher(groupName).matches()) { + Group group = new Group.Builder() + .name(groupName) + .identifierGenerateFromSeed(getGroupIdentifierSeed(groupIdentifier)) + .addUsers(users) + .build(); + groupsById.put(group.getIdentifier(), group); + logger.debug("Refreshed group {}", new Object[] {group}); + } else { + logger.warn("Null, empty, or skipped group name: " + groupName + " or id: " + groupIdentifier); + } + } else { + logger.warn("Unexpected record format. Expected 1 or more comma separated values."); + } + }); + } + + /** + * This method parses the output of the `getGroupsList()` shell command, where we expect the output + * to look like `group-name:group-id`. + *

+ * This method splits each output line on the ":" and attempts to build a Group object + * from the resulting name and gid. + */ + private void reconcilePrimaryGroups(Map uidToUser, Map gidToGroup) { + uidToUser.forEach((primaryGid, primaryUser) -> { + Group primaryGroup = gidToGroup.get(primaryGid); + + if (primaryGroup == null) { + logger.warn("Primary group {} not found for {}", new Object[]{primaryGid, primaryUser.getIdentity()}); + } else if (!excludeGroups.matcher(primaryGroup.getName()).matches()) { + Set groupUsers = primaryGroup.getUsers(); + if (!groupUsers.contains(primaryUser.getIdentifier())) { + Set updatedUserIdentifiers = new HashSet<>(groupUsers); + updatedUserIdentifiers.add(primaryUser.getIdentifier()); + + Group updatedGroup = new Group.Builder() + .identifier(primaryGroup.getIdentifier()) + .name(primaryGroup.getName()) + .addUsers(updatedUserIdentifiers) + .build(); + gidToGroup.put(updatedGroup.getIdentifier(), updatedGroup); + logger.debug("Added user {} to primary group {}", new Object[]{primaryUser, updatedGroup}); + } else { + logger.debug("Primary group {} already contains user {}", new Object[]{primaryGroup, primaryUser}); + } + } else { + logger.debug("Primary group {} excluded from matcher for {}", new Object[]{primaryGroup.getName(), primaryUser.getIdentity()}); + } + }); + } + + private String getUserIdentifierSeed(final String userIdentifier) { + return userIdentifier + "-user"; + } + + private String getGroupIdentifierSeed(final String groupIdentifier) { + return groupIdentifier + "-group"; + } + + + /** + * @return The fixed refresh delay. + */ + public long getRefreshDelay() { + return fixedDelay; + } + + /** + * Testing concession for clearing the internal caches. + */ + void clearCaches() { + synchronized (usersById) { + usersById.clear(); + } + + synchronized (usersByName) { + usersByName.clear(); + } + + synchronized (groupsById) { + groupsById.clear(); + } + } + + /** + * @return The size of the internal user cache. + */ + public int userCacheSize() { + return usersById.size(); + } + + /** + * @return The size of the internal group cache. + */ + public int groupCacheSize() { + return groupsById.size(); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/user/NiFiUser.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/user/NiFiUser.java new file mode 100644 index 0000000000..47127b63c6 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/user/NiFiUser.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.user; + +import java.util.Set; + +/** + * A representation of a NiFi user that has logged into the application + */ +public interface NiFiUser { + + /** + * @return the unique identity of this user + */ + String getIdentity(); + + /** + * @return the groups that this user belongs to if this nifi is configured to load user groups, null otherwise. + */ + Set getGroups(); + + /** + * @return the next user in the proxied entities chain, or null if no more users exist in the chain. + */ + NiFiUser getChain(); + + /** + * @return true if the user is the unauthenticated Anonymous user + */ + boolean isAnonymous(); + + /** + * @return the address of the client that made the request which created this user + */ + String getClientAddress(); + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/user/NiFiUserDetails.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/user/NiFiUserDetails.java new file mode 100644 index 0000000000..ca6ea2e8a8 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/user/NiFiUserDetails.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.user; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.Collections; + +/** + * User details for a NiFi user. + */ +public class NiFiUserDetails implements UserDetails { + + private final NiFiUser user; + + /** + * Creates a new NiFiUserDetails. + * + * @param user user + */ + public NiFiUserDetails(NiFiUser user) { + this.user = user; + } + + /** + * Get the user for this UserDetails. + * + * @return user + */ + public NiFiUser getNiFiUser() { + return user; + } + + /** + * Returns the authorities that this NiFi user has. + * + * @return authorities + */ + @Override + public Collection getAuthorities() { + return Collections.EMPTY_SET; + } + + @Override + public String getPassword() { + return StringUtils.EMPTY; + } + + @Override + public String getUsername() { + return user.getIdentity(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/user/NiFiUserUtils.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/user/NiFiUserUtils.java new file mode 100644 index 0000000000..b5147eadb3 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/user/NiFiUserUtils.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.user; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.util.ArrayList; +import java.util.List; + +/** + * Utility methods for retrieving information about the current application user. + * + */ +public final class NiFiUserUtils { + + /** + * Returns the current NiFiUser or null if the current user is not a NiFiUser. + * + * @return user + */ + public static NiFiUser getNiFiUser() { + NiFiUser user = null; + + // obtain the principal in the current authentication + final SecurityContext context = SecurityContextHolder.getContext(); + final Authentication authentication = context.getAuthentication(); + if (authentication != null) { + Object principal = authentication.getPrincipal(); + if (principal instanceof NiFiUserDetails) { + user = ((NiFiUserDetails) principal).getNiFiUser(); + } + } + + return user; + } + + public static String getNiFiUserIdentity() { + // get the nifi user to extract the username + NiFiUser user = NiFiUserUtils.getNiFiUser(); + if (user == null) { + return "unknown"; + } else { + return user.getIdentity(); + } + } + + /** + * Builds the proxy chain for the specified user. + * + * @param user The current user + * @return The proxy chain for that user in List form + */ + public static List buildProxiedEntitiesChain(final NiFiUser user) { + // calculate the dn chain + final List proxyChain = new ArrayList<>(); + + // build the dn chain + NiFiUser chainedUser = user; + while (chainedUser != null) { + // add the entry for this user + if (chainedUser.isAnonymous()) { + // use an empty string to represent an anonymous user in the proxy entities chain + proxyChain.add(StringUtils.EMPTY); + } else { + proxyChain.add(chainedUser.getIdentity()); + } + + // go to the next user in the chain + chainedUser = chainedUser.getChain(); + } + + return proxyChain; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/user/StandardNiFiUser.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/user/StandardNiFiUser.java new file mode 100644 index 0000000000..92c2274341 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/user/StandardNiFiUser.java @@ -0,0 +1,189 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.user; + +import org.apache.commons.lang3.StringUtils; + +import java.util.Collections; +import java.util.Objects; +import java.util.Set; + +/** + * An implementation of NiFiUser. + */ +public class StandardNiFiUser implements NiFiUser { + + public static final String ANONYMOUS_IDENTITY = "anonymous"; + public static final StandardNiFiUser ANONYMOUS = new Builder().identity(ANONYMOUS_IDENTITY).anonymous(true).build(); + + private final String identity; + private final Set groups; + private final NiFiUser chain; + private final String clientAddress; + private final boolean isAnonymous; + + private StandardNiFiUser(final Builder builder) { + this.identity = builder.identity; + this.groups = builder.groups == null ? null : Collections.unmodifiableSet(builder.groups); + this.chain = builder.chain; + this.clientAddress = builder.clientAddress; + this.isAnonymous = builder.isAnonymous; + } + + /** + * This static builder allows the chain and clientAddress to be populated without allowing calling code to provide a non-anonymous identity of the anonymous user. + * + * @param chain the proxied entities in {@see NiFiUser} form + * @param clientAddress the address the request originated from + * @return an anonymous user instance with the identity "anonymous" + */ + public static StandardNiFiUser populateAnonymousUser(NiFiUser chain, String clientAddress) { + return new Builder().identity(ANONYMOUS_IDENTITY).chain(chain).clientAddress(clientAddress).anonymous(true).build(); + } + + @Override + public String getIdentity() { + return identity; + } + + @Override + public Set getGroups() { + return groups; + } + + @Override + public NiFiUser getChain() { + return chain; + } + + @Override + public boolean isAnonymous() { + return isAnonymous; + } + + @Override + public String getClientAddress() { + return clientAddress; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + + if (!(obj instanceof NiFiUser)) { + return false; + } + + final NiFiUser other = (NiFiUser) obj; + return Objects.equals(this.identity, other.getIdentity()); + } + + @Override + public int hashCode() { + int hash = 7; + hash = 53 * hash + Objects.hashCode(this.identity); + return hash; + } + + @Override + public String toString() { + final String formattedGroups; + if (groups == null) { + formattedGroups = "none"; + } else { + formattedGroups = StringUtils.join(groups, ", "); + } + + return String.format("identity[%s], groups[%s]", getIdentity(), formattedGroups); + } + + /** + * Builder for a StandardNiFiUser + */ + public static class Builder { + + private String identity; + private Set groups; + private NiFiUser chain; + private String clientAddress; + private boolean isAnonymous = false; + + /** + * Sets the identity. + * + * @param identity the identity string for the user (i.e. "Andy" or "CN=alopresto, OU=Apache NiFi") + * @return the builder + */ + public Builder identity(final String identity) { + this.identity = identity; + return this; + } + + /** + * Sets the groups. + * + * @param groups the user groups + * @return the builder + */ + public Builder groups(final Set groups) { + this.groups = groups; + return this; + } + + /** + * Sets the chain. + * + * @param chain the proxy chain that leads to this users + * @return the builder + */ + public Builder chain(final NiFiUser chain) { + this.chain = chain; + return this; + } + + /** + * Sets the client address. + * + * @param clientAddress the source address of the request + * @return the builder + */ + public Builder clientAddress(final String clientAddress) { + this.clientAddress = clientAddress; + return this; + } + + /** + * Sets whether this user is the canonical "anonymous" user + * + * @param isAnonymous true to represent the canonical "anonymous" user + * @return the builder + */ + private Builder anonymous(final boolean isAnonymous) { + this.isAnonymous = isAnonymous; + return this; + } + + /** + * @return builds a StandardNiFiUser from the current state of the builder + */ + public StandardNiFiUser build() { + return new StandardNiFiUser(this); + } + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/util/AccessPolicyProviderUtils.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/util/AccessPolicyProviderUtils.java new file mode 100644 index 0000000000..890626dc25 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/util/AccessPolicyProviderUtils.java @@ -0,0 +1,143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.util; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.security.authorization.AuthorizerConfigurationContext; +import org.apache.nifi.registry.security.authorization.Group; +import org.apache.nifi.registry.security.authorization.UserGroupProvider; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.identity.IdentityMapper; +import org.apache.nifi.registry.util.PropertyValue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Utility methods related to access policies for use by various {@link org.apache.nifi.registry.security.authorization.AccessPolicyProvider} implementations. + */ +public final class AccessPolicyProviderUtils { + + private static final Logger LOGGER = LoggerFactory.getLogger(AccessPolicyProviderUtils.class); + + /** + * The prefix of a property from an AuthorizerConfigurationContext that specifies a NiFi Identity. + */ + public static final String PROP_NIFI_IDENTITY_PREFIX = "NiFi Identity "; + + /** + * The name of the property from an AuthorizerConfigurationContext that specifies the initial admin identity. + */ + public static final String PROP_INITIAL_ADMIN_IDENTITY = "Initial Admin Identity"; + + /** + * A Pattern for identifying properties that represent NiFi Identities. + */ + public static final Pattern NIFI_IDENTITY_PATTERN = Pattern.compile(PROP_NIFI_IDENTITY_PREFIX + "\\S+"); + + /** + * The name of the property from AuthorizerConfigurationContext that specifies a name of a group for NiFi Identities. + */ + public static final String PROP_NIFI_GROUP_NAME = "NiFi Group Name"; + + /** + * Returns the value of the 'Initial Admin Identity' property with any identity mappings applied. + * + * @param configurationContext the configuration context + * @param identityMapper the identity mapper + * @return the value for the initial admin identity + */ + public static String getInitialAdminIdentity(final AuthorizerConfigurationContext configurationContext, final IdentityMapper identityMapper) { + final PropertyValue initialAdminIdentityProp = configurationContext.getProperty(PROP_INITIAL_ADMIN_IDENTITY); + return initialAdminIdentityProp.isSet() ? identityMapper.mapUser(initialAdminIdentityProp.getValue()) : null; + } + + /** + * Returns the values for the 'NiFi Identity' properties with any identity mappings applied. + * + * @param configurationContext the configuration context + * @param identityMapper the identity mapper + * @return the values for the NiFi identities + */ + public static Set getNiFiIdentities(final AuthorizerConfigurationContext configurationContext, final IdentityMapper identityMapper) { + final Set nifiIdentities = new HashSet<>(); + + for (final Map.Entry entry : configurationContext.getProperties().entrySet()) { + final Matcher matcher = NIFI_IDENTITY_PATTERN.matcher(entry.getKey()); + if (matcher.matches() && !StringUtils.isBlank(entry.getValue())) { + nifiIdentities.add(identityMapper.mapUser(entry.getValue())); + } + } + + return nifiIdentities; + } + + /** + * Returns the value for the property 'NiFi Group Name' from the given configuration context. + * + * @param configurationContext the configuration context + * @return the group name, or null if not specified + */ + public static String getNiFiGroupName(final AuthorizerConfigurationContext configurationContext, final IdentityMapper identityMapper) { + final PropertyValue nifiGroupNameProp = configurationContext.getProperty(PROP_NIFI_GROUP_NAME); + final String nifiGroupName = (nifiGroupNameProp != null && nifiGroupNameProp.isSet()) ? nifiGroupNameProp.getValue() : null; + + if (StringUtils.isBlank(nifiGroupName)) { + LOGGER.debug("NiFi Group Name was not specified"); + return null; + } + + return identityMapper.mapGroup(nifiGroupName); + } + + /** + * Returns the identifier of the group with the given group name. + * + * If no group exists with the given name then SecurityProviderCreationException is thrown. + * + * @param groupName the group name + * @param userGroupProvider the UserGroupProvider + * @return the identifier of the group, or null + */ + public static Group getGroup(final String groupName, final UserGroupProvider userGroupProvider) { + final Set groups = userGroupProvider.getGroups(); + LOGGER.trace("All groups: {}", groups); + + final Optional groupOptional = groups.stream() + .filter(group -> group.getName().equals(groupName)) + .findFirst(); + + final Group group = groupOptional.orElseThrow(() -> + new SecurityProviderCreationException( + String.format("Group '%s' could not be found", groupName)) + ); + + LOGGER.debug("Group identifier is: {}", group); + return group; + } + + private AccessPolicyProviderUtils() { + + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/util/InitialPolicies.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/util/InitialPolicies.java new file mode 100644 index 0000000000..5f8c20e77d --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/util/InitialPolicies.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.util; + +import org.apache.nifi.registry.security.authorization.RequestAction; +import org.apache.nifi.registry.security.authorization.resource.ResourceFactory; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Defines the initial policies to be created for the initial users. + */ +public final class InitialPolicies { + + // Resource-actions pairs used by initial admins and NiFi identities + + public static final ResourceAndAction TENANTS_READ = new ResourceAndAction(ResourceFactory.getTenantsResource(), RequestAction.READ); + public static final ResourceAndAction TENANTS_WRITE = new ResourceAndAction(ResourceFactory.getTenantsResource(), RequestAction.WRITE); + public static final ResourceAndAction TENANTS_DELETE = new ResourceAndAction(ResourceFactory.getTenantsResource(), RequestAction.DELETE); + + public static final ResourceAndAction POLICIES_READ = new ResourceAndAction(ResourceFactory.getPoliciesResource(), RequestAction.READ); + public static final ResourceAndAction POLICIES_WRITE = new ResourceAndAction(ResourceFactory.getPoliciesResource(), RequestAction.WRITE); + public static final ResourceAndAction POLICIES_DELETE = new ResourceAndAction(ResourceFactory.getPoliciesResource(), RequestAction.DELETE); + + public static final ResourceAndAction BUCKETS_READ = new ResourceAndAction(ResourceFactory.getBucketsResource(), RequestAction.READ); + public static final ResourceAndAction BUCKETS_WRITE = new ResourceAndAction(ResourceFactory.getBucketsResource(), RequestAction.WRITE); + public static final ResourceAndAction BUCKETS_DELETE = new ResourceAndAction(ResourceFactory.getBucketsResource(), RequestAction.DELETE); + + public static final ResourceAndAction ACTUATOR_READ = new ResourceAndAction(ResourceFactory.getActuatorResource(), RequestAction.READ); + public static final ResourceAndAction ACTUATOR_WRITE = new ResourceAndAction(ResourceFactory.getActuatorResource(), RequestAction.WRITE); + public static final ResourceAndAction ACTUATOR_DELETE = new ResourceAndAction(ResourceFactory.getActuatorResource(), RequestAction.DELETE); + + public static final ResourceAndAction SWAGGER_READ = new ResourceAndAction(ResourceFactory.getSwaggerResource(), RequestAction.READ); + public static final ResourceAndAction SWAGGER_WRITE = new ResourceAndAction(ResourceFactory.getSwaggerResource(), RequestAction.WRITE); + public static final ResourceAndAction SWAGGER_DELETE = new ResourceAndAction(ResourceFactory.getSwaggerResource(), RequestAction.DELETE); + + public static final ResourceAndAction PROXY_READ = new ResourceAndAction(ResourceFactory.getProxyResource(), RequestAction.READ); + public static final ResourceAndAction PROXY_WRITE = new ResourceAndAction(ResourceFactory.getProxyResource(), RequestAction.WRITE); + public static final ResourceAndAction PROXY_DELETE = new ResourceAndAction(ResourceFactory.getProxyResource(), RequestAction.DELETE); + + /** + * Resource-action pairs to create policies for an initial admin. + */ + public static final Set ADMIN_POLICIES = Collections.unmodifiableSet( + new HashSet<>(Arrays.asList( + TENANTS_READ, + TENANTS_WRITE, + TENANTS_DELETE, + POLICIES_READ, + POLICIES_WRITE, + POLICIES_DELETE, + BUCKETS_READ, + BUCKETS_WRITE, + BUCKETS_DELETE, + ACTUATOR_READ, + ACTUATOR_WRITE, + ACTUATOR_DELETE, + SWAGGER_READ, + SWAGGER_WRITE, + SWAGGER_DELETE, + PROXY_READ, + PROXY_WRITE, + PROXY_DELETE + )) + ); + + /** + * Resource-action paris to create policies for initial NiFi users. + */ + public static final Set NIFI_POLICIES = Collections.unmodifiableSet( + new HashSet<>(Arrays.asList( + BUCKETS_READ, + PROXY_READ, + PROXY_WRITE, + PROXY_DELETE + )) + ); + + private InitialPolicies() { + + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/util/ResourceAndAction.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/util/ResourceAndAction.java new file mode 100644 index 0000000000..ed11c86c23 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/util/ResourceAndAction.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.util; + +import org.apache.nifi.registry.security.authorization.RequestAction; +import org.apache.nifi.registry.security.authorization.Resource; + +import java.util.Objects; + +public class ResourceAndAction { + + private final Resource resource; + + private final RequestAction action; + + public ResourceAndAction(final Resource resource, final RequestAction action) { + this.resource = Objects.requireNonNull(resource); + this.action = Objects.requireNonNull(action); + } + + public Resource getResource() { + return resource; + } + + public RequestAction getAction() { + return action; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + final ResourceAndAction that = (ResourceAndAction) o; + return Objects.equals(resource, that.resource) && action == that.action; + } + + @Override + public int hashCode() { + return Objects.hash(resource, action); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/util/UserGroupProviderUtils.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/util/UserGroupProviderUtils.java new file mode 100644 index 0000000000..beb415c1c8 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/util/UserGroupProviderUtils.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.util; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.security.authorization.AuthorizerConfigurationContext; +import org.apache.nifi.registry.security.identity.IdentityMapper; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Utility methods related to access policies for use by various {@link org.apache.nifi.registry.security.authorization.UserGroupProvider} implementations. + */ +public final class UserGroupProviderUtils { + + public static final String PROP_INITIAL_USER_IDENTITY_PREFIX = "Initial User Identity "; + public static final Pattern INITIAL_USER_IDENTITY_PATTERN = Pattern.compile(PROP_INITIAL_USER_IDENTITY_PREFIX + "\\S+"); + + public static Set getInitialUserIdentities(final AuthorizerConfigurationContext configurationContext, final IdentityMapper identityMapper) { + final Set initialUserIdentities = new HashSet<>(); + for (Map.Entry entry : configurationContext.getProperties().entrySet()) { + Matcher matcher = UserGroupProviderUtils.INITIAL_USER_IDENTITY_PATTERN.matcher(entry.getKey()); + if (matcher.matches() && !StringUtils.isBlank(entry.getValue())) { + initialUserIdentities.add(identityMapper.mapUser(entry.getValue())); + } + } + return initialUserIdentities; + } + + private UserGroupProviderUtils() { + + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/crypto/SensitivePropertyProviderConfiguration.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/crypto/SensitivePropertyProviderConfiguration.java new file mode 100644 index 0000000000..78594921b7 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/crypto/SensitivePropertyProviderConfiguration.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.crypto; + +import org.apache.nifi.registry.properties.AESSensitivePropertyProvider; +import org.apache.nifi.registry.properties.SensitivePropertyProtectionException; +import org.apache.nifi.registry.properties.SensitivePropertyProvider; +import org.apache.nifi.registry.properties.SensitivePropertyProviderFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.crypto.NoSuchPaddingException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; + +@Configuration +public class SensitivePropertyProviderConfiguration implements SensitivePropertyProviderFactory { + + private static final Logger logger = LoggerFactory.getLogger(SensitivePropertyProviderConfiguration.class); + + @Autowired(required = false) + private CryptoKeyProvider masterKeyProvider; + + /** + * @return a SensitivePropertyProvider initialized with the master key if present, + * or null if the master key is not present. + */ + @Bean + @Override + public SensitivePropertyProvider getProvider() { + if (masterKeyProvider == null || masterKeyProvider.isEmpty()) { + // This NiFi Registry was not configured with a master key, so the assumption is + // the optional Spring bean normally provided by this method will never be needed + return null; + } + + try { + // Note, this bean is intentionally NOT a singleton because we want the + // returned provider, which has a copy of the sensitive master key material + // to be reaped when it goes out of scope in order to decrease the time + // key material is held in memory. + String key = masterKeyProvider.getKey(); + return new AESSensitivePropertyProvider(masterKeyProvider.getKey()); + } catch (MissingCryptoKeyException | NoSuchAlgorithmException | NoSuchProviderException | NoSuchPaddingException e) { + logger.warn("Error creating AES Sensitive Property Provider", e); + throw new SensitivePropertyProtectionException("Error creating AES Sensitive Property Provider", e); + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/identity/DefaultIdentityMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/identity/DefaultIdentityMapper.java new file mode 100644 index 0000000000..8bf2dc921a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/identity/DefaultIdentityMapper.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.identity; + +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.apache.nifi.registry.properties.util.IdentityMapping; +import org.apache.nifi.registry.properties.util.IdentityMappingUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Collections; +import java.util.List; + +@Component +public class DefaultIdentityMapper implements IdentityMapper { + + final List userIdentityMappings; + final List groupMappings; + + @Autowired + public DefaultIdentityMapper(final NiFiRegistryProperties properties) { + userIdentityMappings = Collections.unmodifiableList(IdentityMappingUtil.getIdentityMappings(properties)); + groupMappings = Collections.unmodifiableList(IdentityMappingUtil.getGroupMappings(properties)); + } + + @Override + public String mapUser(final String userIdentity) { + return IdentityMappingUtil.mapIdentity(userIdentity, userIdentityMappings); + } + + @Override + public String mapGroup(final String groupName) { + return IdentityMappingUtil.mapIdentity(groupName, groupMappings); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/identity/IdentityMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/identity/IdentityMapper.java new file mode 100644 index 0000000000..e5e3974d65 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/identity/IdentityMapper.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.identity; + + +public interface IdentityMapper { + + /** + * Maps the given user identity to a target identity. + * If the given identity does not match configured patters, the input is returned. + * + * @param userIdentity the identity to map + * @return the mapped identity, or the same identity if no mappings matched + */ + String mapUser(String userIdentity); + + /** + * Maps the given group name to a target group name. + * If the given group name does not match configured patters, the input is returned. + * + * @param groupName the group name to map + * @return the mapped group name, or the same group name if no mappings matched + */ + String mapGroup(String groupName); + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/key/Key.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/key/Key.java new file mode 100644 index 0000000000..c110fa8cd7 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/key/Key.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.key; + +import java.io.Serializable; + +/** + * An signing key for a NiFi user. + */ +public class Key implements Serializable { + + private String id; + private String identity; + private String key; + + /** + * The key id. + * + * @return the id + */ + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + /** + * The identity of the user this key is associated with. + * + * @return the identity + */ + public String getIdentity() { + return identity; + } + + public void setIdentity(String identity) { + this.identity = identity; + } + + /** + * The signing key. + * + * @return the signing key + */ + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/key/KeyService.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/key/KeyService.java new file mode 100644 index 0000000000..3b9a7ca9f3 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/key/KeyService.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.key; + +/** + * Manages NiFi user keys. + */ +public interface KeyService { + + /** + * Gets a key for the specified user identity. Returns null if the user has not had a key issued + * + * @param id The key id + * @return The key or null + */ + Key getKey(String id); + + /** + * Gets a key for the specified user identity. If a key does not exist, one will be created. + * + * @param identity The user identity + * @return The key + */ + Key getOrCreateKey(String identity); + + /** + * Deletes keys for the specified identity. + * + * @param identity The user identity + */ + void deleteKey(String identity); +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/IdentityStrategy.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/IdentityStrategy.java new file mode 100644 index 0000000000..135f261ef2 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/IdentityStrategy.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.ldap; + +public enum IdentityStrategy { + USE_DN, + USE_USERNAME; +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/LdapAuthenticationStrategy.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/LdapAuthenticationStrategy.java new file mode 100644 index 0000000000..331fbc34bd --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/LdapAuthenticationStrategy.java @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.ldap; + +public enum LdapAuthenticationStrategy { + ANONYMOUS, + SIMPLE, + LDAPS, + START_TLS +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/LdapIdentityProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/LdapIdentityProvider.java new file mode 100644 index 0000000000..4427ed951d --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/LdapIdentityProvider.java @@ -0,0 +1,355 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.ldap; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.security.authentication.AuthenticationRequest; +import org.apache.nifi.registry.security.authentication.AuthenticationResponse; +import org.apache.nifi.registry.security.authentication.BasicAuthIdentityProvider; +import org.apache.nifi.registry.security.authentication.IdentityProvider; +import org.apache.nifi.registry.security.authentication.IdentityProviderConfigurationContext; +import org.apache.nifi.registry.security.authentication.exception.IdentityAccessException; +import org.apache.nifi.registry.security.authentication.exception.InvalidCredentialsException; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException; +import org.apache.nifi.registry.security.util.SslContextFactory; +import org.apache.nifi.registry.security.util.SslContextFactory.ClientAuth; +import org.apache.nifi.registry.util.FormatUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ldap.AuthenticationException; +import org.springframework.ldap.core.support.AbstractTlsDirContextAuthenticationStrategy; +import org.springframework.ldap.core.support.DefaultTlsDirContextAuthenticationStrategy; +import org.springframework.ldap.core.support.LdapContextSource; +import org.springframework.ldap.core.support.SimpleDirContextAuthenticationStrategy; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider; +import org.springframework.security.ldap.authentication.BindAuthenticator; +import org.springframework.security.ldap.authentication.LdapAuthenticationProvider; +import org.springframework.security.ldap.search.FilterBasedLdapUserSearch; +import org.springframework.security.ldap.search.LdapUserSearch; +import org.springframework.security.ldap.userdetails.LdapUserDetails; + +import javax.naming.Context; +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * LDAP based implementation of a login identity provider. + */ +public class LdapIdentityProvider extends BasicAuthIdentityProvider implements IdentityProvider { + + private static final Logger logger = LoggerFactory.getLogger(LdapIdentityProvider.class); + + private static final String issuer = LdapIdentityProvider.class.getSimpleName(); + + private AbstractLdapAuthenticationProvider ldapAuthenticationProvider; + private long expiration; + private IdentityStrategy identityStrategy; + + @Override + public final void onConfigured(final IdentityProviderConfigurationContext configurationContext) throws SecurityProviderCreationException { + final String rawExpiration = configurationContext.getProperty("Authentication Expiration"); + if (StringUtils.isBlank(rawExpiration)) { + throw new SecurityProviderCreationException("The Authentication Expiration must be specified."); + } + + try { + expiration = FormatUtils.getTimeDuration(rawExpiration, TimeUnit.MILLISECONDS); + } catch (final IllegalArgumentException iae) { + throw new SecurityProviderCreationException(String.format("The Expiration Duration '%s' is not a valid time duration", rawExpiration)); + } + + final LdapContextSource context = new LdapContextSource(); + + final Map baseEnvironment = new HashMap<>(); + + // connect/read time out + setTimeout(configurationContext, baseEnvironment, "Connect Timeout", "com.sun.jndi.ldap.connect.timeout"); + setTimeout(configurationContext, baseEnvironment, "Read Timeout", "com.sun.jndi.ldap.read.timeout"); + + // authentication strategy + final String rawAuthenticationStrategy = configurationContext.getProperty("Authentication Strategy"); + final LdapAuthenticationStrategy authenticationStrategy; + try { + authenticationStrategy = LdapAuthenticationStrategy.valueOf(rawAuthenticationStrategy); + } catch (final IllegalArgumentException iae) { + throw new SecurityProviderCreationException(String.format("Unrecognized authentication strategy '%s'. Possible values are [%s]", + rawAuthenticationStrategy, StringUtils.join(LdapAuthenticationStrategy.values(), ", "))); + } + + switch (authenticationStrategy) { + case ANONYMOUS: + context.setAnonymousReadOnly(true); + break; + default: + final String userDn = configurationContext.getProperty("Manager DN"); + final String password = configurationContext.getProperty("Manager Password"); + + context.setUserDn(userDn); + context.setPassword(password); + + switch (authenticationStrategy) { + case SIMPLE: + context.setAuthenticationStrategy(new SimpleDirContextAuthenticationStrategy()); + break; + case LDAPS: + context.setAuthenticationStrategy(new SimpleDirContextAuthenticationStrategy()); + + // indicate a secure connection + baseEnvironment.put(Context.SECURITY_PROTOCOL, "ssl"); + + // get the configured ssl context + final SSLContext ldapsSslContext = getConfiguredSslContext(configurationContext); + if (ldapsSslContext != null) { + // initialize the ldaps socket factory prior to use + LdapsSocketFactory.initialize(ldapsSslContext.getSocketFactory()); + baseEnvironment.put("java.naming.ldap.factory.socket", LdapsSocketFactory.class.getName()); + } + break; + case START_TLS: + final AbstractTlsDirContextAuthenticationStrategy tlsAuthenticationStrategy = new DefaultTlsDirContextAuthenticationStrategy(); + + // shutdown gracefully + final String rawShutdownGracefully = configurationContext.getProperty("TLS - Shutdown Gracefully"); + if (StringUtils.isNotBlank(rawShutdownGracefully)) { + final boolean shutdownGracefully = Boolean.TRUE.toString().equalsIgnoreCase(rawShutdownGracefully); + tlsAuthenticationStrategy.setShutdownTlsGracefully(shutdownGracefully); + } + + // get the configured ssl context + final SSLContext startTlsSslContext = getConfiguredSslContext(configurationContext); + if (startTlsSslContext != null) { + tlsAuthenticationStrategy.setSslSocketFactory(startTlsSslContext.getSocketFactory()); + } + + // set the authentication strategy + context.setAuthenticationStrategy(tlsAuthenticationStrategy); + break; + } + break; + } + + // referrals + final String rawReferralStrategy = configurationContext.getProperty("Referral Strategy"); + + final ReferralStrategy referralStrategy; + try { + referralStrategy = ReferralStrategy.valueOf(rawReferralStrategy); + } catch (final IllegalArgumentException iae) { + throw new SecurityProviderCreationException(String.format("Unrecognized referral strategy '%s'. Possible values are [%s]", + rawReferralStrategy, StringUtils.join(ReferralStrategy.values(), ", "))); + } + + // using the value as this needs to be the lowercase version while the value is configured with the enum constant + context.setReferral(referralStrategy.getValue()); + + // url + final String urls = configurationContext.getProperty("Url"); + + if (StringUtils.isBlank(urls)) { + throw new SecurityProviderCreationException("LDAP identity provider 'Url' must be specified."); + } + + // connection + context.setUrls(StringUtils.split(urls)); + + // search criteria + final String userSearchBase = configurationContext.getProperty("User Search Base"); + final String userSearchFilter = configurationContext.getProperty("User Search Filter"); + + if (StringUtils.isBlank(userSearchBase) || StringUtils.isBlank(userSearchFilter)) { + throw new SecurityProviderCreationException("LDAP identity provider 'User Search Base' and 'User Search Filter' must be specified."); + } + + final LdapUserSearch userSearch = new FilterBasedLdapUserSearch(userSearchBase, userSearchFilter, context); + + // bind + final BindAuthenticator authenticator = new BindAuthenticator(context); + authenticator.setUserSearch(userSearch); + + // identity strategy + final String rawIdentityStrategy = configurationContext.getProperty("Identity Strategy"); + + if (StringUtils.isBlank(rawIdentityStrategy)) { + logger.info(String.format("Identity Strategy is not configured, defaulting strategy to %s.", IdentityStrategy.USE_DN)); + + // if this value is not configured, default to use dn which was the previous implementation + identityStrategy = IdentityStrategy.USE_DN; + } else { + try { + // attempt to get the configured identity strategy + identityStrategy = IdentityStrategy.valueOf(rawIdentityStrategy); + } catch (final IllegalArgumentException iae) { + throw new SecurityProviderCreationException(String.format("Unrecognized identity strategy '%s'. Possible values are [%s]", + rawIdentityStrategy, StringUtils.join(IdentityStrategy.values(), ", "))); + } + } + + // set the base environment is necessary + if (!baseEnvironment.isEmpty()) { + context.setBaseEnvironmentProperties(baseEnvironment); + } + + try { + // handling initializing beans + context.afterPropertiesSet(); + authenticator.afterPropertiesSet(); + } catch (final Exception e) { + throw new SecurityProviderCreationException(e.getMessage(), e); + } + + // create the underlying provider + ldapAuthenticationProvider = new LdapAuthenticationProvider(authenticator); + } + + @Override + public AuthenticationResponse authenticate(AuthenticationRequest authenticationRequest) throws InvalidCredentialsException, IdentityAccessException { + + if (authenticationRequest == null || StringUtils.isEmpty(authenticationRequest.getUsername())) { + logger.debug("Call to authenticate method with null or empty authenticationRequest, returning null without attempting to authenticate"); + return null; + } + + if (ldapAuthenticationProvider == null) { + throw new IdentityAccessException("The LDAP authentication provider is not initialized."); + } + + try { + final String username = authenticationRequest.getUsername(); + final Object credentials = authenticationRequest.getCredentials(); + final String password = credentials != null && credentials instanceof String ? (String) credentials : null; + + // perform the authentication + final UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, credentials); + final Authentication authentication = ldapAuthenticationProvider.authenticate(token); + logger.debug("Created authentication token: {}", token.toString()); + + // use dn if configured + if (IdentityStrategy.USE_DN.equals(identityStrategy)) { + // attempt to get the ldap user details to get the DN + if (authentication.getPrincipal() instanceof LdapUserDetails) { + final LdapUserDetails userDetails = (LdapUserDetails) authentication.getPrincipal(); + return new AuthenticationResponse(userDetails.getDn(), username, expiration, issuer); + } else { + logger.warn(String.format("Unable to determine user DN for %s, using username.", authentication.getName())); + return new AuthenticationResponse(authentication.getName(), username, expiration, issuer); + } + } else { + return new AuthenticationResponse(authentication.getName(), username, expiration, issuer); + } + } catch (final BadCredentialsException | UsernameNotFoundException | AuthenticationException e) { + throw new InvalidCredentialsException(e.getMessage(), e); + } catch (final Exception e) { + // there appears to be a bug that generates a InternalAuthenticationServiceException wrapped around an AuthenticationException. this + // shouldn't be the case as they the service exception suggestions that something was wrong with the service. while the authentication + // exception suggests that username and/or credentials were incorrect. checking the cause seems to address this scenario. + final Throwable cause = e.getCause(); + if (cause instanceof AuthenticationException) { + throw new InvalidCredentialsException(e.getMessage(), e); + } + + logger.error(e.getMessage()); + if (logger.isDebugEnabled()) { + logger.debug(StringUtils.EMPTY, e); + } + throw new IdentityAccessException("Unable to validate the supplied credentials. Please contact the system administrator.", e); + } + } + + @Override + public final void preDestruction() throws SecurityProviderDestructionException { + } + + private void setTimeout(final IdentityProviderConfigurationContext configurationContext, + final Map baseEnvironment, + final String configurationProperty, + final String environmentKey) { + + final String rawTimeout = configurationContext.getProperty(configurationProperty); + if (StringUtils.isNotBlank(rawTimeout)) { + try { + final Long timeout = FormatUtils.getTimeDuration(rawTimeout, TimeUnit.MILLISECONDS); + baseEnvironment.put(environmentKey, timeout.toString()); + } catch (final IllegalArgumentException iae) { + throw new SecurityProviderCreationException(String.format("The %s '%s' is not a valid time duration", configurationProperty, rawTimeout)); + } + } + } + + private SSLContext getConfiguredSslContext(final IdentityProviderConfigurationContext configurationContext) { + final String rawKeystore = configurationContext.getProperty("TLS - Keystore"); + final String rawKeystorePassword = configurationContext.getProperty("TLS - Keystore Password"); + final String rawKeystoreType = configurationContext.getProperty("TLS - Keystore Type"); + final String rawTruststore = configurationContext.getProperty("TLS - Truststore"); + final String rawTruststorePassword = configurationContext.getProperty("TLS - Truststore Password"); + final String rawTruststoreType = configurationContext.getProperty("TLS - Truststore Type"); + final String rawClientAuth = configurationContext.getProperty("TLS - Client Auth"); + final String rawProtocol = configurationContext.getProperty("TLS - Protocol"); + + // create the ssl context + final SSLContext sslContext; + try { + if (StringUtils.isBlank(rawKeystore) && StringUtils.isBlank(rawTruststore)) { + sslContext = null; + } else { + // ensure the protocol is specified + if (StringUtils.isBlank(rawProtocol)) { + throw new SecurityProviderCreationException("TLS - Protocol must be specified."); + } + + if (StringUtils.isBlank(rawKeystore)) { + sslContext = SslContextFactory.createTrustSslContext(rawTruststore, rawTruststorePassword.toCharArray(), rawTruststoreType, rawProtocol); + } else if (StringUtils.isBlank(rawTruststore)) { + sslContext = SslContextFactory.createSslContext(rawKeystore, rawKeystorePassword.toCharArray(), rawKeystoreType, rawProtocol); + } else { + // determine the client auth if specified + final ClientAuth clientAuth; + if (StringUtils.isBlank(rawClientAuth)) { + clientAuth = ClientAuth.NONE; + } else { + try { + clientAuth = ClientAuth.valueOf(rawClientAuth); + } catch (final IllegalArgumentException iae) { + throw new SecurityProviderCreationException(String.format("Unrecognized client auth '%s'. Possible values are [%s]", + rawClientAuth, StringUtils.join(ClientAuth.values(), ", "))); + } + } + + sslContext = SslContextFactory.createSslContext(rawKeystore, rawKeystorePassword.toCharArray(), rawKeystoreType, + rawTruststore, rawTruststorePassword.toCharArray(), rawTruststoreType, clientAuth, rawProtocol); + } + } + } catch (final KeyStoreException | NoSuchAlgorithmException | CertificateException | UnrecoverableKeyException | KeyManagementException | IOException e) { + throw new SecurityProviderCreationException(e.getMessage(), e); + } + + return sslContext; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/LdapsSocketFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/LdapsSocketFactory.java new file mode 100644 index 0000000000..dff95726e5 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/LdapsSocketFactory.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.ldap; + +import javax.net.SocketFactory; +import javax.net.ssl.SSLSocketFactory; +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; + +/** + * SSLSocketFactory used when connecting to a Directory Server over LDAPS. + */ +public class LdapsSocketFactory extends SSLSocketFactory { + + // singleton + private static LdapsSocketFactory instance; + + // delegate + private SSLSocketFactory delegate; + + /** + * Initializes the LdapsSocketFactory with the specified SSLSocketFactory. The specified + * socket factory will be used as a delegate for all subsequent instances of this class. + * + * @param sslSocketFactory delegate socket factory + */ + public static void initialize(final SSLSocketFactory sslSocketFactory) { + instance = new LdapsSocketFactory(sslSocketFactory); + } + + /** + * Gets the LdapsSocketFactory that was previously initialized. + * + * @return socket factory + */ + public static SocketFactory getDefault() { + return instance; + } + + /** + * Creates a new LdapsSocketFactory. + * + * @param sslSocketFactory delegate socket factory + */ + private LdapsSocketFactory(final SSLSocketFactory sslSocketFactory) { + delegate = sslSocketFactory; + } + + // delegate methods + + @Override + public String[] getSupportedCipherSuites() { + return delegate.getSupportedCipherSuites(); + } + + @Override + public String[] getDefaultCipherSuites() { + return delegate.getDefaultCipherSuites(); + } + + @Override + public Socket createSocket(Socket socket, String string, int i, boolean bln) throws IOException { + return delegate.createSocket(socket, string, i, bln); + } + + @Override + public Socket createSocket(InetAddress ia, int i, InetAddress ia1, int i1) throws IOException { + return delegate.createSocket(ia, i, ia1, i1); + } + + @Override + public Socket createSocket(InetAddress ia, int i) throws IOException { + return delegate.createSocket(ia, i); + } + + @Override + public Socket createSocket(String string, int i, InetAddress ia, int i1) throws IOException, UnknownHostException { + return delegate.createSocket(string, i, ia, i1); + } + + @Override + public Socket createSocket(String string, int i) throws IOException, UnknownHostException { + return delegate.createSocket(string, i); + } + + @Override + public Socket createSocket() throws IOException { + return delegate.createSocket(); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/ReferralStrategy.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/ReferralStrategy.java new file mode 100644 index 0000000000..4258cdede9 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/ReferralStrategy.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.ldap; + +public enum ReferralStrategy { + + FOLLOW("follow"), + IGNORE("ignore"), + THROW("throw"); + + private final String value; + + private ReferralStrategy(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/tenants/LdapUserGroupProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/tenants/LdapUserGroupProvider.java new file mode 100644 index 0000000000..8ecd56d358 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/tenants/LdapUserGroupProvider.java @@ -0,0 +1,860 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.ldap.tenants; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.security.authorization.AuthorizerConfigurationContext; +import org.apache.nifi.registry.security.authorization.Group; +import org.apache.nifi.registry.security.authorization.User; +import org.apache.nifi.registry.security.authorization.UserAndGroups; +import org.apache.nifi.registry.security.authorization.UserGroupProvider; +import org.apache.nifi.registry.security.authorization.UserGroupProviderInitializationContext; +import org.apache.nifi.registry.security.authorization.annotation.AuthorizerContext; +import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException; +import org.apache.nifi.registry.security.identity.IdentityMapper; +import org.apache.nifi.registry.security.ldap.LdapAuthenticationStrategy; +import org.apache.nifi.registry.security.ldap.LdapsSocketFactory; +import org.apache.nifi.registry.security.ldap.ReferralStrategy; +import org.apache.nifi.registry.security.util.SslContextFactory; +import org.apache.nifi.registry.security.util.SslContextFactory.ClientAuth; +import org.apache.nifi.registry.util.FormatUtils; +import org.apache.nifi.registry.util.PropertyValue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ldap.control.PagedResultsDirContextProcessor; +import org.springframework.ldap.core.ContextSource; +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.core.DirContextOperations; +import org.springframework.ldap.core.DirContextProcessor; +import org.springframework.ldap.core.LdapTemplate; +import org.springframework.ldap.core.LdapTemplate.NullDirContextProcessor; +import org.springframework.ldap.core.support.AbstractContextMapper; +import org.springframework.ldap.core.support.AbstractTlsDirContextAuthenticationStrategy; +import org.springframework.ldap.core.support.DefaultTlsDirContextAuthenticationStrategy; +import org.springframework.ldap.core.support.LdapContextSource; +import org.springframework.ldap.core.support.SimpleDirContextAuthenticationStrategy; +import org.springframework.ldap.core.support.SingleContextSource; +import org.springframework.ldap.filter.AndFilter; +import org.springframework.ldap.filter.EqualsFilter; +import org.springframework.ldap.filter.HardcodedFilter; + +import javax.naming.Context; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.SearchControls; +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Abstract LDAP based implementation of a login identity provider. + */ +public class LdapUserGroupProvider implements UserGroupProvider { + + private static final Logger logger = LoggerFactory.getLogger(LdapUserGroupProvider.class); + + public static final String PROP_CONNECT_TIMEOUT = "Connect Timeout"; + public static final String PROP_READ_TIMEOUT = "Read Timeout"; + public static final String PROP_AUTHENTICATION_STRATEGY = "Authentication Strategy"; + public static final String PROP_MANAGER_DN = "Manager DN"; + public static final String PROP_MANAGER_PASSWORD = "Manager Password"; + public static final String PROP_REFERRAL_STRATEGY = "Referral Strategy"; + public static final String PROP_URL = "Url"; + public static final String PROP_PAGE_SIZE = "Page Size"; + public static final String PROP_GROUP_MEMBERSHIP_ENFORCE_CASE_SENSITIVITY = "Group Membership - Enforce Case Sensitivity"; + + public static final String PROP_USER_SEARCH_BASE = "User Search Base"; + public static final String PROP_USER_OBJECT_CLASS = "User Object Class"; + public static final String PROP_USER_SEARCH_SCOPE = "User Search Scope"; + public static final String PROP_USER_SEARCH_FILTER = "User Search Filter"; + public static final String PROP_USER_IDENTITY_ATTRIBUTE = "User Identity Attribute"; + public static final String PROP_USER_GROUP_ATTRIBUTE = "User Group Name Attribute"; + public static final String PROP_USER_GROUP_REFERENCED_GROUP_ATTRIBUTE = "User Group Name Attribute - Referenced Group Attribute"; + + public static final String PROP_GROUP_SEARCH_BASE = "Group Search Base"; + public static final String PROP_GROUP_OBJECT_CLASS = "Group Object Class"; + public static final String PROP_GROUP_SEARCH_SCOPE = "Group Search Scope"; + public static final String PROP_GROUP_SEARCH_FILTER = "Group Search Filter"; + public static final String PROP_GROUP_NAME_ATTRIBUTE = "Group Name Attribute"; + public static final String PROP_GROUP_MEMBER_ATTRIBUTE = "Group Member Attribute"; + public static final String PROP_GROUP_MEMBER_REFERENCED_USER_ATTRIBUTE = "Group Member Attribute - Referenced User Attribute"; + + public static final String PROP_SYNC_INTERVAL = "Sync Interval"; + + private IdentityMapper identityMapper; + + private ScheduledExecutorService ldapSync; + private AtomicReference tenants = new AtomicReference<>(null); + + private String userSearchBase; + private SearchScope userSearchScope; + private String userSearchFilter; + private String userIdentityAttribute; + private String userObjectClass; + private String userGroupNameAttribute; + private String userGroupReferencedGroupAttribute; + private boolean useDnForUserIdentity; + private boolean performUserSearch; + + private String groupSearchBase; + private SearchScope groupSearchScope; + private String groupSearchFilter; + private String groupMemberAttribute; + private String groupMemberReferencedUserAttribute; + private String groupNameAttribute; + private String groupObjectClass; + private boolean useDnForGroupName; + private boolean performGroupSearch; + + private Integer pageSize; + + private boolean groupMembershipEnforceCaseSensitivity; + + @Override + public void initialize(final UserGroupProviderInitializationContext initializationContext) throws SecurityProviderCreationException { + ldapSync = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() { + final ThreadFactory factory = Executors.defaultThreadFactory(); + + @Override + public Thread newThread(Runnable r) { + final Thread thread = factory.newThread(r); + thread.setName(String.format("%s (%s) - background sync thread", getClass().getSimpleName(), initializationContext.getIdentifier())); + return thread; + } + }); + } + + @Override + public void onConfigured(final AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException { + final LdapContextSource context = new LdapContextSource(); + + final Map baseEnvironment = new HashMap<>(); + + // connect/read time out + setTimeout(configurationContext, baseEnvironment, PROP_CONNECT_TIMEOUT, "com.sun.jndi.ldap.connect.timeout"); + setTimeout(configurationContext, baseEnvironment, PROP_READ_TIMEOUT, "com.sun.jndi.ldap.read.timeout"); + + // authentication strategy + final PropertyValue rawAuthenticationStrategy = configurationContext.getProperty(PROP_AUTHENTICATION_STRATEGY); + final LdapAuthenticationStrategy authenticationStrategy; + try { + authenticationStrategy = LdapAuthenticationStrategy.valueOf(rawAuthenticationStrategy.getValue()); + } catch (final IllegalArgumentException iae) { + throw new SecurityProviderCreationException(String.format("Unrecognized authentication strategy '%s'. Possible values are [%s]", + rawAuthenticationStrategy.getValue(), StringUtils.join(LdapAuthenticationStrategy.values(), ", "))); + } + + switch (authenticationStrategy) { + case ANONYMOUS: + context.setAnonymousReadOnly(true); + break; + default: + final String userDn = configurationContext.getProperty(PROP_MANAGER_DN).getValue(); + final String password = configurationContext.getProperty(PROP_MANAGER_PASSWORD).getValue(); + + context.setUserDn(userDn); + context.setPassword(password); + + switch (authenticationStrategy) { + case SIMPLE: + context.setAuthenticationStrategy(new SimpleDirContextAuthenticationStrategy()); + break; + case LDAPS: + context.setAuthenticationStrategy(new SimpleDirContextAuthenticationStrategy()); + + // indicate a secure connection + baseEnvironment.put(Context.SECURITY_PROTOCOL, "ssl"); + + // get the configured ssl context + final SSLContext ldapsSslContext = getConfiguredSslContext(configurationContext); + if (ldapsSslContext != null) { + // initialize the ldaps socket factory prior to use + LdapsSocketFactory.initialize(ldapsSslContext.getSocketFactory()); + baseEnvironment.put("java.naming.ldap.factory.socket", LdapsSocketFactory.class.getName()); + } + break; + case START_TLS: + final AbstractTlsDirContextAuthenticationStrategy tlsAuthenticationStrategy = new DefaultTlsDirContextAuthenticationStrategy(); + + // shutdown gracefully + final String rawShutdownGracefully = configurationContext.getProperty("TLS - Shutdown Gracefully").getValue(); + if (StringUtils.isNotBlank(rawShutdownGracefully)) { + final boolean shutdownGracefully = Boolean.TRUE.toString().equalsIgnoreCase(rawShutdownGracefully); + tlsAuthenticationStrategy.setShutdownTlsGracefully(shutdownGracefully); + } + + // get the configured ssl context + final SSLContext startTlsSslContext = getConfiguredSslContext(configurationContext); + if (startTlsSslContext != null) { + tlsAuthenticationStrategy.setSslSocketFactory(startTlsSslContext.getSocketFactory()); + } + + // set the authentication strategy + context.setAuthenticationStrategy(tlsAuthenticationStrategy); + break; + } + break; + } + + // referrals + final String rawReferralStrategy = configurationContext.getProperty(PROP_REFERRAL_STRATEGY).getValue(); + + final ReferralStrategy referralStrategy; + try { + referralStrategy = ReferralStrategy.valueOf(rawReferralStrategy); + } catch (final IllegalArgumentException iae) { + throw new SecurityProviderCreationException(String.format("Unrecognized referral strategy '%s'. Possible values are [%s]", + rawReferralStrategy, StringUtils.join(ReferralStrategy.values(), ", "))); + } + + // using the value as this needs to be the lowercase version while the value is configured with the enum constant + context.setReferral(referralStrategy.getValue()); + + // url + final String urls = configurationContext.getProperty(PROP_URL).getValue(); + + if (StringUtils.isBlank(urls)) { + throw new SecurityProviderCreationException("LDAP identity provider 'Url' must be specified."); + } + + // connection + context.setUrls(StringUtils.split(urls)); + + // raw user search base + final PropertyValue rawUserSearchBase = configurationContext.getProperty(PROP_USER_SEARCH_BASE); + final PropertyValue rawUserObjectClass = configurationContext.getProperty(PROP_USER_OBJECT_CLASS); + final PropertyValue rawUserSearchScope = configurationContext.getProperty(PROP_USER_SEARCH_SCOPE); + + // if loading the users, ensure the object class set + if (rawUserSearchBase.isSet() && !rawUserObjectClass.isSet()) { + throw new SecurityProviderCreationException("LDAP user group provider 'User Object Class' must be specified when 'User Search Base' is set."); + } + + // if loading the users, ensure the search scope is set + if (rawUserSearchBase.isSet() && !rawUserSearchScope.isSet()) { + throw new SecurityProviderCreationException("LDAP user group provider 'User Search Scope' must be specified when 'User Search Base' is set."); + } + + // user search criteria + userSearchBase = rawUserSearchBase.getValue(); + userObjectClass = rawUserObjectClass.getValue(); + userSearchFilter = configurationContext.getProperty(PROP_USER_SEARCH_FILTER).getValue(); + userIdentityAttribute = configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE).getValue(); + userGroupNameAttribute = configurationContext.getProperty(PROP_USER_GROUP_ATTRIBUTE).getValue(); + userGroupReferencedGroupAttribute = configurationContext.getProperty(PROP_USER_GROUP_REFERENCED_GROUP_ATTRIBUTE).getValue(); + + try { + userSearchScope = SearchScope.valueOf(rawUserSearchScope.getValue()); + } catch (final IllegalArgumentException iae) { + throw new SecurityProviderCreationException(String.format("Unrecognized user search scope '%s'. Possible values are [%s]", + rawUserSearchScope.getValue(), StringUtils.join(SearchScope.values(), ", "))); + } + + // determine user behavior + useDnForUserIdentity = StringUtils.isBlank(userIdentityAttribute); + performUserSearch = StringUtils.isNotBlank(userSearchBase); + + // raw group search criteria + final PropertyValue rawGroupSearchBase = configurationContext.getProperty(PROP_GROUP_SEARCH_BASE); + final PropertyValue rawGroupObjectClass = configurationContext.getProperty(PROP_GROUP_OBJECT_CLASS); + final PropertyValue rawGroupSearchScope = configurationContext.getProperty(PROP_GROUP_SEARCH_SCOPE); + + // if loading the groups, ensure the object class is set + if (rawGroupSearchBase.isSet() && !rawGroupObjectClass.isSet()) { + throw new SecurityProviderCreationException("LDAP user group provider 'Group Object Class' must be specified when 'Group Search Base' is set."); + } + + // if loading the groups, ensure the search scope is set + if (rawGroupSearchBase.isSet() && !rawGroupSearchScope.isSet()) { + throw new SecurityProviderCreationException("LDAP user group provider 'Group Search Scope' must be specified when 'Group Search Base' is set."); + } + + // group search criteria + groupSearchBase = rawGroupSearchBase.getValue(); + groupObjectClass = rawGroupObjectClass.getValue(); + groupSearchFilter = configurationContext.getProperty(PROP_GROUP_SEARCH_FILTER).getValue(); + groupNameAttribute = configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE).getValue(); + groupMemberAttribute = configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE).getValue(); + groupMemberReferencedUserAttribute = configurationContext.getProperty(PROP_GROUP_MEMBER_REFERENCED_USER_ATTRIBUTE).getValue(); + + try { + groupSearchScope = SearchScope.valueOf(rawGroupSearchScope.getValue()); + } catch (final IllegalArgumentException iae) { + throw new SecurityProviderCreationException(String.format("Unrecognized group search scope '%s'. Possible values are [%s]", + rawGroupSearchScope.getValue(), StringUtils.join(SearchScope.values(), ", "))); + } + + // determine group behavior + useDnForGroupName = StringUtils.isBlank(groupNameAttribute); + performGroupSearch = StringUtils.isNotBlank(groupSearchBase); + + // ensure we are either searching users or groups (at least one must be specified) + if (!performUserSearch && !performGroupSearch) { + throw new SecurityProviderCreationException("LDAP user group provider 'User Search Base' or 'Group Search Base' must be specified."); + } + + // ensure group member attribute is set if searching groups but not users + if (performGroupSearch && !performUserSearch && StringUtils.isBlank(groupMemberAttribute)) { + throw new SecurityProviderCreationException("'Group Member Attribute' is required when searching groups but not users."); + } + + // ensure that performUserSearch is set when groupMemberReferencedUserAttribute is specified + if (StringUtils.isNotBlank(groupMemberReferencedUserAttribute) && !performUserSearch) { + throw new SecurityProviderCreationException("''User Search Base' must be set when specifying 'Group Member Attribute - Referenced User Attribute'."); + } + + // ensure that performGroupSearch is set when userGroupReferencedGroupAttribute is specified + if (StringUtils.isNotBlank(userGroupReferencedGroupAttribute) && !performGroupSearch) { + throw new SecurityProviderCreationException("'Group Search Base' must be set when specifying 'User Group Name Attribute - Referenced Group Attribute'."); + } + + // get the page size if configured + final PropertyValue rawPageSize = configurationContext.getProperty(PROP_PAGE_SIZE); + if (rawPageSize.isSet() && StringUtils.isNotBlank(rawPageSize.getValue())) { + pageSize = rawPageSize.asInteger(); + } + + // get whether group membership should be case sensitive + final String rawGroupMembershipEnforceCaseSensitivity = configurationContext.getProperty(PROP_GROUP_MEMBERSHIP_ENFORCE_CASE_SENSITIVITY).getValue(); + groupMembershipEnforceCaseSensitivity = Boolean.parseBoolean(rawGroupMembershipEnforceCaseSensitivity); + + // set the base environment is necessary + if (!baseEnvironment.isEmpty()) { + context.setBaseEnvironmentProperties(baseEnvironment); + } + + try { + // handling initializing beans + context.afterPropertiesSet(); + } catch (final Exception e) { + throw new SecurityProviderCreationException(e.getMessage(), e); + } + + final PropertyValue rawSyncInterval = configurationContext.getProperty(PROP_SYNC_INTERVAL); + final long syncInterval; + if (rawSyncInterval.isSet()) { + try { + syncInterval = FormatUtils.getTimeDuration(rawSyncInterval.getValue(), TimeUnit.MILLISECONDS); + } catch (final IllegalArgumentException iae) { + throw new SecurityProviderCreationException(String.format("The %s '%s' is not a valid time duration", PROP_SYNC_INTERVAL, rawSyncInterval.getValue())); + } + } else { + throw new SecurityProviderCreationException("The 'Sync Interval' must be specified."); + } + + try { + // perform the initial load, tenants must be loaded as the configured UserGroupProvider is supplied + // to the AccessPolicyProvider for granting initial permissions + load(context); + + // ensure the tenants were successfully synced + if (tenants.get() == null) { + throw new SecurityProviderCreationException("Unable to sync users and groups."); + } + + // schedule the background thread to load the users/groups + ldapSync.scheduleWithFixedDelay(() -> { + try { + load(context); + } catch (final Throwable t) { + logger.error("Failed to sync User/Groups from LDAP due to {}. Will try again in {} millis.", new Object[] {t.toString(), syncInterval}); + if (logger.isDebugEnabled()) { + logger.error("", t); + } + } + }, syncInterval, syncInterval, TimeUnit.MILLISECONDS); + } catch (final AuthorizationAccessException e) { + throw new SecurityProviderCreationException(e); + } + } + + @Override + public Set getUsers() throws AuthorizationAccessException { + return tenants.get().getAllUsers(); + } + + @Override + public User getUser(String identifier) throws AuthorizationAccessException { + return tenants.get().getUsersById().get(identifier); + } + + @Override + public User getUserByIdentity(String identity) throws AuthorizationAccessException { + return tenants.get().getUser(identity); + } + + @Override + public Set getGroups() throws AuthorizationAccessException { + return tenants.get().getAllGroups(); + } + + @Override + public Group getGroup(String identifier) throws AuthorizationAccessException { + return tenants.get().getGroupsById().get(identifier); + } + + @Override + public UserAndGroups getUserAndGroups(String identity) throws AuthorizationAccessException { + final TenantHolder holder = tenants.get(); + return new UserAndGroups() { + @Override + public User getUser() { + return holder.getUser(identity); + } + + @Override + public Set getGroups() { + return holder.getGroups(identity); + } + }; + } + + /** + * Reloads the tenants. + */ + private void load(final ContextSource contextSource) { + // create the ldapTemplate based on the context source. use a single source context to use the same connection + // to support paging when configured + final SingleContextSource singleContextSource = new SingleContextSource(contextSource.getReadOnlyContext()); + final LdapTemplate ldapTemplate = new LdapTemplate(singleContextSource); + + try { + final List userList = new ArrayList<>(); + final List groupList = new ArrayList<>(); + + // group dn -> user identifiers lookup + final Map> groupToUserIdentifierMappings = new HashMap<>(); + + // user dn -> user lookup + final Map userLookup = new HashMap<>(); + + if (performUserSearch) { + // search controls + final SearchControls userControls = new SearchControls(); + userControls.setSearchScope(userSearchScope.ordinal()); + + // consider paging support for users + final DirContextProcessor userProcessor; + if (pageSize == null) { + userProcessor = new NullDirContextProcessor(); + } else { + userProcessor = new PagedResultsDirContextProcessor(pageSize); + } + + // looking for objects matching the user object class + final AndFilter userFilter = new AndFilter(); + userFilter.and(new EqualsFilter("objectClass", userObjectClass)); + + // if a filter has been provided by the user, we add it to the filter + if (StringUtils.isNotBlank(userSearchFilter)) { + userFilter.and(new HardcodedFilter(userSearchFilter)); + } + + do { + userList.addAll(ldapTemplate.search(userSearchBase, userFilter.encode(), userControls, new AbstractContextMapper() { + @Override + protected User doMapFromContext(DirContextOperations ctx) { + // get the user identity + final String identity = getUserIdentity(ctx); + + // build the user + final User user = new User.Builder().identifierGenerateFromSeed(identity).identity(identity).build(); + + // store the user for group member later + userLookup.put(getReferencedUserValue(ctx), user); + + if (StringUtils.isNotBlank(userGroupNameAttribute)) { + final Attribute attributeGroups = ctx.getAttributes().get(userGroupNameAttribute); + + if (attributeGroups == null) { + logger.debug("User group name attribute [{}] does not exist for {}. " + + "This may be due to misconfiguration or this user record may not have any group membership attributes defined. " + + "Ignoring group membership. ", userGroupNameAttribute, identity); + } else { + try { + final NamingEnumeration groupValues = (NamingEnumeration) attributeGroups.getAll(); + while (groupValues.hasMoreElements()) { + final String groupValue = groupValues.next(); + + // if we are performing a group search, then we need to normalize the group value so that each + // user associating with it can be matched. if we are not performing a group search then these + // values will be used to actually build the group itself. case sensitivity is for group + // membership, not group identification. + final String groupValueNormalized; + if (performGroupSearch) { + groupValueNormalized = groupMembershipEnforceCaseSensitivity ? groupValue : groupValue.toLowerCase(); + } else { + groupValueNormalized = groupValue; + } + + // store the group -> user identifier mapping... if case sensitivity is disabled, the group reference value will + // be lowercased when adding to groupToUserIdentifierMappings + groupToUserIdentifierMappings.computeIfAbsent(groupValueNormalized, g -> new HashSet<>()).add(user.getIdentifier()); + } + } catch (NamingException e) { + throw new AuthorizationAccessException("Error while retrieving user group name attribute [" + userIdentityAttribute + "]."); + } + } + } + + return user; + } + }, userProcessor)); + } while (hasMorePages(userProcessor)); + } + + if (performGroupSearch) { + final SearchControls groupControls = new SearchControls(); + groupControls.setSearchScope(groupSearchScope.ordinal()); + + // consider paging support for groups + final DirContextProcessor groupProcessor; + if (pageSize == null) { + groupProcessor = new NullDirContextProcessor(); + } else { + groupProcessor = new PagedResultsDirContextProcessor(pageSize); + } + + // looking for objects matching the group object class + AndFilter groupFilter = new AndFilter(); + groupFilter.and(new EqualsFilter("objectClass", groupObjectClass)); + + // if a filter has been provided by the user, we add it to the filter + if(StringUtils.isNotBlank(groupSearchFilter)) { + groupFilter.and(new HardcodedFilter(groupSearchFilter)); + } + + do { + groupList.addAll(ldapTemplate.search(groupSearchBase, groupFilter.encode(), groupControls, new AbstractContextMapper() { + @Override + protected Group doMapFromContext(DirContextOperations ctx) { + final String dn = ctx.getDn().toString(); + + // get the group identity + final String name = getGroupName(ctx); + + // get the value of this group that may associate it to users + final String referencedGroupValue = getReferencedGroupValue(ctx); + + if (!StringUtils.isBlank(groupMemberAttribute)) { + Attribute attributeUsers = ctx.getAttributes().get(groupMemberAttribute); + if (attributeUsers == null) { + logger.debug("Group member attribute [{}] does not exist for {}. " + + "This may be due to misconfiguration or this group record may not have any user attributes defined. " + + "Ignoring group membership.", groupMemberAttribute, name); + } else { + try { + final NamingEnumeration userValues = (NamingEnumeration) attributeUsers.getAll(); + while (userValues.hasMoreElements()) { + final String userValue = userValues.next(); + + if (performUserSearch) { + // find the user by it's referenced attribute and add the identifier to this group. + // need to normalize here based on the desired case sensitivity. if case sensitivity + // is disabled, the user reference value will be lowercased when adding to userLookup + final String userValueNormalized = groupMembershipEnforceCaseSensitivity ? userValue : userValue.toLowerCase(); + final User user = userLookup.get(userValueNormalized); + + // ensure the user is known + if (user != null) { + groupToUserIdentifierMappings.computeIfAbsent(referencedGroupValue, g -> new HashSet<>()).add(user.getIdentifier()); + } else { + logger.debug(String.format("%s contains member %s but that user was not found while searching users. " + + "This may be due to misconfiguration or because that user is not a NiFi Registry user as defined by the User Search Base and Filter. " + + "Ignoring group membership.", name, userValue)); + } + } else { + // since performUserSearch is false, then the referenced group attribute must be blank... the user value must be the dn. + // no need to normalize here since group membership is driven solely through this group (not through the userLookup + // populated above). we are either going to use this value directly as the user identity or we are going to query + // the directory server again which should handle the case sensitivity accordingly. + final String userDn = userValue; + + final String userIdentity; + if (useDnForUserIdentity) { + // use the user value to avoid the unnecessary look up + userIdentity = identityMapper.mapUser(userDn); + } else { + // lookup the user to extract the user identity + userIdentity = getUserIdentity((DirContextAdapter) ldapTemplate.lookup(userDn)); + } + + // build the user + final User user = new User.Builder().identifierGenerateFromSeed(userIdentity).identity(userIdentity).build(); + + // add this user + userList.add(user); + groupToUserIdentifierMappings.computeIfAbsent(referencedGroupValue, g -> new HashSet<>()).add(user.getIdentifier()); + } + } + } catch (NamingException e) { + throw new AuthorizationAccessException("Error while retrieving group name attribute [" + groupNameAttribute + "]."); + } + } + } + + // build this group + final Group.Builder groupBuilder = new Group.Builder().identifierGenerateFromSeed(name).name(name); + + // add all users that were associated with this referenced group attribute + if (groupToUserIdentifierMappings.containsKey(referencedGroupValue)) { + groupToUserIdentifierMappings.remove(referencedGroupValue).forEach(userIdentifier -> groupBuilder.addUser(userIdentifier)); + } + + return groupBuilder.build(); + } + }, groupProcessor)); + } while (hasMorePages(groupProcessor)); + + // any remaining groupDn's were referenced by a user but not found while searching groups + groupToUserIdentifierMappings.forEach((referencedGroupValue, userIdentifiers) -> { + logger.debug(String.format("[%s] are members of %s but that group was not found while searching groups. " + + "This may be due to misconfiguration or because that group is not a NiFi Registry group as defined by the Group Search Base and Filter. " + + "Ignoring group membership.", StringUtils.join(userIdentifiers, ", "), referencedGroupValue)); + }); + } else { + // since performGroupSearch is false, then the referenced user attribute must be blank... the group value must be the dn + + // groups are not being searched so lookup any groups identified while searching users + groupToUserIdentifierMappings.forEach((groupDn, userIdentifiers) -> { + final String groupName; + if (useDnForGroupName) { + // use the dn to avoid the unnecessary look up + groupName = identityMapper.mapGroup(groupDn); + } else { + groupName = getGroupName((DirContextAdapter) ldapTemplate.lookup(groupDn)); + } + + // define the group + final Group.Builder groupBuilder = new Group.Builder().identifierGenerateFromSeed(groupName).name(groupName); + + // add each user + userIdentifiers.forEach(userIdentifier -> groupBuilder.addUser(userIdentifier)); + + // build the group + groupList.add(groupBuilder.build()); + }); + } + + if (logger.isDebugEnabled()) { + logger.debug("-------------------------------------"); + logger.debug("Loaded the following users from LDAP:"); + userList.forEach((user) -> logger.debug(" - " + user)); + logger.debug("--------------------------------------"); + logger.debug("Loaded the following groups from LDAP:"); + groupList.forEach((group) -> logger.debug(" - " + group)); + logger.debug("--------------------------------------"); + } + + // record the updated tenants + tenants.set(new TenantHolder(new HashSet<>(userList), new HashSet<>(groupList))); + } finally { + singleContextSource.destroy(); + } + } + + private boolean hasMorePages(final DirContextProcessor processor ) { + return processor instanceof PagedResultsDirContextProcessor && ((PagedResultsDirContextProcessor) processor).hasMore(); + } + + private String getUserIdentity(final DirContextOperations ctx) { + final String identity; + + if (useDnForUserIdentity) { + identity = ctx.getDn().toString(); + } else { + final Attribute attributeName = ctx.getAttributes().get(userIdentityAttribute); + if (attributeName == null) { + throw new AuthorizationAccessException("User identity attribute [" + userIdentityAttribute + "] does not exist."); + } + + try { + identity = (String) attributeName.get(); + } catch (NamingException e) { + throw new AuthorizationAccessException("Error while retrieving user name attribute [" + userIdentityAttribute + "]."); + } + } + + return identityMapper.mapUser(identity); + } + + private String getReferencedUserValue(final DirContextOperations ctx) { + final String referencedUserValue; + + if (StringUtils.isBlank(groupMemberReferencedUserAttribute)) { + referencedUserValue = ctx.getDn().toString(); + } else { + final Attribute attributeName = ctx.getAttributes().get(groupMemberReferencedUserAttribute); + if (attributeName == null) { + throw new AuthorizationAccessException("Referenced user value attribute [" + groupMemberReferencedUserAttribute + "] does not exist."); + } + + try { + referencedUserValue = (String) attributeName.get(); + } catch (NamingException e) { + throw new AuthorizationAccessException("Error while retrieving reference user value attribute [" + groupMemberReferencedUserAttribute + "]."); + } + } + + return groupMembershipEnforceCaseSensitivity ? referencedUserValue : referencedUserValue.toLowerCase(); + } + + private String getGroupName(final DirContextOperations ctx) { + final String name; + + if (useDnForGroupName) { + name = ctx.getDn().toString(); + } else { + final Attribute attributeName = ctx.getAttributes().get(groupNameAttribute); + if (attributeName == null) { + throw new AuthorizationAccessException("Group identity attribute [" + groupNameAttribute + "] does not exist."); + } + + try { + name = (String) attributeName.get(); + } catch (NamingException e) { + throw new AuthorizationAccessException("Error while retrieving group name attribute [" + groupNameAttribute + "]."); + } + } + + return identityMapper.mapGroup(name); + } + + private String getReferencedGroupValue(final DirContextOperations ctx) { + final String referencedGroupValue; + + if (StringUtils.isBlank(userGroupReferencedGroupAttribute)) { + referencedGroupValue = ctx.getDn().toString(); + } else { + final Attribute attributeName = ctx.getAttributes().get(userGroupReferencedGroupAttribute); + if (attributeName == null) { + throw new AuthorizationAccessException("Referenced group value attribute [" + userGroupReferencedGroupAttribute + "] does not exist."); + } + + try { + referencedGroupValue = (String) attributeName.get(); + } catch (NamingException e) { + throw new AuthorizationAccessException("Error while retrieving referenced group value attribute [" + userGroupReferencedGroupAttribute + "]."); + } + } + + return groupMembershipEnforceCaseSensitivity ? referencedGroupValue : referencedGroupValue.toLowerCase(); + } + + @AuthorizerContext + public void setIdentityMapper(final IdentityMapper identityMapper) { + this.identityMapper = identityMapper; + } + + @Override + public final void preDestruction() throws SecurityProviderDestructionException { + ldapSync.shutdown(); + try { + if (!ldapSync.awaitTermination(10000, TimeUnit.MILLISECONDS)) { + logger.info("Failed to stop ldap sync thread in 10 sec. Terminating"); + ldapSync.shutdownNow(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private void setTimeout(final AuthorizerConfigurationContext configurationContext, + final Map baseEnvironment, + final String configurationProperty, + final String environmentKey) { + + final PropertyValue rawTimeout = configurationContext.getProperty(configurationProperty); + if (rawTimeout.isSet()) { + try { + final Long timeout = FormatUtils.getTimeDuration(rawTimeout.getValue(), TimeUnit.MILLISECONDS); + baseEnvironment.put(environmentKey, timeout.toString()); + } catch (final IllegalArgumentException iae) { + throw new SecurityProviderCreationException(String.format("The %s '%s' is not a valid time duration", configurationProperty, rawTimeout)); + } + } + } + + private SSLContext getConfiguredSslContext(final AuthorizerConfigurationContext configurationContext) { + final String rawKeystore = configurationContext.getProperty("TLS - Keystore").getValue(); + final String rawKeystorePassword = configurationContext.getProperty("TLS - Keystore Password").getValue(); + final String rawKeystoreType = configurationContext.getProperty("TLS - Keystore Type").getValue(); + final String rawTruststore = configurationContext.getProperty("TLS - Truststore").getValue(); + final String rawTruststorePassword = configurationContext.getProperty("TLS - Truststore Password").getValue(); + final String rawTruststoreType = configurationContext.getProperty("TLS - Truststore Type").getValue(); + final String rawClientAuth = configurationContext.getProperty("TLS - Client Auth").getValue(); + final String rawProtocol = configurationContext.getProperty("TLS - Protocol").getValue(); + + // create the ssl context + final SSLContext sslContext; + try { + if (StringUtils.isBlank(rawKeystore) && StringUtils.isBlank(rawTruststore)) { + sslContext = null; + } else { + // ensure the protocol is specified + if (StringUtils.isBlank(rawProtocol)) { + throw new SecurityProviderCreationException("TLS - Protocol must be specified."); + } + + if (StringUtils.isBlank(rawKeystore)) { + sslContext = SslContextFactory.createTrustSslContext(rawTruststore, rawTruststorePassword.toCharArray(), rawTruststoreType, rawProtocol); + } else if (StringUtils.isBlank(rawTruststore)) { + sslContext = SslContextFactory.createSslContext(rawKeystore, rawKeystorePassword.toCharArray(), rawKeystoreType, rawProtocol); + } else { + // determine the client auth if specified + final ClientAuth clientAuth; + if (StringUtils.isBlank(rawClientAuth)) { + clientAuth = ClientAuth.NONE; + } else { + try { + clientAuth = ClientAuth.valueOf(rawClientAuth); + } catch (final IllegalArgumentException iae) { + throw new SecurityProviderCreationException(String.format("Unrecognized client auth '%s'. Possible values are [%s]", + rawClientAuth, StringUtils.join(ClientAuth.values(), ", "))); + } + } + + sslContext = SslContextFactory.createSslContext(rawKeystore, rawKeystorePassword.toCharArray(), rawKeystoreType, + rawTruststore, rawTruststorePassword.toCharArray(), rawTruststoreType, clientAuth, rawProtocol); + } + } + } catch (final KeyStoreException | NoSuchAlgorithmException | CertificateException | UnrecoverableKeyException | KeyManagementException | IOException e) { + throw new SecurityProviderCreationException(e.getMessage(), e); + } + + return sslContext; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/tenants/SearchScope.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/tenants/SearchScope.java new file mode 100644 index 0000000000..2e5e8a2092 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/tenants/SearchScope.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.ldap.tenants; + +/** + * Scope for searching a directory server. + */ +public enum SearchScope { + + OBJECT, + ONE_LEVEL, + SUBTREE; + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/tenants/TenantHolder.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/tenants/TenantHolder.java new file mode 100644 index 0000000000..7ef3a8c013 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/tenants/TenantHolder.java @@ -0,0 +1,165 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.ldap.tenants; + + +import org.apache.nifi.registry.security.authorization.Group; +import org.apache.nifi.registry.security.authorization.User; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * A holder to provide atomic access to user group data structures. + */ +public class TenantHolder { + + private final Set allUsers; + private final Map usersById; + private final Map usersByIdentity; + + private final Set allGroups; + private final Map groupsById; + private final Map> groupsByUserIdentity; + + /** + * Creates a new holder and populates all convenience data structures. + */ + public TenantHolder(final Set allUsers, final Set allGroups) { + // create a convenience map to retrieve a user by id + final Map userByIdMap = Collections.unmodifiableMap(createUserByIdMap(allUsers)); + + // create a convenience map to retrieve a user by identity + final Map userByIdentityMap = Collections.unmodifiableMap(createUserByIdentityMap(allUsers)); + + // create a convenience map to retrieve a group by id + final Map groupByIdMap = Collections.unmodifiableMap(createGroupByIdMap(allGroups)); + + // create a convenience map to retrieve the groups for a user identity + final Map> groupsByUserIdentityMap = Collections.unmodifiableMap(createGroupsByUserIdentityMap(allGroups, allUsers)); + + // set all the holders + this.allUsers = allUsers; + this.allGroups = allGroups; + this.usersById = userByIdMap; + this.usersByIdentity = userByIdentityMap; + this.groupsById = groupByIdMap; + this.groupsByUserIdentity = groupsByUserIdentityMap; + } + + /** + * Creates a Map from user identifier to User. + * + * @param users the set of all users + * @return the Map from user identifier to User + */ + private Map createUserByIdMap(final Set users) { + Map usersMap = new HashMap<>(); + for (User user : users) { + usersMap.put(user.getIdentifier(), user); + } + return usersMap; + } + + /** + * Creates a Map from user identity to User. + * + * @param users the set of all users + * @return the Map from user identity to User + */ + private Map createUserByIdentityMap(final Set users) { + Map usersMap = new HashMap<>(); + for (User user : users) { + usersMap.put(user.getIdentity(), user); + } + return usersMap; + } + + /** + * Creates a Map from group identifier to Group. + * + * @param groups the set of all groups + * @return the Map from group identifier to Group + */ + private Map createGroupByIdMap(final Set groups) { + Map groupsMap = new HashMap<>(); + for (Group group : groups) { + groupsMap.put(group.getIdentifier(), group); + } + return groupsMap; + } + + /** + * Creates a Map from user identity to the set of Groups for that identity. + * + * @param groups all groups + * @param users all users + * @return a Map from User identity to the set of Groups for that identity + */ + private Map> createGroupsByUserIdentityMap(final Set groups, final Set users) { + Map> groupsByUserIdentity = new HashMap<>(); + + for (User user : users) { + Set userGroups = new HashSet<>(); + for (Group group : groups) { + for (String groupUser : group.getUsers()) { + if (groupUser.equals(user.getIdentifier())) { + userGroups.add(group); + } + } + } + + groupsByUserIdentity.put(user.getIdentity(), userGroups); + } + + return groupsByUserIdentity; + } + + Set getAllUsers() { + return allUsers; + } + + Map getUsersById() { + return usersById; + } + + Set getAllGroups() { + return allGroups; + } + + Map getGroupsById() { + return groupsById; + } + + public User getUser(String identity) { + if (identity == null) { + throw new IllegalArgumentException("Identity cannot be null"); + } + return usersByIdentity.get(identity); + } + + public Set getGroups(String userIdentity) { + if (userIdentity == null) { + throw new IllegalArgumentException("User Identity cannot be null"); + } + return groupsByUserIdentity.get(userIdentity); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/util/ClassLoaderUtils.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/util/ClassLoaderUtils.java new file mode 100644 index 0000000000..1f05cd3bb6 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/util/ClassLoaderUtils.java @@ -0,0 +1,138 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FilenameFilter; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +public class ClassLoaderUtils { + + static final Logger LOGGER = LoggerFactory.getLogger(ClassLoaderUtils.class); + + public static ClassLoader getCustomClassLoader(String modulePath, ClassLoader parentClassLoader, FilenameFilter filenameFilter) throws MalformedURLException { + URL[] classpaths = getURLsForClasspath(modulePath, filenameFilter, false); + return createModuleClassLoader(classpaths, parentClassLoader); + } + + /** + * + * @param modulePath a module path to get URLs from, the module path may be + * a comma-separated list of paths + * @param filenameFilter a filter to apply when a module path is a directory + * and performs a listing, a null filter will return all matches + * @param suppressExceptions indicates whether to suppress exceptions + * @return an array of URL instances representing all of the modules + * resolved from processing modulePath + * @throws MalformedURLException if a module path does not exist + */ + public static URL[] getURLsForClasspath(String modulePath, FilenameFilter filenameFilter, boolean suppressExceptions) throws MalformedURLException { + return getURLsForClasspath(modulePath == null ? Collections.emptySet() : Collections.singleton(modulePath), filenameFilter, suppressExceptions); + } + + /** + * + * @param modulePaths one or modules paths to get URLs from, each module + * path may be a comma-separated list of paths + * @param filenameFilter a filter to apply when a module path is a directory + * and performs a listing, a null filter will return all matches + * @param suppressExceptions if true then all modules will attempt to be + * resolved even if some throw an exception, if false the first exception + * will be thrown + * @return an array of URL instances representing all of the modules + * resolved from processing modulePaths + * @throws MalformedURLException if a module path does not exist + */ + public static URL[] getURLsForClasspath(Set modulePaths, FilenameFilter filenameFilter, boolean suppressExceptions) throws MalformedURLException { + // use LinkedHashSet to maintain the ordering that the incoming paths are processed + Set modules = new LinkedHashSet<>(); + if (modulePaths != null) { + modulePaths.stream() + .flatMap(path -> Arrays.stream(path.split(","))) + .filter(path -> isNotBlank(path)) + .map(String::trim) + .forEach(m -> modules.add(m)); + } + return toURLs(modules, filenameFilter, suppressExceptions); + } + + private static boolean isNotBlank(final String value) { + return value != null && !value.trim().isEmpty(); + } + + protected static URL[] toURLs(Set modulePaths, FilenameFilter filenameFilter, boolean suppressExceptions) throws MalformedURLException { + List additionalClasspath = new LinkedList<>(); + if (modulePaths != null) { + for (String modulePathString : modulePaths) { + // If the path is already a URL, just add it (but don't check if it exists, too expensive and subject to network availability) + boolean isUrl = true; + try { + additionalClasspath.add(new URL(modulePathString)); + } catch (MalformedURLException mue) { + isUrl = false; + } + if (!isUrl) { + try { + File modulePath = new File(modulePathString); + + if (modulePath.exists()) { + + additionalClasspath.add(modulePath.toURI().toURL()); + + if (modulePath.isDirectory()) { + File[] files = modulePath.listFiles(filenameFilter); + + if (files != null) { + for (File classpathResource : files) { + if (classpathResource.isDirectory()) { + LOGGER.warn("Recursive directories are not supported, skipping " + classpathResource.getAbsolutePath()); + } else { + additionalClasspath.add(classpathResource.toURI().toURL()); + } + } + } + } + } else { + throw new MalformedURLException("Path specified does not exist"); + } + } catch (MalformedURLException e) { + if (!suppressExceptions) { + throw e; + } + } + } + } + } + return additionalClasspath.toArray(new URL[additionalClasspath.size()]); + } + + protected static ClassLoader createModuleClassLoader(URL[] modules, ClassLoader parentClassLoader) { + return new URLClassLoader(modules, parentClassLoader); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/util/XmlUtils.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/util/XmlUtils.java new file mode 100644 index 0000000000..2caa8faa46 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/util/XmlUtils.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.util; + +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; +import javax.xml.transform.stream.StreamSource; +import java.io.InputStream; + +public class XmlUtils { + + public static XMLStreamReader createSafeReader(InputStream inputStream) throws XMLStreamException { + if (inputStream == null) { + throw new IllegalArgumentException("The provided input stream cannot be null"); + } + return createSafeReader(new StreamSource(inputStream)); + } + + public static XMLStreamReader createSafeReader(StreamSource source) throws XMLStreamException { + if (source == null) { + throw new IllegalArgumentException("The provided source cannot be null"); + } + + XMLInputFactory xif = XMLInputFactory.newFactory(); + xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false); + xif.setProperty(XMLInputFactory.SUPPORT_DTD, false); + return xif.createXMLStreamReader(source); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/AbstractMultiVersionSerializer.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/AbstractMultiVersionSerializer.java new file mode 100644 index 0000000000..c5abe433e3 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/AbstractMultiVersionSerializer.java @@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.serialization; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + *

+ * A serializer for an entity of type T that maps a "version" of the data model to a serializer. + *

+ * + *

+ * When serializing, the serializer associated with the {@link #getCurrentDataModelVersion()} is used. + * The version will be written as a header at the beginning of the OutputStream then followed by the content. + *

+ * + *

+ * When deserializing, each registered serializer will be asked to read a data model version number from the input stream + * in descending version order until a version number is read successfully. + * + * Then the associated serializer to the read data model version is used to deserialize content back to the target object. + * If no serializer can read the version, or no serializer is registered for the read version, then SerializationException is thrown. + *

+ * + */ +public abstract class AbstractMultiVersionSerializer implements Serializer { + + private static final Logger logger = LoggerFactory.getLogger(AbstractMultiVersionSerializer.class); + + private final Map> serializersByVersion; + private final VersionedSerializer defaultSerializer; + private final List descendingVersions; + public static final int MAX_HEADER_BYTES = 1024; + + public AbstractMultiVersionSerializer() { + final Map> tempSerializers = createVersionedSerializers(); + this.serializersByVersion = Collections.unmodifiableMap(tempSerializers); + this.defaultSerializer = tempSerializers.get(getCurrentDataModelVersion()); + + final List sortedVersions = new ArrayList<>(serializersByVersion.keySet()); + sortedVersions.sort(Collections.reverseOrder(Integer::compareTo)); + this.descendingVersions = sortedVersions; + } + + /** + * Called from default constructor to create the map from data model version to corresponding serializer. + * + * @return the map of versioned serializers + */ + protected abstract Map> createVersionedSerializers(); + + /** + * @return the current data model version + */ + protected abstract int getCurrentDataModelVersion(); + + @Override + public void serialize(final T entity, final OutputStream out) throws SerializationException { + defaultSerializer.serialize(getCurrentDataModelVersion(), entity, out); + } + + @Override + public T deserialize(final InputStream input) throws SerializationException { + + final InputStream markSupportedInput = input.markSupported() ? input : new BufferedInputStream(input); + + // Mark the beginning of the stream. + markSupportedInput.mark(MAX_HEADER_BYTES); + + // Applying each serializer + for (int serializerVersion : descendingVersions) { + final VersionedSerializer serializer = serializersByVersion.get(serializerVersion); + + // Serializer version will not be the data model version always. + // E.g. higher version of serializer can read the old data model version number if it has the same header structure, + // but it does not mean the serializer is compatible with the old format. + final int version; + try { + version = serializer.readDataModelVersion(markSupportedInput); + if (!serializersByVersion.containsKey(version)) { + throw new SerializationException(String.format( + "Version %d was returned by %s, but no serializer is registered for that version.", version, serializer)); + } + } catch (SerializationException e) { + logger.debug("Deserialization failed with {}", serializer, e); + continue; + } finally { + // Either when continue with the next serializer, or proceed deserialization with the corresponding serializer, + // reset the stream position. + try { + markSupportedInput.reset(); + } catch (IOException resetException) { + // Should not happen. + logger.error("Unable to reset the input stream.", resetException); + } + } + + return serializersByVersion.get(version).deserialize(markSupportedInput); + } + + throw new SerializationException("Unable to find a serializer compatible with the input."); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/ExtensionSerializer.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/ExtensionSerializer.java new file mode 100644 index 0000000000..f8dfcae3c5 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/ExtensionSerializer.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.serialization; + +import org.apache.nifi.registry.extension.component.manifest.Extension; +import org.apache.nifi.registry.serialization.jackson.JacksonExtensionSerializer; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + +/** + * See {@link AbstractMultiVersionSerializer} for further information. + * + *

+ * Current data model version is 1. + * Data Model Version Histories: + *

    + *
  • version 1: Serialized by {@link org.apache.nifi.registry.serialization.jackson.JacksonExtensionSerializer}
  • + *
+ *

+ */ +@Service +public class ExtensionSerializer extends AbstractMultiVersionSerializer { + + static final Integer CURRENT_DATA_MODEL_VERSION = 1; + + @Override + protected Map> createVersionedSerializers() { + final Map> tempMap = new HashMap<>(); + tempMap.put(CURRENT_DATA_MODEL_VERSION, new JacksonExtensionSerializer()); + return tempMap; + } + + @Override + protected int getCurrentDataModelVersion() { + return CURRENT_DATA_MODEL_VERSION; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/FlowContent.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/FlowContent.java new file mode 100644 index 0000000000..8340bdd131 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/FlowContent.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.serialization; + +import org.apache.nifi.registry.flow.VersionedFlowSnapshot; + +/** + * Wrapper element to contain everything that is serialized for a given version of a flow. + */ +public class FlowContent { + + private VersionedFlowSnapshot flowSnapshot; + + public VersionedFlowSnapshot getFlowSnapshot() { + return flowSnapshot; + } + + public void setFlowSnapshot(VersionedFlowSnapshot flowSnapshot) { + this.flowSnapshot = flowSnapshot; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/FlowContentSerializer.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/FlowContentSerializer.java new file mode 100644 index 0000000000..3466aa93c5 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/FlowContentSerializer.java @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.serialization; + +import org.apache.nifi.registry.flow.VersionedProcessGroup; +import org.apache.nifi.registry.serialization.jackson.JacksonFlowContentSerializer; +import org.apache.nifi.registry.serialization.jackson.JacksonVersionedProcessGroupSerializer; +import org.apache.nifi.registry.serialization.jaxb.JAXBVersionedProcessGroupSerializer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Serializer that handles versioned serialization for flow content. + * + *

+ * Current data model version is 3. + * Data Model Version Histories: + *

    + *
  • version 3: Serialized by {@link JacksonFlowContentSerializer}
  • + *
  • version 2: Serialized by {@link JacksonVersionedProcessGroupSerializer}
  • + *
  • version 1: Serialized by {@link JAXBVersionedProcessGroupSerializer}
  • + *
+ *

+ */ +@Service +public class FlowContentSerializer { + + private static final Logger logger = LoggerFactory.getLogger(FlowContentSerializer.class); + + static final Integer START_USING_SNAPSHOT_VERSION = 3; + static final Integer CURRENT_DATA_MODEL_VERSION = 3; + + private final Map> processGroupSerializers; + private final Map> flowContentSerializers; + private final Map> allSerializers; + + private final List descendingVersions; + + public FlowContentSerializer() { + final Map> tempFlowContentSerializers = new HashMap<>(); + tempFlowContentSerializers.put(3, new JacksonFlowContentSerializer()); + flowContentSerializers = Collections.unmodifiableMap(tempFlowContentSerializers); + + final Map> tempProcessGroupSerializers = new HashMap<>(); + tempProcessGroupSerializers.put(2, new JacksonVersionedProcessGroupSerializer()); + tempProcessGroupSerializers.put(1, new JAXBVersionedProcessGroupSerializer()); + processGroupSerializers = Collections.unmodifiableMap(tempProcessGroupSerializers); + + final Map> tempAllSerializers = new HashMap<>(); + tempAllSerializers.putAll(processGroupSerializers); + tempAllSerializers.putAll(flowContentSerializers); + allSerializers = Collections.unmodifiableMap(tempAllSerializers); + + final List sortedVersions = new ArrayList<>(allSerializers.keySet()); + sortedVersions.sort(Collections.reverseOrder(Integer::compareTo)); + this.descendingVersions = sortedVersions; + } + + /** + * Tries to read a data model version using each VersionedSerializer, in descending version order. + * If no version could be read from any serializer, then a SerializationException is thrown. + * + * When deserializing, clients are expected to call this method to obtain the version, then call + * {@method isProcessGroupVersion}, which then determines if {@method deserializeProcessGroup} + * should be used, or if {@method deserializeFlowContent} should be used. + * + * @param input the input stream containing serialized flow content + * @return the data model version from the input stream + * @throws SerializationException if the data model version could not be read with any serializer + */ + public int readDataModelVersion(final InputStream input) throws SerializationException { + final InputStream markSupportedInput = input.markSupported() ? input : new BufferedInputStream(input); + + // Mark the beginning of the stream. + markSupportedInput.mark(SerializationConstants.MAX_HEADER_BYTES); + + // Try each serializer in descending version order + for (final int serializerVersion : descendingVersions) { + final VersionedSerializer serializer = allSerializers.get(serializerVersion); + try { + return serializer.readDataModelVersion(markSupportedInput); + } catch (SerializationException e) { + if (logger.isDebugEnabled()) { + logger.error("Unable to read the data model version due to: {}", e.getMessage()); + } + continue; + } finally { + // Reset the stream position. + try { + markSupportedInput.reset(); + } catch (IOException resetException) { + // Should not happen. + logger.error("Unable to reset the input stream.", resetException); + } + } + } + + throw new SerializationException("Unable to read the data model version for the flow content."); + } + + public Integer getCurrentDataModelVersion() { + return CURRENT_DATA_MODEL_VERSION; + } + + public boolean isProcessGroupVersion(final int dataModelVersion) { + return dataModelVersion < START_USING_SNAPSHOT_VERSION; + } + + public VersionedProcessGroup deserializeProcessGroup(final int dataModelVersion, final InputStream input) throws SerializationException { + final VersionedSerializer serializer = processGroupSerializers.get(dataModelVersion); + if (serializer == null) { + throw new IllegalArgumentException("No VersionedProcessGroup serializer exists for data model version: " + dataModelVersion); + } + + return serializer.deserialize(input); + } + + public FlowContent deserializeFlowContent(final int dataModelVersion, final InputStream input) throws SerializationException { + final VersionedSerializer serializer = flowContentSerializers.get(dataModelVersion); + if (serializer == null) { + throw new IllegalArgumentException("No FlowContent serializer exists for data model version: " + dataModelVersion); + } + + return serializer.deserialize(input); + } + + public void serializeFlowContent(final FlowContent flowContent, final OutputStream out) throws SerializationException { + final VersionedSerializer serializer = flowContentSerializers.get(CURRENT_DATA_MODEL_VERSION); + serializer.serialize(CURRENT_DATA_MODEL_VERSION, flowContent, out); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/SerializationConstants.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/SerializationConstants.java new file mode 100644 index 0000000000..9c9c3841f5 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/SerializationConstants.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.serialization; + +public interface SerializationConstants { + + int MAX_HEADER_BYTES = 1024; + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/SerializationException.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/SerializationException.java new file mode 100644 index 0000000000..dd05e7709a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/SerializationException.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.serialization; + +/** + * An error that can occur during serialization or deserialization. + */ +public class SerializationException extends RuntimeException { + + public SerializationException(String message) { + super(message); + } + + public SerializationException(String message, Throwable cause) { + super(message, cause); + } + + public SerializationException(Throwable cause) { + super(cause); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/Serializer.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/Serializer.java new file mode 100644 index 0000000000..ca424ad273 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/Serializer.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.serialization; + +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Serializes and de-serializes objects. + */ +public interface Serializer { + + /** + * Serializes a snapshot to the given output stream. + * + * @param t the object to serialize + * @param out the output stream to serialize to + */ + void serialize(T t, OutputStream out) throws SerializationException; + + /** + * Deserializes the given InputStream back to an object of the given type. + * + * @param input the InputStream to deserialize + * @return the deserialized object + */ + T deserialize(InputStream input) throws SerializationException; + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/VersionedSerializer.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/VersionedSerializer.java new file mode 100644 index 0000000000..b3c626f782 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/VersionedSerializer.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.serialization; + +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Serializes and de-serializes objects. + * This interface is designed to provide backward compatibility to different versioned serialization formats. + * So that serialized data model and format can evolve overtime. + */ +public interface VersionedSerializer { + + /** + * Serialize the given object into the target output stream with the specified version format. + * Implementation classes are responsible to serialize the version to the head of the serialized content, + * so that it can be retrieved by {@link #readDataModelVersion(InputStream)} method efficiently + * without reading the entire byte array. + * + * @param dataModelVersion the data model version + * @param t the object to serialize + * @param out the target output stream + * @throws SerializationException thrown when serialization failed + */ + void serialize(int dataModelVersion, T t, OutputStream out) throws SerializationException; + + /** + * Read data model version from the given InputStream. + *

+ * Even if an implementation serializer was able to read a version, it does not necessary mean + * the same serializers {@link #deserialize(InputStream)} method will be called. + * For example, when the header structure has not been changed, the newer version of serializer may be able to + * read older data model version. But deserialization should be done with the older serializer. + *

+ * @param input the input stream to read version from + * @return the read data model version + * @throws SerializationException thrown when reading version failed + */ + int readDataModelVersion(InputStream input) throws SerializationException; + + /** + * Deserializes the given InputStream back to an object of the given type. + * + * @param input the InputStream to deserialize, + * the position of input is reset to the the beginning of the stream when this method is called + * @return the deserialized object + * @throws SerializationException thrown when deserialization failed + */ + T deserialize(InputStream input) throws SerializationException; +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/JacksonExtensionSerializer.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/JacksonExtensionSerializer.java new file mode 100644 index 0000000000..82994dd084 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/JacksonExtensionSerializer.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.serialization.jackson; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.apache.nifi.registry.extension.component.manifest.Extension; +import org.apache.nifi.registry.serialization.SerializationException; + +/** + * A Jackson serializer for Extensions. + */ +public class JacksonExtensionSerializer extends JacksonSerializer { + + @Override + TypeReference> getDeserializeTypeRef() throws SerializationException { + return new TypeReference>() {}; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/JacksonFlowContentSerializer.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/JacksonFlowContentSerializer.java new file mode 100644 index 0000000000..77dd37aadc --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/JacksonFlowContentSerializer.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.serialization.jackson; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.apache.nifi.registry.serialization.FlowContent; +import org.apache.nifi.registry.serialization.SerializationException; + +/** + * A Jackson serializer for FlowContent. + */ +public class JacksonFlowContentSerializer extends JacksonSerializer { + + @Override + TypeReference> getDeserializeTypeRef() throws SerializationException { + return new TypeReference>() {}; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/JacksonSerializer.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/JacksonSerializer.java new file mode 100644 index 0000000000..d159ea3618 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/JacksonSerializer.java @@ -0,0 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.serialization.jackson; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.nifi.registry.serialization.SerializationConstants; +import org.apache.nifi.registry.serialization.SerializationException; +import org.apache.nifi.registry.serialization.VersionedSerializer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; + +/** + * A Serializer that uses Jackson for serializing/deserializing. + */ +public abstract class JacksonSerializer implements VersionedSerializer { + + private static final Logger logger = LoggerFactory.getLogger(JacksonSerializer.class); + + private static final String JSON_HEADER = "\"header\""; + private static final String DATA_MODEL_VERSION = "dataModelVersion"; + + private final ObjectMapper objectMapper = ObjectMapperProvider.getMapper(); + + @Override + public void serialize(int dataModelVersion, T t, OutputStream out) throws SerializationException { + if (t == null) { + throw new IllegalArgumentException("The object to serialize cannot be null"); + } + + if (out == null) { + throw new IllegalArgumentException("OutputStream cannot be null"); + } + + final SerializationContainer container = new SerializationContainer<>(); + container.setHeader(Collections.singletonMap(DATA_MODEL_VERSION, String.valueOf(dataModelVersion))); + container.setContent(t); + + try { + objectMapper.writerWithDefaultPrettyPrinter().writeValue(out, container); + } catch (IOException e) { + throw new SerializationException("Unable to serialize object", e); + } + } + + @Override + public T deserialize(InputStream input) throws SerializationException { + final TypeReference> typeRef = getDeserializeTypeRef(); + try { + final SerializationContainer container = objectMapper.readValue(input, typeRef); + return container.getContent(); + } catch (IOException e) { + throw new SerializationException("Unable to deserialize object", e); + } + } + + abstract TypeReference> getDeserializeTypeRef() throws SerializationException; + + @Override + public int readDataModelVersion(InputStream input) throws SerializationException { + final byte[] headerBytes = new byte[SerializationConstants.MAX_HEADER_BYTES]; + final int readHeaderBytes; + try { + readHeaderBytes = input.read(headerBytes); + } catch (IOException e) { + throw new SerializationException("Could not read additional bytes to parse as serialization version 2 or later. " + + e.getMessage(), e); + } + + // Seek '"header"'. + final String headerStr = new String(headerBytes, 0, readHeaderBytes, StandardCharsets.UTF_8); + final int headerIndex = headerStr.indexOf(JSON_HEADER); + if (headerIndex < 0) { + throw new SerializationException(String.format("Could not find %s in the first %d bytes", + JSON_HEADER, readHeaderBytes)); + } + + final int headerStart = headerStr.indexOf("{", headerIndex); + if (headerStart < 0) { + throw new SerializationException(String.format("Could not find '{' starting header object in the first %d bytes.", readHeaderBytes)); + } + + final int headerEnd = headerStr.indexOf("}", headerStart); + if (headerEnd < 0) { + throw new SerializationException(String.format("Could not find '}' ending header object in the first %d bytes.", readHeaderBytes)); + } + + final String headerObjectStr = headerStr.substring(headerStart, headerEnd + 1); + logger.debug("headerObjectStr={}", headerObjectStr); + + try { + final TypeReference> typeRef = new TypeReference>() {}; + final HashMap header = objectMapper.readValue(headerObjectStr, typeRef); + if (!header.containsKey(DATA_MODEL_VERSION)) { + throw new SerializationException("Missing " + DATA_MODEL_VERSION); + } + + return Integer.parseInt(header.get(DATA_MODEL_VERSION)); + } catch (IOException e) { + throw new SerializationException(String.format("Failed to parse header string '%s' due to %s", headerObjectStr, e), e); + } catch (NumberFormatException e) { + throw new SerializationException(String.format("Failed to parse version string due to %s", e.getMessage()), e); + } + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/JacksonVersionedProcessGroupSerializer.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/JacksonVersionedProcessGroupSerializer.java new file mode 100644 index 0000000000..4bd763ec1f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/JacksonVersionedProcessGroupSerializer.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.serialization.jackson; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.apache.nifi.registry.flow.VersionedProcessGroup; +import org.apache.nifi.registry.serialization.SerializationException; + +/** + * A Jackson serializer for VersionedProcessGroups. + */ +public class JacksonVersionedProcessGroupSerializer extends JacksonSerializer { + + @Override + TypeReference> getDeserializeTypeRef() throws SerializationException { + return new TypeReference>() {}; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/ObjectMapperProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/ObjectMapperProvider.java new file mode 100644 index 0000000000..bfe915edc5 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/ObjectMapperProvider.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.serialization.jackson; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +/** + * Provides a singleton ObjectMapper. + */ +@Configuration +public class ObjectMapperProvider { + + private static final ObjectMapper mapper = new ObjectMapper(); + + static { + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + mapper.setDefaultPropertyInclusion(JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, JsonInclude.Include.NON_NULL)); + mapper.setAnnotationIntrospector(new JaxbAnnotationIntrospector(mapper.getTypeFactory())); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true); + } + + public static ObjectMapper getMapper() { + return mapper; + } + + @Bean + @Primary + public ObjectMapper getObjectMapperBean() { + return getMapper(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/SerializationContainer.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/SerializationContainer.java new file mode 100644 index 0000000000..8c4d474017 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/SerializationContainer.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.serialization.jackson; + +import io.swagger.annotations.ApiModelProperty; + +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; +import java.util.Map; + +@XmlRootElement +@XmlType(propOrder = {"header", "content"}) +public class SerializationContainer { + + private Map header; + private T content; + + @ApiModelProperty("The serialization headers") + public Map getHeader() { + return header; + } + + public void setHeader(Map header) { + this.header = header; + } + + @ApiModelProperty("The serialized content") + public T getContent() { + return content; + } + + public void setContent(T content) { + this.content = content; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jaxb/JAXBSerializer.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jaxb/JAXBSerializer.java new file mode 100644 index 0000000000..5290fb5ada --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jaxb/JAXBSerializer.java @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.serialization.jaxb; + +import org.apache.nifi.registry.serialization.SerializationException; +import org.apache.nifi.registry.serialization.VersionedSerializer; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +/** + * A Serializer that uses JAXB for serializing/deserializing. + */ +public class JAXBSerializer implements VersionedSerializer { + + private static final String MAGIC_HEADER = "Flows"; + private static final byte[] MAGIC_HEADER_BYTES = MAGIC_HEADER.getBytes(StandardCharsets.UTF_8); + + private final JAXBContext jaxbContext; + + /** + * Load the JAXBContext. + */ + public JAXBSerializer(final Class clazz) { + try { + this.jaxbContext = JAXBContext.newInstance(clazz); + } catch (JAXBException e) { + throw new RuntimeException("Unable to create JAXBContext: " + e.getMessage(), e); + } + } + + @Override + public void serialize(final int dataModelVersion, final T t, final OutputStream out) throws SerializationException { + if (t == null) { + throw new IllegalArgumentException("The object to serialize cannot be null"); + } + + if (out == null) { + throw new IllegalArgumentException("OutputStream cannot be null"); + } + + final ByteBuffer byteBuffer = ByteBuffer.allocate(9); + byteBuffer.put(MAGIC_HEADER_BYTES); + byteBuffer.putInt(dataModelVersion); + + try { + out.write(byteBuffer.array()); + } catch (final IOException e) { + throw new SerializationException("Unable to write header while serializing process group", e); + } + + try { + final Marshaller marshaller = jaxbContext.createMarshaller(); + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); + marshaller.marshal(t, out); + } catch (JAXBException e) { + throw new SerializationException("Unable to serialize object", e); + } + } + + @Override + public T deserialize(final InputStream input) throws SerializationException { + if (input == null) { + throw new IllegalArgumentException("InputStream cannot be null"); + } + + try { + // Consume the header bytes. + readDataModelVersion(input); + final Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); + return (T) unmarshaller.unmarshal(input); + } catch (JAXBException e) { + throw new SerializationException("Unable to deserialize object", e); + } + } + + @Override + public int readDataModelVersion(InputStream input) throws SerializationException { + final int headerLength = 9; + final byte[] buffer = new byte[headerLength]; + + int bytesRead = -1; + try { + bytesRead = input.read(buffer, 0, headerLength); + } catch (final IOException e) { + throw new SerializationException("Unable to read header while deserializing process group", e); + } + + if (bytesRead < headerLength) { + throw new SerializationException("Unable to read header while deserializing process group, expected" + + headerLength + " bytes, but found " + bytesRead); + } + + final ByteBuffer bb = ByteBuffer.wrap(buffer); + final byte[] magicHeaderBytes = new byte[MAGIC_HEADER_BYTES.length]; + bb.get(magicHeaderBytes); + for (int i = 0; i < MAGIC_HEADER_BYTES.length; i++) { + if (MAGIC_HEADER_BYTES[i] != magicHeaderBytes[i]) { + throw new SerializationException("Unable to read header while deserializing process group." + + " Header byte sequence does not match"); + } + } + + return bb.getInt(MAGIC_HEADER_BYTES.length); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jaxb/JAXBVersionedProcessGroupSerializer.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jaxb/JAXBVersionedProcessGroupSerializer.java new file mode 100644 index 0000000000..3efdd3363e --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jaxb/JAXBVersionedProcessGroupSerializer.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.serialization.jaxb; + +import org.apache.nifi.registry.flow.VersionedProcessGroup; + +/** + * A JAXB serializer for VersionedFlowSnapshots. + */ +public class JAXBVersionedProcessGroupSerializer extends JAXBSerializer { + + public JAXBVersionedProcessGroupSerializer() { + super(VersionedProcessGroup.class); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/AuthorizationService.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/AuthorizationService.java new file mode 100644 index 0000000000..1a348c3cd6 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/AuthorizationService.java @@ -0,0 +1,856 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.service; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.authorization.AccessPolicy; +import org.apache.nifi.registry.authorization.AccessPolicySummary; +import org.apache.nifi.registry.authorization.CurrentUser; +import org.apache.nifi.registry.authorization.Permissions; +import org.apache.nifi.registry.authorization.Resource; +import org.apache.nifi.registry.authorization.ResourcePermissions; +import org.apache.nifi.registry.authorization.Tenant; +import org.apache.nifi.registry.authorization.User; +import org.apache.nifi.registry.authorization.UserGroup; +import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.exception.ResourceNotFoundException; +import org.apache.nifi.registry.security.authorization.AccessPolicyProvider; +import org.apache.nifi.registry.security.authorization.AccessPolicyProviderInitializationContext; +import org.apache.nifi.registry.security.authorization.AuthorizableLookup; +import org.apache.nifi.registry.security.authorization.Authorizer; +import org.apache.nifi.registry.security.authorization.AuthorizerCapabilityDetection; +import org.apache.nifi.registry.security.authorization.AuthorizerConfigurationContext; +import org.apache.nifi.registry.security.authorization.ConfigurableAccessPolicyProvider; +import org.apache.nifi.registry.security.authorization.ConfigurableUserGroupProvider; +import org.apache.nifi.registry.security.authorization.Group; +import org.apache.nifi.registry.security.authorization.ManagedAuthorizer; +import org.apache.nifi.registry.security.authorization.RequestAction; +import org.apache.nifi.registry.security.authorization.UntrustedProxyException; +import org.apache.nifi.registry.security.authorization.UserAndGroups; +import org.apache.nifi.registry.security.authorization.UserGroupProvider; +import org.apache.nifi.registry.security.authorization.UserGroupProviderInitializationContext; +import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException; +import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException; +import org.apache.nifi.registry.security.authorization.resource.Authorizable; +import org.apache.nifi.registry.security.authorization.resource.ResourceFactory; +import org.apache.nifi.registry.security.authorization.resource.ResourceType; +import org.apache.nifi.registry.security.authorization.user.NiFiUser; +import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Service for performing operations on users, groups, and policies. + */ +@Service +public class AuthorizationService { + + private static final Logger LOGGER = LoggerFactory.getLogger(AuthorizationService.class); + + public static final String MSG_NON_MANAGED_AUTHORIZER = "This NiFi Registry is not configured to internally manage users, groups, or policies. Please contact your system administrator."; + public static final String MSG_NON_CONFIGURABLE_POLICIES = "This NiFi Registry is not configured to allow configurable policies. Please contact your system administrator."; + public static final String MSG_NON_CONFIGURABLE_USERS = "This NiFi Registry is not configured to allow configurable users and groups. Please contact your system administrator."; + + private AuthorizableLookup authorizableLookup; + private Authorizer authorizer; + private RegistryService registryService; + private UserGroupProvider userGroupProvider; + private AccessPolicyProvider accessPolicyProvider; + + @Autowired + public AuthorizationService( + final AuthorizableLookup authorizableLookup, + final Authorizer authorizer, + final RegistryService registryService) { + this.authorizableLookup = authorizableLookup; + this.authorizer = authorizer; + this.registryService = registryService; + + if (AuthorizerCapabilityDetection.isManagedAuthorizer(this.authorizer)) { + this.accessPolicyProvider = ((ManagedAuthorizer) authorizer).getAccessPolicyProvider(); + } else { + this.accessPolicyProvider = createExceptionThrowingAccessPolicyProvider(); + } + this.userGroupProvider = accessPolicyProvider.getUserGroupProvider(); + } + + + // ---------------------- Authorization methods ------------------------------------- + + public AuthorizableLookup getAuthorizableLookup() { + return authorizableLookup; + } + + public void authorize(Authorizable authorizable, RequestAction action) throws AccessDeniedException { + authorizable.authorize(authorizer, action, NiFiUserUtils.getNiFiUser()); + } + + public boolean isManagedAuthorizer() { + return AuthorizerCapabilityDetection.isManagedAuthorizer(authorizer); + } + + public boolean isConfigurableUserGroupProvider() { + return AuthorizerCapabilityDetection.isConfigurableUserGroupProvider(authorizer); + } + + public boolean isConfigurableAccessPolicyProvider() { + return AuthorizerCapabilityDetection.isConfigurableAccessPolicyProvider(authorizer); + } + + public void verifyAuthorizerIsManaged() { + if (!isManagedAuthorizer()) { + throw new IllegalStateException(AuthorizationService.MSG_NON_MANAGED_AUTHORIZER); + } + } + + public void verifyAuthorizerSupportsConfigurablePolicies() { + if (!isConfigurableAccessPolicyProvider()) { + verifyAuthorizerIsManaged(); + throw new IllegalStateException(AuthorizationService.MSG_NON_CONFIGURABLE_POLICIES); + } + } + + public void verifyAuthorizerSupportsConfigurableUserGroups() { + if (!isConfigurableUserGroupProvider()) { + throw new IllegalStateException(AuthorizationService.MSG_NON_CONFIGURABLE_USERS); + } + } + + // ---------------------- Permissions methods --------------------------------------- + + public CurrentUser getCurrentUser() { + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + final CurrentUser currentUser = new CurrentUser(); + currentUser.setIdentity(user.getIdentity()); + currentUser.setAnonymous(user.isAnonymous()); + currentUser.setResourcePermissions(getTopLevelPermissions()); + return currentUser; + } + + public Permissions getPermissionsForResource(Authorizable authorizableResource) { + NiFiUser user = NiFiUserUtils.getNiFiUser(); + final Permissions permissions = new Permissions(); + permissions.setCanRead(authorizableResource.isAuthorized(authorizer, RequestAction.READ, user)); + permissions.setCanWrite(authorizableResource.isAuthorized(authorizer, RequestAction.WRITE, user)); + permissions.setCanDelete(authorizableResource.isAuthorized(authorizer, RequestAction.DELETE, user)); + return permissions; + } + + public Permissions getPermissionsForResource(Authorizable authorizableResource, Permissions knownParentAuthorizablePermissions) { + if (knownParentAuthorizablePermissions == null) { + return getPermissionsForResource(authorizableResource); + } + + final Permissions permissions = new Permissions(knownParentAuthorizablePermissions); + NiFiUser user = NiFiUserUtils.getNiFiUser(); + + if (!permissions.getCanRead()) { + permissions.setCanRead(authorizableResource.isAuthorized(authorizer, RequestAction.READ, user)); + } + + if (!permissions.getCanWrite()) { + permissions.setCanWrite(authorizableResource.isAuthorized(authorizer, RequestAction.WRITE, user)); + } + + if (!permissions.getCanDelete()) { + permissions.setCanDelete(authorizableResource.isAuthorized(authorizer, RequestAction.DELETE, user)); + } + + return permissions; + } + + private ResourcePermissions getTopLevelPermissions() { + + NiFiUser user = NiFiUserUtils.getNiFiUser(); + ResourcePermissions resourcePermissions = new ResourcePermissions(); + + final Permissions bucketsPermissions = getPermissionsForResource(authorizableLookup.getBucketsAuthorizable()); + resourcePermissions.setBuckets(bucketsPermissions); + + final Permissions policiesPermissions = getPermissionsForResource(authorizableLookup.getPoliciesAuthorizable()); + resourcePermissions.setPolicies(policiesPermissions); + + final Permissions tenantsPermissions = getPermissionsForResource(authorizableLookup.getTenantsAuthorizable()); + resourcePermissions.setTenants(tenantsPermissions); + + final Permissions proxyPermissions = getPermissionsForResource(authorizableLookup.getProxyAuthorizable()); + resourcePermissions.setProxy(proxyPermissions); + + return resourcePermissions; + } + + // ---------------------- User methods ---------------------------------------------- + + public User createUser(final User user) { + verifyUserGroupProviderIsConfigurable(); + + if (StringUtils.isBlank(user.getIdentity())) { + throw new IllegalArgumentException("User identity must be specified when creating a new user."); + } + + final org.apache.nifi.registry.security.authorization.User createdUser = + configurableUserGroupProvider().addUser(userFromDTO(user)); + return userToDTO(createdUser); + } + + public List getUsers() { + return userGroupProvider.getUsers().stream().map(this::userToDTO).collect(Collectors.toList()); + } + + public User getUser(final String identifier) { + final org.apache.nifi.registry.security.authorization.User user = userGroupProvider.getUser(identifier); + if (user == null) { + LOGGER.warn("The specified user id [{}] does not exist.", identifier); + throw new ResourceNotFoundException("The specified user ID does not exist in this registry."); + } + + return userToDTO(user); + } + + public User getUserByIdentity(final String identity) { + final org.apache.nifi.registry.security.authorization.User user = userGroupProvider.getUserByIdentity(identity); + if (user == null) { + LOGGER.warn("The specified user identity [{}] does not exist.", identity); + throw new ResourceNotFoundException("The specified user ID does not exist in this registry."); + } + + return userToDTO(user); + } + + public void verifyUserExists(final String identifier) { + final org.apache.nifi.registry.security.authorization.User user = userGroupProvider.getUser(identifier); + if (user == null) { + LOGGER.warn("The specified user id [{}] does not exist.", identifier); + throw new ResourceNotFoundException("The specified user ID does not exist in this registry."); + } + } + + public User updateUser(final User user) { + verifyUserGroupProviderIsConfigurable(); + + final org.apache.nifi.registry.security.authorization.User updatedUser = + configurableUserGroupProvider().updateUser(userFromDTO(user)); + + if (updatedUser == null) { + LOGGER.warn("The specified user id [{}] does not exist.", user.getIdentifier()); + throw new ResourceNotFoundException("The specified user ID does not exist in this registry."); + } + + return userToDTO(updatedUser); + } + + public User deleteUser(final String identifier) { + verifyUserGroupProviderIsConfigurable(); + + final org.apache.nifi.registry.security.authorization.User user = userGroupProvider.getUser(identifier); + if (user == null) { + LOGGER.warn("The specified user id [{}] does not exist.", identifier); + throw new ResourceNotFoundException("The specified user ID does not exist in this registry."); + } + + configurableUserGroupProvider().deleteUser(user); + return userToDTO(user); + } + + // ---------------------- User Group methods -------------------------------------- + + public UserGroup createUserGroup(final UserGroup userGroup) { + verifyUserGroupProviderIsConfigurable(); + + if (StringUtils.isBlank(userGroup.getIdentity())) { + throw new IllegalArgumentException("User group identity must be specified when creating a new group."); + } + + final org.apache.nifi.registry.security.authorization.Group createdGroup = + configurableUserGroupProvider().addGroup(userGroupFromDTO(userGroup)); + return userGroupToDTO(createdGroup); + } + + public List getUserGroups() { + return userGroupProvider.getGroups().stream().map(this::userGroupToDTO).collect(Collectors.toList()); + } + + public UserGroup getUserGroup(final String identifier) { + final org.apache.nifi.registry.security.authorization.Group group = userGroupProvider.getGroup(identifier); + + if (group == null) { + LOGGER.warn("The specified user group id [{}] does not exist.", identifier); + throw new ResourceNotFoundException("The specified user group ID does not exist in this registry."); + } + + return userGroupToDTO(group); + } + + public void verifyUserGroupExists(final String identifier) { + final org.apache.nifi.registry.security.authorization.Group group = userGroupProvider.getGroup(identifier); + if (group == null) { + LOGGER.warn("The specified user group id [{}] does not exist.", identifier); + throw new ResourceNotFoundException("The specified user group ID does not exist in this registry."); + } + } + + public UserGroup updateUserGroup(final UserGroup userGroup) { + verifyUserGroupProviderIsConfigurable(); + + final org.apache.nifi.registry.security.authorization.Group updatedGroup = + configurableUserGroupProvider().updateGroup(userGroupFromDTO(userGroup)); + + if (updatedGroup == null) { + LOGGER.warn("The specified user group id [{}] does not exist.", userGroup.getIdentifier()); + throw new ResourceNotFoundException("The specified user group ID does not exist in this registry."); + } + + return userGroupToDTO(updatedGroup); + } + + public UserGroup deleteUserGroup(final String identifier) { + verifyUserGroupProviderIsConfigurable(); + + final Group group = userGroupProvider.getGroup(identifier); + if (group == null) { + LOGGER.warn("The specified user group id [{}] does not exist.", group.getIdentifier()); + throw new ResourceNotFoundException("The specified user group ID does not exist in this registry."); + } + + configurableUserGroupProvider().deleteGroup(group); + return userGroupToDTO(group); + } + + + // ---------------------- Access Policy methods ---------------------------------------- + + public AccessPolicy createAccessPolicy(final AccessPolicy accessPolicy) { + verifyAccessPolicyProviderIsConfigurable(); + + if (accessPolicy.getResource() == null) { + throw new IllegalArgumentException("Resource must be specified when creating a new access policy."); + } + + RequestAction.valueOfValue(accessPolicy.getAction()); + + final org.apache.nifi.registry.security.authorization.AccessPolicy createdAccessPolicy = + configurableAccessPolicyProvider().addAccessPolicy(accessPolicyFromDTO(accessPolicy)); + return accessPolicyToDTO(createdAccessPolicy); + } + + public AccessPolicy getAccessPolicy(final String identifier) { + final org.apache.nifi.registry.security.authorization.AccessPolicy accessPolicy = + accessPolicyProvider.getAccessPolicy(identifier); + + if (accessPolicy == null) { + LOGGER.warn("The specified access policy id [{}] does not exist.", identifier); + throw new ResourceNotFoundException("The specified policy does not exist in this registry."); + } + + return accessPolicyToDTO(accessPolicy); + } + + public AccessPolicy getAccessPolicy(final String resource, final RequestAction action) { + final org.apache.nifi.registry.security.authorization.AccessPolicy accessPolicy = + accessPolicyProvider.getAccessPolicy(resource, action); + + if (accessPolicy == null) { + throw new ResourceNotFoundException("No policy found for action='" + action + "', resource='" + resource + "'"); + } + + return accessPolicyToDTO(accessPolicy); + } + + public List getAccessPolicies() { + return accessPolicyProvider.getAccessPolicies().stream().map(this::accessPolicyToDTO).collect(Collectors.toList()); + } + + public List getAccessPolicySummaries() { + return accessPolicyProvider.getAccessPolicies().stream().map(this::accessPolicyToSummaryDTO).collect(Collectors.toList()); + } + + private List getAccessPolicySummariesForUser(String userIdentifier) { + return accessPolicyProvider.getAccessPolicies().stream() + .filter(accessPolicy -> { + if (accessPolicy.getUsers().contains(userIdentifier)) { + return true; + } + return accessPolicy.getGroups().stream().anyMatch(g -> { + final Group group = userGroupProvider.getGroup(g); + return group != null && group.getUsers().contains(userIdentifier); + }); + }) + .map(this::accessPolicyToSummaryDTO) + .collect(Collectors.toList()); + } + + private List getAccessPolicySummariesForUserGroup(String userGroupIdentifier) { + return accessPolicyProvider.getAccessPolicies().stream() + .filter(accessPolicy -> accessPolicy.getGroups().contains(userGroupIdentifier)) + .map(this::accessPolicyToSummaryDTO) + .collect(Collectors.toList()); + } + + public void verifyAccessPolicyExists(final String identifier) { + final org.apache.nifi.registry.security.authorization.AccessPolicy accessPolicy = + accessPolicyProvider.getAccessPolicy(identifier); + if (accessPolicy == null) { + LOGGER.warn("The specified access policy id [{}] does not exist.", identifier); + throw new ResourceNotFoundException("The specified policy does not exist in this registry."); + } + } + + public AccessPolicy updateAccessPolicy(final AccessPolicy accessPolicy) { + verifyAccessPolicyProviderIsConfigurable(); + + // Don't allow changing action or resource of existing policy (should only be adding/removing users/groups) + final org.apache.nifi.registry.security.authorization.AccessPolicy currentAccessPolicy = + accessPolicyProvider.getAccessPolicy(accessPolicy.getIdentifier()); + + if (currentAccessPolicy == null) { + LOGGER.warn("The specified access policy id [{}] does not exist.", accessPolicy.getIdentifier()); + throw new ResourceNotFoundException("The specified policy does not exist in this registry."); + } + + accessPolicy.setResource(currentAccessPolicy.getResource()); + accessPolicy.setAction(currentAccessPolicy.getAction().toString()); + + final org.apache.nifi.registry.security.authorization.AccessPolicy updatedAccessPolicy = + configurableAccessPolicyProvider().updateAccessPolicy(accessPolicyFromDTO(accessPolicy)); + + if (updatedAccessPolicy == null) { + LOGGER.warn("The specified access policy id [{}] does not exist.", accessPolicy.getIdentifier()); + throw new ResourceNotFoundException("The specified policy does not exist in this registry."); + } + + return accessPolicyToDTO(updatedAccessPolicy); + } + + public AccessPolicy deleteAccessPolicy(final String identifier) { + verifyAccessPolicyProviderIsConfigurable(); + + final org.apache.nifi.registry.security.authorization.AccessPolicy accessPolicy = + accessPolicyProvider.getAccessPolicy(identifier); + + if (accessPolicy == null) { + LOGGER.warn("The specified access policy id [{}] does not exist.", identifier); + throw new ResourceNotFoundException("The specified policy does not exist in this registry."); + } + + configurableAccessPolicyProvider().deleteAccessPolicy(accessPolicy); + return accessPolicyToDTO(accessPolicy); + } + + + // ---------------------- Resource Lookup methods -------------------------------------- + + public List getResources() { + final List dtoResources = + getAuthorizableResources() + .stream() + .map(AuthorizationService::resourceToDTO) + .collect(Collectors.toList()); + return dtoResources; + } + + public List getAuthorizedResources(RequestAction actionType) { + return getAuthorizedResources(actionType, null); + } + + public List getAuthorizedResources(RequestAction actionType, ResourceType resourceType) { + final List authorizedResources = + getAuthorizableResources(resourceType) + .stream() + .filter(resource -> { + String resourceId = resource.getIdentifier(); + try { + authorizableLookup + .getAuthorizableByResource(resource.getIdentifier()) + .authorize(authorizer, actionType, NiFiUserUtils.getNiFiUser()); + return true; + } catch (AccessDeniedException | UntrustedProxyException e) { + return false; + } + }) + .map(AuthorizationService::resourceToDTO) + .collect(Collectors.toList()); + + return authorizedResources; + } + + // ---------------------- Private Helper methods -------------------------------------- + + private ConfigurableUserGroupProvider configurableUserGroupProvider() { + return ((ConfigurableUserGroupProvider) userGroupProvider); + } + + private ConfigurableAccessPolicyProvider configurableAccessPolicyProvider() { + return ((ConfigurableAccessPolicyProvider) accessPolicyProvider); + } + + private void verifyUserGroupProviderIsConfigurable() { + if (!(userGroupProvider instanceof ConfigurableUserGroupProvider)) { + throw new IllegalStateException(MSG_NON_CONFIGURABLE_USERS); + } + } + + private void verifyAccessPolicyProviderIsConfigurable() { + if (!(accessPolicyProvider instanceof ConfigurableAccessPolicyProvider)) { + throw new IllegalStateException(MSG_NON_CONFIGURABLE_POLICIES); + } + } + + private ResourcePermissions getTopLevelPermissions(String tenantIdentifier) { + ResourcePermissions resourcePermissions = new ResourcePermissions(); + + final Permissions bucketsPermissions = getPermissionsForResource(tenantIdentifier, ResourceFactory.getBucketsResource()); + resourcePermissions.setBuckets(bucketsPermissions); + + final Permissions policiesPermissions = getPermissionsForResource(tenantIdentifier, ResourceFactory.getPoliciesResource()); + resourcePermissions.setPolicies(policiesPermissions); + + final Permissions tenantsPermissions = getPermissionsForResource(tenantIdentifier, ResourceFactory.getTenantsResource()); + resourcePermissions.setTenants(tenantsPermissions); + + final Permissions proxyPermissions = getPermissionsForResource(tenantIdentifier, ResourceFactory.getProxyResource()); + resourcePermissions.setProxy(proxyPermissions); + + return resourcePermissions; + } + + private Permissions getPermissionsForResource(String tenantIdentifier, org.apache.nifi.registry.security.authorization.Resource resource) { + + Permissions permissions = new Permissions(); + permissions.setCanRead(checkTenantBelongsToPolicy(tenantIdentifier, resource, RequestAction.READ)); + permissions.setCanWrite(checkTenantBelongsToPolicy(tenantIdentifier, resource, RequestAction.WRITE)); + permissions.setCanDelete(checkTenantBelongsToPolicy(tenantIdentifier, resource, RequestAction.DELETE)); + return permissions; + + } + + private boolean checkTenantBelongsToPolicy(String tenantIdentifier, org.apache.nifi.registry.security.authorization.Resource resource, RequestAction action) { + org.apache.nifi.registry.security.authorization.AccessPolicy policy = + accessPolicyProvider.getAccessPolicy(resource.getIdentifier(), action); + + if (policy == null) { + return false; + } + + boolean tenantInPolicy = policy.getUsers().contains(tenantIdentifier) || policy.getGroups().contains(tenantIdentifier); + return tenantInPolicy; + } + + private List getAuthorizableResources() { + return getAuthorizableResources(null); + } + + private List getAuthorizableResources(ResourceType includeFilter) { + + final List resources = new ArrayList<>(); + + if (includeFilter == null || includeFilter.equals(ResourceType.Policy)) { + resources.add(ResourceFactory.getPoliciesResource()); + } + if (includeFilter == null || includeFilter.equals(ResourceType.Tenant)) { + resources.add(ResourceFactory.getTenantsResource()); + } + if (includeFilter == null || includeFilter.equals(ResourceType.Proxy)) { + resources.add(ResourceFactory.getProxyResource()); + } + if (includeFilter == null || includeFilter.equals(ResourceType.Actuator)) { + resources.add(ResourceFactory.getActuatorResource()); + } + if (includeFilter == null || includeFilter.equals(ResourceType.Swagger)) { + resources.add(ResourceFactory.getSwaggerResource()); + } + if (includeFilter == null || includeFilter.equals(ResourceType.Bucket)) { + resources.add(ResourceFactory.getBucketsResource()); + // add all buckets + for (final Bucket bucket : registryService.getBuckets()) { + resources.add(ResourceFactory.getBucketResource(bucket.getIdentifier(), bucket.getName())); + } + } + + return resources; + } + + private User userToDTO( + final org.apache.nifi.registry.security.authorization.User user) { + if (user == null) { + return null; + } + String userIdentifier = user.getIdentifier(); + + Collection groupsContainingUser = userGroupProvider.getGroups().stream() + .filter(group -> group.getUsers().contains(userIdentifier)) + .map(this::tenantToDTO) + .collect(Collectors.toList()); + Collection accessPolicySummaries = getAccessPolicySummariesForUser(userIdentifier); + + User userDTO = new User(user.getIdentifier(), user.getIdentity()); + userDTO.setConfigurable(AuthorizerCapabilityDetection.isUserConfigurable(authorizer, user)); + userDTO.setResourcePermissions(getTopLevelPermissions(userDTO.getIdentifier())); + userDTO.addUserGroups(groupsContainingUser); + userDTO.addAccessPolicies(accessPolicySummaries); + return userDTO; + } + + private UserGroup userGroupToDTO( + final org.apache.nifi.registry.security.authorization.Group userGroup) { + if (userGroup == null) { + return null; + } + + Collection userTenants = userGroup.getUsers() != null + ? userGroup.getUsers().stream().map(this::tenantIdToDTO).filter(Objects::nonNull).collect(Collectors.toSet()) : null; + Collection accessPolicySummaries = getAccessPolicySummariesForUserGroup(userGroup.getIdentifier()); + + UserGroup userGroupDTO = new UserGroup(userGroup.getIdentifier(), userGroup.getName()); + userGroupDTO.setConfigurable(AuthorizerCapabilityDetection.isGroupConfigurable(authorizer, userGroup)); + userGroupDTO.setResourcePermissions(getTopLevelPermissions(userGroupDTO.getIdentifier())); + userGroupDTO.addUsers(userTenants); + userGroupDTO.addAccessPolicies(accessPolicySummaries); + return userGroupDTO; + } + + private AccessPolicy accessPolicyToDTO( + final org.apache.nifi.registry.security.authorization.AccessPolicy accessPolicy) { + if (accessPolicy == null) { + return null; + } + + Collection users = accessPolicy.getUsers() != null + ? accessPolicy.getUsers().stream().map(this::tenantIdToDTO).filter(Objects::nonNull).collect(Collectors.toList()) : null; + Collection userGroups = accessPolicy.getGroups() != null + ? accessPolicy.getGroups().stream().map(this::tenantIdToDTO).filter(Objects::nonNull).collect(Collectors.toList()) : null; + + Boolean isConfigurable = AuthorizerCapabilityDetection.isAccessPolicyConfigurable(authorizer, accessPolicy); + + return accessPolicyToDTO(accessPolicy, userGroups, users, isConfigurable); + } + + private Tenant tenantIdToDTO(String identifier) { + final org.apache.nifi.registry.security.authorization.User user = userGroupProvider.getUser(identifier); + if (user != null) { + return tenantToDTO(user); + } else { + org.apache.nifi.registry.security.authorization.Group group = userGroupProvider.getGroup(identifier); + return tenantToDTO(group); + } + } + + private AccessPolicySummary accessPolicyToSummaryDTO( + final org.apache.nifi.registry.security.authorization.AccessPolicy accessPolicy) { + if (accessPolicy == null) { + return null; + } + + Boolean isConfigurable = AuthorizerCapabilityDetection.isAccessPolicyConfigurable(authorizer, accessPolicy); + + final AccessPolicySummary accessPolicySummaryDTO = new AccessPolicySummary(); + accessPolicySummaryDTO.setIdentifier(accessPolicy.getIdentifier()); + accessPolicySummaryDTO.setAction(accessPolicy.getAction().toString()); + accessPolicySummaryDTO.setResource(accessPolicy.getResource()); + accessPolicySummaryDTO.setConfigurable(isConfigurable); + return accessPolicySummaryDTO; + } + + private Tenant tenantToDTO(org.apache.nifi.registry.security.authorization.User user) { + if (user == null) { + return null; + } + Tenant tenantDTO = new Tenant(user.getIdentifier(), user.getIdentity()); + tenantDTO.setConfigurable(AuthorizerCapabilityDetection.isUserConfigurable(authorizer, user)); + return tenantDTO; + } + + private Tenant tenantToDTO(org.apache.nifi.registry.security.authorization.Group group) { + if (group == null) { + return null; + } + Tenant tenantDTO = new Tenant(group.getIdentifier(), group.getName()); + tenantDTO.setConfigurable(AuthorizerCapabilityDetection.isGroupConfigurable(authorizer, group)); + return tenantDTO; + } + + private static Resource resourceToDTO(org.apache.nifi.registry.security.authorization.Resource resource) { + if (resource == null) { + return null; + } + Resource resourceDto = new Resource(); + resourceDto.setIdentifier(resource.getIdentifier()); + resourceDto.setName(resource.getName()); + return resourceDto; + } + + private static org.apache.nifi.registry.security.authorization.User userFromDTO( + final User userDTO) { + if (userDTO == null) { + return null; + } + return new org.apache.nifi.registry.security.authorization.User.Builder() + .identifier(userDTO.getIdentifier()) + .identity(userDTO.getIdentity()) + .build(); + } + + private static org.apache.nifi.registry.security.authorization.Group userGroupFromDTO( + final UserGroup userGroupDTO) { + if (userGroupDTO == null) { + return null; + } + org.apache.nifi.registry.security.authorization.Group.Builder groupBuilder = new org.apache.nifi.registry.security.authorization.Group.Builder() + .identifier(userGroupDTO.getIdentifier()) + .name(userGroupDTO.getIdentity()); + Set users = userGroupDTO.getUsers(); + if (users != null) { + groupBuilder.addUsers(users.stream().map(Tenant::getIdentifier).collect(Collectors.toSet())); + } + return groupBuilder.build(); + } + + private static org.apache.nifi.registry.security.authorization.AccessPolicy accessPolicyFromDTO( + final AccessPolicy accessPolicyDTO) { + org.apache.nifi.registry.security.authorization.AccessPolicy.Builder accessPolicyBuilder = + new org.apache.nifi.registry.security.authorization.AccessPolicy.Builder() + .identifier(accessPolicyDTO.getIdentifier()) + .resource(accessPolicyDTO.getResource()) + .action(RequestAction.valueOfValue(accessPolicyDTO.getAction())); + + Set dtoUsers = accessPolicyDTO.getUsers(); + if (accessPolicyDTO.getUsers() != null) { + accessPolicyBuilder.addUsers(dtoUsers.stream().map(Tenant::getIdentifier).collect(Collectors.toSet())); + } + + Set dtoUserGroups = accessPolicyDTO.getUserGroups(); + if (dtoUserGroups != null) { + accessPolicyBuilder.addGroups(dtoUserGroups.stream().map(Tenant::getIdentifier).collect(Collectors.toSet())); + } + + return accessPolicyBuilder.build(); + } + + private static AccessPolicy accessPolicyToDTO( + final org.apache.nifi.registry.security.authorization.AccessPolicy accessPolicy, + final Collection userGroups, + final Collection users, + final Boolean isConfigurable) { + + if (accessPolicy == null) { + return null; + } + + final AccessPolicy accessPolicyDTO = new AccessPolicy(); + accessPolicyDTO.setIdentifier(accessPolicy.getIdentifier()); + accessPolicyDTO.setAction(accessPolicy.getAction().toString()); + accessPolicyDTO.setResource(accessPolicy.getResource()); + accessPolicyDTO.setConfigurable(isConfigurable); + accessPolicyDTO.addUsers(users); + accessPolicyDTO.addUserGroups(userGroups); + return accessPolicyDTO; + } + + private static AccessPolicyProvider createExceptionThrowingAccessPolicyProvider() { + + return new AccessPolicyProvider() { + @Override + public Set getAccessPolicies() throws AuthorizationAccessException { + throw new IllegalStateException(MSG_NON_MANAGED_AUTHORIZER); + } + + @Override + public org.apache.nifi.registry.security.authorization.AccessPolicy getAccessPolicy(String identifier) throws AuthorizationAccessException { + throw new IllegalStateException(MSG_NON_MANAGED_AUTHORIZER); + } + + @Override + public org.apache.nifi.registry.security.authorization.AccessPolicy getAccessPolicy(String resourceIdentifier, RequestAction action) throws AuthorizationAccessException { + throw new IllegalStateException(MSG_NON_MANAGED_AUTHORIZER); + } + + @Override + public UserGroupProvider getUserGroupProvider() { + return new UserGroupProvider() { + @Override + public Set getUsers() throws AuthorizationAccessException { + throw new IllegalStateException(MSG_NON_MANAGED_AUTHORIZER); + } + + @Override + public org.apache.nifi.registry.security.authorization.User getUser(String identifier) throws AuthorizationAccessException { + throw new IllegalStateException(MSG_NON_MANAGED_AUTHORIZER); + } + + @Override + public org.apache.nifi.registry.security.authorization.User getUserByIdentity(String identity) throws AuthorizationAccessException { + throw new IllegalStateException(MSG_NON_MANAGED_AUTHORIZER); + } + + @Override + public Set getGroups() throws AuthorizationAccessException { + throw new IllegalStateException(MSG_NON_MANAGED_AUTHORIZER); + } + + @Override + public Group getGroup(String identifier) throws AuthorizationAccessException { + throw new IllegalStateException(MSG_NON_MANAGED_AUTHORIZER); + } + + @Override + public UserAndGroups getUserAndGroups(String identity) throws AuthorizationAccessException { + throw new IllegalStateException(MSG_NON_MANAGED_AUTHORIZER); + } + + @Override + public void initialize(UserGroupProviderInitializationContext initializationContext) throws SecurityProviderCreationException { + + } + + @Override + public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException { + + } + + @Override + public void preDestruction() throws SecurityProviderDestructionException { + + } + }; + } + + @Override + public void initialize(AccessPolicyProviderInitializationContext initializationContext) throws SecurityProviderCreationException { + } + + @Override + public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException { + } + + @Override + public void preDestruction() throws SecurityProviderDestructionException { + } + }; + + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/MetadataService.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/MetadataService.java new file mode 100644 index 0000000000..639a116a0b --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/MetadataService.java @@ -0,0 +1,483 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.service; + +import org.apache.nifi.registry.db.entity.BucketEntity; +import org.apache.nifi.registry.db.entity.BucketItemEntity; +import org.apache.nifi.registry.db.entity.BundleEntity; +import org.apache.nifi.registry.db.entity.BundleVersionDependencyEntity; +import org.apache.nifi.registry.db.entity.BundleVersionEntity; +import org.apache.nifi.registry.db.entity.ExtensionAdditionalDetailsEntity; +import org.apache.nifi.registry.db.entity.ExtensionEntity; +import org.apache.nifi.registry.db.entity.FlowEntity; +import org.apache.nifi.registry.db.entity.FlowSnapshotEntity; +import org.apache.nifi.registry.db.entity.TagCountEntity; +import org.apache.nifi.registry.extension.bundle.BundleFilterParams; +import org.apache.nifi.registry.extension.bundle.BundleVersionFilterParams; +import org.apache.nifi.registry.extension.component.ExtensionFilterParams; +import org.apache.nifi.registry.extension.component.manifest.ProvidedServiceAPI; + +import java.util.List; +import java.util.Set; + +/** + * A service for managing metadata about all objects stored by the registry. + * + */ +public interface MetadataService { + + /** + * Creates the given bucket. + * + * @param bucket the bucket to create + * @return the created bucket + */ + BucketEntity createBucket(BucketEntity bucket); + + /** + * Retrieves the bucket with the given id. + * + * @param bucketIdentifier the id of the bucket to retrieve + * @return the bucket with the given id, or null if it does not exist + */ + BucketEntity getBucketById(String bucketIdentifier); + + /** + * Retrieves the buckets with the given name. The name comparison must be case-insensitive. + * + * @param name the name of the bucket to retrieve + * @return the buckets with the given name, or empty list if none exist + */ + List getBucketsByName(String name); + + /** + * Updates the given bucket, only the name and description should be allowed to be updated. + * + * @param bucket the updated bucket to save + * @return the updated bucket, or null if no bucket with the given id exists + */ + BucketEntity updateBucket(BucketEntity bucket); + + /** + * Deletes the bucket, as well as any objects that reference the bucket. + * + * @param bucket the bucket to delete + */ + void deleteBucket(BucketEntity bucket); + + /** + * Retrieves all buckets with the given ids. + * + * @param bucketIds the ids of the buckets to retrieve + * @return the set of all buckets + */ + List getBuckets(Set bucketIds); + + /** + * Retrieves all buckets. + * + * @return the set of all buckets + */ + List getAllBuckets(); + + // -------------------------------------------------------------------------------------------- + + /** + * Retrieves items for the given bucket. + * + * @param bucketId the id of bucket to retrieve items for + * @return the set of items for the bucket + */ + List getBucketItems(String bucketId); + + /** + * Retrieves items for the given buckets. + * + * @param bucketIds the ids of buckets to retrieve items for + * @return the set of items for the bucket + */ + List getBucketItems(Set bucketIds); + + // -------------------------------------------------------------------------------------------- + + /** + * Creates a versioned flow in the given bucket. + * + * @param flow the versioned flow to create + * @return the created versioned flow + * @throws IllegalStateException if no bucket with the given identifier exists + */ + FlowEntity createFlow(FlowEntity flow); + + /** + * Retrieves the versioned flow with the given id and DOES NOT populate the versionCount. + * + * @param flowIdentifier the identifier of the flow to retrieve + * @return the versioned flow with the given id, or null if no flow with the given id exists + */ + FlowEntity getFlowById(String flowIdentifier); + + /** + * Retrieves the versioned flow with the given id and DOES populate the versionCount. + * + * @param flowIdentifier the identifier of the flow to retrieve + * @return the versioned flow with the given id, or null if no flow with the given id exists + */ + FlowEntity getFlowByIdWithSnapshotCounts(String flowIdentifier); + + /** + * Retrieves the versioned flows with the given name. The name comparison must be case-insensitive. + * + * @param name the name of the flow to retrieve + * @return the versioned flows with the given name, or empty list if no flows with the given name exists + */ + List getFlowsByName(String name); + + /** + * Retrieves the versioned flows with the given name in the given bucket. The name comparison must be case-insensitive. + * + * @param bucketIdentifier the identifier of the bucket + * @param name the name of the flow to retrieve + * @return the versioned flows with the given name in the given bucket, or empty list if no flows with the given name exists + */ + List getFlowsByName(String bucketIdentifier, String name); + + /** + * Retrieves the versioned flows for the given bucket. + * + * @param bucketIdentifier the bucket id to retrieve flows for + * @return the flows in the given bucket + */ + List getFlowsByBucket(String bucketIdentifier); + + /** + * Updates the given versioned flow, only the name and description should be allowed to be updated. + * + * @param flow the updated versioned flow to save + * @return the updated versioned flow + */ + FlowEntity updateFlow(FlowEntity flow); + + /** + * Deletes the flow if one exists. + * + * @param flow the flow to delete + */ + void deleteFlow(FlowEntity flow); + + // -------------------------------------------------------------------------------------------- + + /** + * Creates a versioned flow snapshot. + * + * @param flowSnapshot the snapshot to create + * @return the created snapshot + * @throws IllegalStateException if the versioned flow specified by flowSnapshot.getFlowIdentifier() does not exist + */ + FlowSnapshotEntity createFlowSnapshot(FlowSnapshotEntity flowSnapshot); + + /** + * Retrieves the snapshot for the given flow identifier and snapshot version. + * + * @param flowIdentifier the identifier of the flow the snapshot belongs to + * @param version the version of the snapshot + * @return the versioned flow snapshot for the given flow identifier and version, or null if none exists + */ + FlowSnapshotEntity getFlowSnapshot(String flowIdentifier, Integer version); + + /** + * Retrieves the snapshot with the latest version number for the given flow in the given bucket. + * + * @param flowIdentifier the id of flow to retrieve the latest snapshot for + * @return the latest snapshot for the flow, or null if one doesn't exist + */ + FlowSnapshotEntity getLatestSnapshot(String flowIdentifier); + + /** + * Retrieves the snapshots for the given flow in the given bucket. + * + * @param flowIdentifier the id of the flow + * @return the snapshots + */ + List getSnapshots(String flowIdentifier); + + /** + * Deletes the flow snapshot. + * + * @param flowSnapshot the flow snapshot to delete + */ + void deleteFlowSnapshot(FlowSnapshotEntity flowSnapshot); + + // -------------------------------------------------------------------------------------------- + + /** + * Creates the given extension bundle. + * + * @param extensionBundle the extension bundle to create + * @return the created extension bundle + */ + BundleEntity createBundle(BundleEntity extensionBundle); + + /** + * Retrieves the extension bundle with the given id. + * + * @param extensionBundleId the id of the extension bundle + * @return the extension bundle with the id, or null if one does not exist + */ + BundleEntity getBundle(String extensionBundleId); + + /** + * Retrieves the extension bundle in the given bucket with the given group and artifact id. + * + * @return the extension bundle, or null if one does not exist + */ + BundleEntity getBundle(String bucketId, String groupId, String artifactId); + + /** + * Retrieves all extension bundles in the buckets with the given bucket ids. + * + * @param bucketIds the bucket ids + * @param filterParams the optional filter params + * @return the list of all extension bundles in the given buckets + */ + List getBundles(Set bucketIds, BundleFilterParams filterParams); + + /** + * Retrieves the extension bundles for the given bucket. + * + * @param bucketId the bucket id + * @return the list of extension bundles for the bucket + */ + List getBundlesByBucket(String bucketId); + + /** + * Retrieves the extension bundles for the given bucket and group. + * + * @param bucketId the bucket id + * @param groupId the group id + * @return the list of extension bundles for the bucket and group + */ + List getBundlesByBucketAndGroup(String bucketId, String groupId); + + /** + * Deletes the given extension bundle. + * + * @param extensionBundle the extension bundle to delete + */ + void deleteBundle(BundleEntity extensionBundle); + + /** + * Deletes the extension bundle with the given id. + * + * @param extensionBundleId the id extension bundle to delete + */ + void deleteBundle(String extensionBundleId); + + // -------------------------------------------------------------------------------------------- + + /** + * Creates a version of an extension bundle. + * + * @param extensionBundleVersion the bundle version to create + * @return the created bundle version + */ + BundleVersionEntity createBundleVersion(BundleVersionEntity extensionBundleVersion); + + /** + * Retrieves the extension bundle version for the given bundle id and version. + * + * @param extensionBundleId the id of the extension bundle + * @param version the version of the extension bundle + * @return the extension bundle version, or null if does not exist + */ + BundleVersionEntity getBundleVersion(String extensionBundleId, String version); + + /** + * Retrieves the extension bundle version by bucket, group, artifact, version. + * + * @param bucketId the bucket id + * @param groupId the group id + * @param artifactId the artifact id + * @param version the version + * @return the extension bundle version, or null if does not exist + */ + BundleVersionEntity getBundleVersion(String bucketId, String groupId, String artifactId, String version); + + /** + * Retrieves the extension bundle versions in the given buckets, matching the optional filter parameters. + * + * @param bucketIdentifiers the bucket identifiers + * @param filterParams the optional filter params + * @return the extension bundle versions + */ + List getBundleVersions(Set bucketIdentifiers, BundleVersionFilterParams filterParams); + + /** + * Retrieves the extension bundle versions for the given extension bundle id. + * + * @param extensionBundleId the extension bundle id + * @return the list of extension bundle versions + */ + List getBundleVersions(String extensionBundleId); + + /** + * Retrieves the extension bundle version with the given group id and artifact id in the given bucket. + * + * @param bucketId the bucket id + * @param groupId the group id + * @param artifactId the artifact id + * @return the list of extension bundles + */ + List getBundleVersions(String bucketId, String groupId, String artifactId); + + /** + * Retrieves the extension bundle versions with the given group id, artifact id, and version across all buckets. + * + * @param groupId the group id + * @param artifactId the artifact id + * @param version the versions + * @return all bundle versions for the group id, artifact id, and version + */ + List getBundleVersionsGlobal(String groupId, String artifactId, String version); + + /** + * Deletes the extension bundle version. + * + * @param extensionBundleVersion the extension bundle version to delete + */ + void deleteBundleVersion(BundleVersionEntity extensionBundleVersion); + + /** + * Deletes the extension bundle version. + * + * @param extensionBundleVersionId the id of the extension bundle version + */ + void deleteBundleVersion(String extensionBundleVersionId); + + // -------------------------------------------------------------------------------------------- + + /** + * Creates the given extension bundle version dependency. + * + * @param dependencyEntity the dependency entity + * @return the created dependency + */ + BundleVersionDependencyEntity createDependency(BundleVersionDependencyEntity dependencyEntity); + + /** + * Retrieves the bundle dependencies for the given bundle version. + * + * @param extensionBundleVersionId the id of the extension bundle version + * @return the list of dependencies + */ + List getDependenciesForBundleVersion(String extensionBundleVersionId); + + // -------------------------------------------------------------------------------------------- + + /** + * Creates the given extension. + * + * @param extension the extension to create + * @return the created extension + */ + ExtensionEntity createExtension(ExtensionEntity extension); + + /** + * Retrieves the extension with the given id. + * + * @param id the id of the extension + * @return the extension with the id, or null if one does not exist + */ + ExtensionEntity getExtensionById(String id); + + /** + * Retrieves the extension with the given name in the given bundle version. + * + * @param bundleVersionId the bundle version id + * @param name the name + * @return the extension + */ + ExtensionEntity getExtensionByName(String bundleVersionId, String name); + + /** + * Retrieves the additional details documentation for the given extension. + * + * @param bundleVersionId the bundle version id + * @param name the name of the extension + * @return the additional details content, or an empty optional + */ + ExtensionAdditionalDetailsEntity getExtensionAdditionalDetails(String bundleVersionId, String name); + + /** + * Retrieves all extensions in the given buckets. + * + * @param bucketIdentifiers the bucket identifiers to retrieve extensions from + * @param filterParams the filter params + * @return the list of all extensions in the given buckets + */ + List getExtensions(Set bucketIdentifiers, ExtensionFilterParams filterParams); + + /** + * Retrieves the extensions in the given buckets that provide the given service API. + * + * @param bucketIdentifiers the identifiers of the buckets + * @param providedServiceAPI the provided service API + * @return the extensions that provided the service API + */ + List getExtensionsByProvidedServiceApi(Set bucketIdentifiers, ProvidedServiceAPI providedServiceAPI); + + /** + * Retrieves the extensions for the given extension bundle version. + * + * @param extensionBundleVersionId the id of the extension bundle version + * @return the extensions in the given bundle + */ + List getExtensionsByBundleVersionId(String extensionBundleVersionId); + + /** + * Retrieves the set of all extension tags. + * + * @return the set of all extension tags + */ + List getAllExtensionTags(); + + /** + * Deletes the given extension. + * + * @param extension the extension to delete + */ + void deleteExtension(ExtensionEntity extension); + + + // -------------------------------------------------------------------------------------------- + + /** + * @return the set of field names for Buckets + */ + Set getBucketFields(); + + /** + * @return the set of field names for BucketItems + */ + Set getBucketItemFields(); + + /** + * @return the set of field names for Flows + */ + Set getFlowFields(); + +} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/QueryParameters.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/QueryParameters.java new file mode 100644 index 0000000000..99ef9dd3b8 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/QueryParameters.java @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.service; + +import org.apache.nifi.registry.params.SortOrder; +import org.apache.nifi.registry.params.SortParameter; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * Parameters to be passed into service layer for methods that require sorting and paging. + */ +public class QueryParameters { + + public static final QueryParameters EMPTY_PARAMETERS = new QueryParameters.Builder().build(); + + private final Integer pageNum; + + private final Integer numRows; + + private final List sortParameters; + + private QueryParameters(final Builder builder) { + this.pageNum = builder.pageNum; + this.numRows = builder.numRows; + this.sortParameters = Collections.unmodifiableList(new ArrayList<>(builder.sortParameters)); + + if (this.pageNum != null && this.numRows != null) { + if (this.pageNum < 0) { + throw new IllegalStateException("Offset cannot be negative"); + } + + if (this.numRows < 0) { + throw new IllegalStateException("Number of rows cannot be negative"); + } + } + } + + public Integer getPageNum() { + return pageNum; + } + + public Integer getNumRows() { + return numRows; + } + + public List getSortParameters() { + return sortParameters; + } + + /** + * Builder for QueryParameters. + */ + public static class Builder { + + private Integer pageNum; + private Integer numRows; + private List sortParameters = new ArrayList<>(); + + public Builder pageNum(Integer pageNum) { + this.pageNum = pageNum; + return this; + } + + public Builder numRows(Integer numRows) { + this.numRows = numRows; + return this; + } + + public Builder addSort(final SortParameter sort) { + this.sortParameters.add(sort); + return this; + } + + public Builder addSort(final String fieldName, final SortOrder order) { + this.sortParameters.add(new SortParameter(fieldName, order)); + return this; + } + + public Builder addSorts(final Collection sorts) { + if (sorts != null) { + this.sortParameters.addAll(sorts); + } + return this; + } + + public Builder clearSorts() { + this.sortParameters.clear(); + return this; + } + + public QueryParameters build() { + return new QueryParameters(this); + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/RegistryService.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/RegistryService.java new file mode 100644 index 0000000000..b50196deb1 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/RegistryService.java @@ -0,0 +1,984 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.service; + +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.bucket.BucketItem; +import org.apache.nifi.registry.db.entity.BucketEntity; +import org.apache.nifi.registry.db.entity.BucketItemEntity; +import org.apache.nifi.registry.db.entity.BundleEntity; +import org.apache.nifi.registry.db.entity.FlowEntity; +import org.apache.nifi.registry.db.entity.FlowSnapshotEntity; +import org.apache.nifi.registry.diff.ComponentDifferenceGroup; +import org.apache.nifi.registry.diff.VersionedFlowDifference; +import org.apache.nifi.registry.exception.ResourceNotFoundException; +import org.apache.nifi.registry.extension.BundleCoordinate; +import org.apache.nifi.registry.extension.BundlePersistenceProvider; +import org.apache.nifi.registry.flow.FlowPersistenceProvider; +import org.apache.nifi.registry.flow.FlowSnapshotContext; +import org.apache.nifi.registry.flow.VersionedComponent; +import org.apache.nifi.registry.flow.VersionedFlow; +import org.apache.nifi.registry.flow.VersionedFlowSnapshot; +import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata; +import org.apache.nifi.registry.flow.VersionedProcessGroup; +import org.apache.nifi.registry.flow.diff.ComparableDataFlow; +import org.apache.nifi.registry.flow.diff.ConciseEvolvingDifferenceDescriptor; +import org.apache.nifi.registry.flow.diff.FlowComparator; +import org.apache.nifi.registry.flow.diff.FlowComparison; +import org.apache.nifi.registry.flow.diff.FlowDifference; +import org.apache.nifi.registry.flow.diff.StandardComparableDataFlow; +import org.apache.nifi.registry.flow.diff.StandardFlowComparator; +import org.apache.nifi.registry.provider.extension.StandardBundleCoordinate; +import org.apache.nifi.registry.provider.flow.StandardFlowSnapshotContext; +import org.apache.nifi.registry.serialization.FlowContent; +import org.apache.nifi.registry.serialization.FlowContentSerializer; +import org.apache.nifi.registry.service.alias.RegistryUrlAliasService; +import org.apache.nifi.registry.service.mapper.BucketMappings; +import org.apache.nifi.registry.service.mapper.ExtensionMappings; +import org.apache.nifi.registry.service.mapper.FlowMappings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.Validator; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.stream.Collectors; + +/** + * Main service for all back-end operations on buckets and flows. + */ +@Service +public class RegistryService { + + private static final Logger LOGGER = LoggerFactory.getLogger(RegistryService.class); + + private final MetadataService metadataService; + private final FlowPersistenceProvider flowPersistenceProvider; + private final BundlePersistenceProvider bundlePersistenceProvider; + private final FlowContentSerializer flowContentSerializer; + private final Validator validator; + private final RegistryUrlAliasService registryUrlAliasService; + + @Autowired + public RegistryService(final MetadataService metadataService, + final FlowPersistenceProvider flowPersistenceProvider, + final BundlePersistenceProvider bundlePersistenceProvider, + final FlowContentSerializer flowContentSerializer, + final Validator validator, + final RegistryUrlAliasService registryUrlAliasService) { + this.metadataService = Validate.notNull(metadataService); + this.flowPersistenceProvider = Validate.notNull(flowPersistenceProvider); + this.bundlePersistenceProvider = Validate.notNull(bundlePersistenceProvider); + this.flowContentSerializer = Validate.notNull(flowContentSerializer); + this.validator = Validate.notNull(validator); + this.registryUrlAliasService = Validate.notNull(registryUrlAliasService); + } + + private void validate(T t, String invalidMessage) { + final Set> violations = validator.validate(t); + if (violations.size() > 0) { + throw new ConstraintViolationException(invalidMessage, violations); + } + } + + // ---------------------- Bucket methods --------------------------------------------- + + public Bucket createBucket(final Bucket bucket) { + if (bucket == null) { + throw new IllegalArgumentException("Bucket cannot be null"); + } + + // set the created time, and clear out the flows since its read-only + bucket.setCreatedTimestamp(System.currentTimeMillis()); + + if (bucket.isAllowBundleRedeploy() == null) { + bucket.setAllowBundleRedeploy(false); + } + + if (bucket.isAllowPublicRead() == null) { + bucket.setAllowPublicRead(false); + } + + validate(bucket, "Cannot create Bucket"); + + final List bucketsWithSameName = metadataService.getBucketsByName(bucket.getName()); + if (bucketsWithSameName.size() > 0) { + throw new IllegalStateException("A bucket with the same name already exists"); + } + + final BucketEntity createdBucket = metadataService.createBucket(BucketMappings.map(bucket)); + return BucketMappings.map(createdBucket); + } + + public Bucket getBucket(final String bucketIdentifier) { + if (bucketIdentifier == null) { + throw new IllegalArgumentException("Bucket identifier cannot be null"); + } + + final BucketEntity bucket = metadataService.getBucketById(bucketIdentifier); + if (bucket == null) { + LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier); + throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry."); + } + + return BucketMappings.map(bucket); + } + + public void verifyBucketExists(final String bucketIdentifier) { + if (bucketIdentifier == null) { + throw new IllegalArgumentException("Bucket identifier cannot be null"); + } + + final BucketEntity bucket = metadataService.getBucketById(bucketIdentifier); + if (bucket == null) { + LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier); + throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry."); + } + } + + public Bucket getBucketByName(final String bucketName) { + if (bucketName == null) { + throw new IllegalArgumentException("Bucket name cannot be null"); + } + + final List buckets = metadataService.getBucketsByName(bucketName); + if (buckets.isEmpty()) { + LOGGER.warn("The specified bucket name [{}] does not exist.", bucketName); + throw new ResourceNotFoundException("The specified bucket name does not exist in this registry."); + } + + return BucketMappings.map(buckets.get(0)); + } + + public List getBuckets() { + final List buckets = metadataService.getAllBuckets(); + return buckets.stream().map(b -> BucketMappings.map(b)).collect(Collectors.toList()); + } + + public List getBuckets(final Set bucketIds) { + final List buckets = metadataService.getBuckets(bucketIds); + return buckets.stream().map(b -> BucketMappings.map(b)).collect(Collectors.toList()); + } + + public Bucket updateBucket(final Bucket bucket) { + if (bucket == null) { + throw new IllegalArgumentException("Bucket cannot be null"); + } + + if (bucket.getIdentifier() == null) { + throw new IllegalArgumentException("Bucket identifier cannot be null"); + } + + if (bucket.getName() != null && StringUtils.isBlank(bucket.getName())) { + throw new IllegalArgumentException("Bucket name cannot be blank"); + } + + // ensure a bucket with the given id exists + final BucketEntity existingBucketById = metadataService.getBucketById(bucket.getIdentifier()); + if (existingBucketById == null) { + LOGGER.warn("The specified bucket id [{}] does not exist.", bucket.getIdentifier()); + throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry."); + } + + // ensure a different bucket with the same name does not exist + // since we're allowing partial updates here, only check this if a non-null name is provided + if (StringUtils.isNotBlank(bucket.getName())) { + final List bucketsWithSameName = metadataService.getBucketsByName(bucket.getName()); + if (bucketsWithSameName != null) { + for (final BucketEntity bucketWithSameName : bucketsWithSameName) { + if (!bucketWithSameName.getId().equals(existingBucketById.getId())){ + throw new IllegalStateException("A bucket with the same name already exists - " + bucket.getName()); + } + } + } + } + + // transfer over the new values to the existing bucket + if (StringUtils.isNotBlank(bucket.getName())) { + existingBucketById.setName(bucket.getName()); + } + + if (bucket.getDescription() != null) { + existingBucketById.setDescription(bucket.getDescription()); + } + + if (bucket.isAllowBundleRedeploy() != null) { + existingBucketById.setAllowExtensionBundleRedeploy(bucket.isAllowBundleRedeploy()); + } + + if (bucket.isAllowPublicRead() != null) { + existingBucketById.setAllowPublicRead(bucket.isAllowPublicRead()); + } + + // perform the actual update + final BucketEntity updatedBucket = metadataService.updateBucket(existingBucketById); + return BucketMappings.map(updatedBucket); + } + + public Bucket deleteBucket(final String bucketIdentifier) { + if (bucketIdentifier == null) { + throw new IllegalArgumentException("Bucket identifier cannot be null"); + } + + // ensure the bucket exists + final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier); + if (existingBucket == null) { + LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier); + throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry."); + } + + // for each flow in the bucket, delete all snapshots from the flow persistence provider + for (final FlowEntity flowEntity : metadataService.getFlowsByBucket(existingBucket.getId())) { + flowPersistenceProvider.deleteAllFlowContent(bucketIdentifier, flowEntity.getId()); + } + + // for each bundle in the bucket, delete all versions from the bundle persistence provider + for (final BundleEntity bundleEntity : metadataService.getBundlesByBucket(existingBucket.getId())) { + final BundleCoordinate bundleCoordinate = new StandardBundleCoordinate.Builder() + .bucketId(bundleEntity.getBucketId()) + .groupId(bundleEntity.getGroupId()) + .artifactId(bundleEntity.getArtifactId()) + .build(); + bundlePersistenceProvider.deleteAllBundleVersions(bundleCoordinate); + } + + // now delete the bucket from the metadata provider, which deletes all flows referencing it + metadataService.deleteBucket(existingBucket); + + return BucketMappings.map(existingBucket); + } + + // ---------------------- BucketItem methods --------------------------------------------- + + public List getBucketItems(final String bucketIdentifier) { + if (bucketIdentifier == null) { + throw new IllegalArgumentException("Bucket identifier cannot be null"); + } + + final BucketEntity bucket = metadataService.getBucketById(bucketIdentifier); + if (bucket == null) { + LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier); + throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry."); + } + + final List bucketItems = new ArrayList<>(); + metadataService.getBucketItems(bucket.getId()).stream().forEach(b -> addBucketItem(bucketItems, b)); + return bucketItems; + } + + public List getBucketItems(final Set bucketIdentifiers) { + if (bucketIdentifiers == null || bucketIdentifiers.isEmpty()) { + throw new IllegalArgumentException("Bucket identifiers cannot be null or empty"); + } + + final List bucketItems = new ArrayList<>(); + metadataService.getBucketItems(bucketIdentifiers).stream().forEach(b -> addBucketItem(bucketItems, b)); + return bucketItems; + } + + private void addBucketItem(final List bucketItems, final BucketItemEntity itemEntity) { + // Currently we don't populate the bucket name for items so we pass in null in the map methods + if (itemEntity instanceof FlowEntity) { + final FlowEntity flowEntity = (FlowEntity) itemEntity; + bucketItems.add(FlowMappings.map(null, flowEntity)); + } else if (itemEntity instanceof BundleEntity) { + final BundleEntity bundleEntity = (BundleEntity) itemEntity; + bucketItems.add(ExtensionMappings.map(null, bundleEntity)); + } else { + LOGGER.error("Unknown type of BucketItemEntity: " + itemEntity.getClass().getCanonicalName()); + } + } + + // ---------------------- VersionedFlow methods --------------------------------------------- + + public VersionedFlow createFlow(final String bucketIdentifier, final VersionedFlow versionedFlow) { + if (StringUtils.isBlank(bucketIdentifier)) { + throw new IllegalArgumentException("Bucket identifier cannot be null or blank"); + } + + if (versionedFlow == null) { + throw new IllegalArgumentException("Versioned flow cannot be null"); + } + + if (versionedFlow.getBucketIdentifier() != null && !bucketIdentifier.equals(versionedFlow.getBucketIdentifier())) { + throw new IllegalArgumentException("Bucket identifiers must match"); + } + + if (versionedFlow.getBucketIdentifier() == null) { + versionedFlow.setBucketIdentifier(bucketIdentifier); + } + + final long timestamp = System.currentTimeMillis(); + versionedFlow.setCreatedTimestamp(timestamp); + versionedFlow.setModifiedTimestamp(timestamp); + + validate(versionedFlow, "Cannot create versioned flow"); + + // ensure the bucket exists + final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier); + if (existingBucket == null) { + LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier); + throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry."); + } + + // ensure another flow with the same name doesn't exist + final List flowsWithSameName = metadataService.getFlowsByName(existingBucket.getId(), versionedFlow.getName()); + if (flowsWithSameName != null && flowsWithSameName.size() > 0) { + throw new IllegalStateException("A versioned flow with the same name already exists in the selected bucket"); + } + + // convert from dto to entity and set the bucket relationship + final FlowEntity flowEntity = FlowMappings.map(versionedFlow); + flowEntity.setBucketId(existingBucket.getId()); + + // persist the flow and return the created entity + final FlowEntity createdFlow = metadataService.createFlow(flowEntity); + return FlowMappings.map(existingBucket, createdFlow); + } + + public VersionedFlow getFlow(final String bucketIdentifier, final String flowIdentifier) { + if (StringUtils.isBlank(bucketIdentifier)) { + throw new IllegalArgumentException("Bucket identifier cannot be null or blank"); + } + + if (StringUtils.isBlank(flowIdentifier)) { + throw new IllegalArgumentException("Versioned flow identifier cannot be null or blank"); + } + + // ensure the bucket exists + final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier); + if (existingBucket == null) { + LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier); + throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry."); + } + + final FlowEntity existingFlow = metadataService.getFlowByIdWithSnapshotCounts(flowIdentifier); + if (existingFlow == null) { + LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier); + throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket."); + } + + if (!existingBucket.getId().equals(existingFlow.getBucketId())) { + throw new IllegalStateException("The requested flow is not located in the given bucket"); + } + + return FlowMappings.map(existingBucket, existingFlow); + } + + public VersionedFlow getFlow(final String flowIdentifier) { + if (StringUtils.isBlank(flowIdentifier)) { + throw new IllegalArgumentException("Versioned flow identifier cannot be null or blank"); + } + + final FlowEntity existingFlow = metadataService.getFlowByIdWithSnapshotCounts(flowIdentifier); + if (existingFlow == null) { + LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier); + throw new ResourceNotFoundException("The specified flow ID does not exist."); + } + + final BucketEntity existingBucket = metadataService.getBucketById(existingFlow.getBucketId()); + return FlowMappings.map(existingBucket, existingFlow); + } + + public void verifyFlowExists(final String flowIdentifier) { + if (StringUtils.isBlank(flowIdentifier)) { + throw new IllegalArgumentException("Versioned flow identifier cannot be null or blank"); + } + + final FlowEntity existingFlow = metadataService.getFlowById(flowIdentifier); + if (existingFlow == null) { + LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier); + throw new ResourceNotFoundException("The specified flow ID does not exist."); + } + } + + public List getFlows(final String bucketId) { + if (StringUtils.isBlank(bucketId)) { + throw new IllegalArgumentException("Bucket identifier cannot be null"); + } + + final BucketEntity existingBucket = metadataService.getBucketById(bucketId); + if (existingBucket == null) { + LOGGER.warn("The specified bucket id [{}] does not exist.", bucketId); + throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry."); + } + + // return non-verbose set of flows for the given bucket + final List flows = metadataService.getFlowsByBucket(existingBucket.getId()); + return flows.stream().map(f -> FlowMappings.map(existingBucket, f)).collect(Collectors.toList()); + } + + public VersionedFlow updateFlow(final VersionedFlow versionedFlow) { + if (versionedFlow == null) { + throw new IllegalArgumentException("Versioned flow cannot be null"); + } + + if (StringUtils.isBlank(versionedFlow.getIdentifier())) { + throw new IllegalArgumentException("Versioned flow identifier cannot be null or blank"); + } + + if (StringUtils.isBlank(versionedFlow.getBucketIdentifier())) { + throw new IllegalArgumentException("Versioned flow bucket identifier cannot be null or blank"); + } + + if (versionedFlow.getName() != null && StringUtils.isBlank(versionedFlow.getName())) { + throw new IllegalArgumentException("Versioned flow name cannot be blank"); + } + + // ensure the bucket exists + final BucketEntity existingBucket = metadataService.getBucketById(versionedFlow.getBucketIdentifier()); + if (existingBucket == null) { + LOGGER.warn("The specified bucket id [{}] does not exist.", versionedFlow.getBucketIdentifier()); + throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry."); + } + + final FlowEntity existingFlow = metadataService.getFlowByIdWithSnapshotCounts(versionedFlow.getIdentifier()); + if (existingFlow == null) { + LOGGER.warn("The specified flow id [{}] does not exist.", versionedFlow.getIdentifier()); + throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket."); + } + + if (!existingBucket.getId().equals(existingFlow.getBucketId())) { + throw new IllegalStateException("The requested flow is not located in the given bucket"); + } + + // ensure a different flow with the same name does not exist + // since we're allowing partial updates here, only check this if a non-null name is provided + if (StringUtils.isNotBlank(versionedFlow.getName())) { + final List flowsWithSameName = metadataService.getFlowsByName(existingBucket.getId(), versionedFlow.getName()); + if (flowsWithSameName != null) { + for (final FlowEntity flowWithSameName : flowsWithSameName) { + if(!flowWithSameName.getId().equals(existingFlow.getId())) { + throw new IllegalStateException("A versioned flow with the same name already exists in the selected bucket"); + } + } + } + } + + // transfer over the new values to the existing flow + if (StringUtils.isNotBlank(versionedFlow.getName())) { + existingFlow.setName(versionedFlow.getName()); + } + + if (versionedFlow.getDescription() != null) { + existingFlow.setDescription(versionedFlow.getDescription()); + } + + // perform the actual update + final FlowEntity updatedFlow = metadataService.updateFlow(existingFlow); + return FlowMappings.map(existingBucket, updatedFlow); + } + + public VersionedFlow deleteFlow(final String bucketIdentifier, final String flowIdentifier) { + if (StringUtils.isBlank(bucketIdentifier)) { + throw new IllegalArgumentException("Bucket identifier cannot be null or blank"); + } + if (StringUtils.isBlank(flowIdentifier)) { + throw new IllegalArgumentException("Flow identifier cannot be null or blank"); + } + + // ensure the bucket exists + final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier); + if (existingBucket == null) { + LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier); + throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry."); + } + + // ensure the flow exists + final FlowEntity existingFlow = metadataService.getFlowById(flowIdentifier); + if (existingFlow == null) { + LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier); + throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket."); + } + + if (!existingBucket.getId().equals(existingFlow.getBucketId())) { + throw new IllegalStateException("The requested flow is not located in the given bucket"); + } + + // delete all snapshots from the flow persistence provider + flowPersistenceProvider.deleteAllFlowContent(existingFlow.getBucketId(), existingFlow.getId()); + + // now delete the flow from the metadata provider + metadataService.deleteFlow(existingFlow); + + return FlowMappings.map(existingBucket, existingFlow); + } + + // ---------------------- VersionedFlowSnapshot methods --------------------------------------------- + + public VersionedFlowSnapshot createFlowSnapshot(final VersionedFlowSnapshot flowSnapshot) { + if (flowSnapshot == null) { + throw new IllegalArgumentException("Versioned flow snapshot cannot be null"); + } + + // validation will ensure that the metadata and contents are not null + if (flowSnapshot.getSnapshotMetadata() != null) { + flowSnapshot.getSnapshotMetadata().setTimestamp(System.currentTimeMillis()); + } + + // these fields aren't used for creation + flowSnapshot.setFlow(null); + flowSnapshot.setBucket(null); + + validate(flowSnapshot, "Cannot create versioned flow snapshot"); + + final VersionedFlowSnapshotMetadata snapshotMetadata = flowSnapshot.getSnapshotMetadata(); + + // ensure the bucket exists + final BucketEntity existingBucket = metadataService.getBucketById(snapshotMetadata.getBucketIdentifier()); + if (existingBucket == null) { + LOGGER.warn("The specified bucket id [{}] does not exist.", snapshotMetadata.getBucketIdentifier()); + throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry."); + } + + // ensure the flow exists + final FlowEntity existingFlow = metadataService.getFlowById(snapshotMetadata.getFlowIdentifier()); + if (existingFlow == null) { + LOGGER.warn("The specified flow id [{}] does not exist.", snapshotMetadata.getFlowIdentifier()); + throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket."); + } + + if (!existingBucket.getId().equals(existingFlow.getBucketId())) { + throw new IllegalStateException("The requested flow is not located in the given bucket"); + } + + if (snapshotMetadata.getVersion() == 0) { + throw new IllegalArgumentException("Version must be greater than zero, or use -1 to indicate latest version"); + } + + // convert the set of FlowSnapshotEntity to set of VersionedFlowSnapshotMetadata + final SortedSet sortedSnapshots = new TreeSet<>(); + final List existingFlowSnapshots = metadataService.getSnapshots(existingFlow.getId()); + if (existingFlowSnapshots != null) { + existingFlowSnapshots.stream().forEach(s -> sortedSnapshots.add(FlowMappings.map(existingBucket, s))); + } + + // if we already have snapshots we need to verify the new one has the correct version + if (sortedSnapshots.size() > 0) { + final VersionedFlowSnapshotMetadata lastSnapshot = sortedSnapshots.last(); + + // if we have existing versions and a client sends -1, then make this the latest version + if (snapshotMetadata.getVersion() == -1) { + snapshotMetadata.setVersion(lastSnapshot.getVersion() + 1); + } else if (snapshotMetadata.getVersion() <= lastSnapshot.getVersion()) { + throw new IllegalStateException("A Versioned flow snapshot with the same version already exists: " + snapshotMetadata.getVersion()); + } else if (snapshotMetadata.getVersion() > (lastSnapshot.getVersion() + 1)) { + throw new IllegalStateException("Version must be a one-up number, last version was " + lastSnapshot.getVersion() + + " and version for this snapshot was " + snapshotMetadata.getVersion()); + } + + } else if (snapshotMetadata.getVersion() == -1) { + // if we have no existing versions and a client sends -1, then this is the first version + snapshotMetadata.setVersion(1); + } else if (snapshotMetadata.getVersion() != 1) { + throw new IllegalStateException("Version of first snapshot must be 1"); + } + + // serialize the snapshot + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + registryUrlAliasService.setInternal(flowSnapshot.getFlowContents()); + + final FlowContent flowContent = new FlowContent(); + flowContent.setFlowSnapshot(flowSnapshot); + + // temporarily remove the metadata so it isn't serialized, but then put it back for returning the response + flowSnapshot.setSnapshotMetadata(null); + flowContentSerializer.serializeFlowContent(flowContent, out); + flowSnapshot.setSnapshotMetadata(snapshotMetadata); + + // save the serialized snapshot to the persistence provider + final Bucket bucket = BucketMappings.map(existingBucket); + final VersionedFlow versionedFlow = FlowMappings.map(existingBucket, existingFlow); + final FlowSnapshotContext context = new StandardFlowSnapshotContext.Builder(bucket, versionedFlow, snapshotMetadata).build(); + flowPersistenceProvider.saveFlowContent(context, out.toByteArray()); + + // create snapshot in the metadata provider + metadataService.createFlowSnapshot(FlowMappings.map(snapshotMetadata)); + + // update the modified date on the flow + metadataService.updateFlow(existingFlow); + + // get the updated flow, we need to use "with counts" here so we can return this is a part of the response + final FlowEntity updatedFlow = metadataService.getFlowByIdWithSnapshotCounts(snapshotMetadata.getFlowIdentifier()); + if (updatedFlow == null) { + throw new ResourceNotFoundException("Versioned flow does not exist for identifier " + snapshotMetadata.getFlowIdentifier()); + } + final VersionedFlow updatedVersionedFlow = FlowMappings.map(existingBucket, updatedFlow); + + flowSnapshot.setBucket(bucket); + flowSnapshot.setFlow(updatedVersionedFlow); + registryUrlAliasService.setExternal(flowSnapshot.getFlowContents()); + return flowSnapshot; + } + + public VersionedFlowSnapshot getFlowSnapshot(final String bucketIdentifier, final String flowIdentifier, final Integer version) { + if (StringUtils.isBlank(bucketIdentifier)) { + throw new IllegalArgumentException("Bucket identifier cannot be null or blank"); + } + + if (StringUtils.isBlank(flowIdentifier)) { + throw new IllegalArgumentException("Flow identifier cannot be null or blank"); + } + + if (version == null) { + throw new IllegalArgumentException("Version cannot be null or blank"); + } + + final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier); + if (existingBucket == null) { + LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier); + throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry."); + } + + // we need to populate the version count here so we have to do this retrieval instead of snapshotEntity.getFlow() + final FlowEntity flowEntityWithCount = metadataService.getFlowByIdWithSnapshotCounts(flowIdentifier); + if (flowEntityWithCount == null) { + LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier); + throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket."); + } + + if (!existingBucket.getId().equals(flowEntityWithCount.getBucketId())) { + throw new IllegalStateException("The requested flow is not located in the given bucket"); + } + + return getVersionedFlowSnapshot(existingBucket, flowEntityWithCount, version); + } + + private VersionedFlowSnapshot getVersionedFlowSnapshot(final BucketEntity bucketEntity, final FlowEntity flowEntity, final Integer version) { + // ensure the snapshot exists + final FlowSnapshotEntity snapshotEntity = metadataService.getFlowSnapshot(flowEntity.getId(), version); + if (snapshotEntity == null) { + LOGGER.warn("The specified flow snapshot id [{}] does not exist for version [{}].", flowEntity.getId(), version); + throw new ResourceNotFoundException("The specified versioned flow snapshot does not exist for this flow."); + } + + // get the serialized bytes of the snapshot + final byte[] serializedSnapshot = flowPersistenceProvider.getFlowContent(bucketEntity.getId(), flowEntity.getId(), version); + + if (serializedSnapshot == null || serializedSnapshot.length == 0) { + throw new IllegalStateException("No serialized content found for snapshot with flow identifier " + + flowEntity.getId() + " and version " + version); + } + + // deserialize the content + final InputStream input = new ByteArrayInputStream(serializedSnapshot); + final VersionedFlowSnapshot snapshot = deserializeFlowContent(input); + + // map entities to data model + final Bucket bucket = BucketMappings.map(bucketEntity); + final VersionedFlow versionedFlow = FlowMappings.map(bucketEntity, flowEntity); + final VersionedFlowSnapshotMetadata snapshotMetadata = FlowMappings.map(bucketEntity, snapshotEntity); + + // create the snapshot to return + registryUrlAliasService.setExternal(snapshot.getFlowContents()); + snapshot.setSnapshotMetadata(snapshotMetadata); + snapshot.setFlow(versionedFlow); + snapshot.setBucket(bucket); + return snapshot; + } + + private VersionedFlowSnapshot deserializeFlowContent(final InputStream input) { + // attempt to read the version header from the serialized content + final int dataModelVersion = flowContentSerializer.readDataModelVersion(input); + + // determine how to do deserialize based on the data model version + if (flowContentSerializer.isProcessGroupVersion(dataModelVersion)) { + final VersionedProcessGroup processGroup = flowContentSerializer.deserializeProcessGroup(dataModelVersion, input); + final VersionedFlowSnapshot snapshot = new VersionedFlowSnapshot(); + snapshot.setFlowContents(processGroup); + return snapshot; + } else { + final FlowContent flowContent = flowContentSerializer.deserializeFlowContent(dataModelVersion, input); + return flowContent.getFlowSnapshot(); + } + } + + /** + * Returns all versions of a flow, sorted newest to oldest. + * + * @param bucketIdentifier the id of the bucket to search for the flowIdentifier + * @param flowIdentifier the id of the flow to retrieve from the specified bucket + * @return all versions of the specified flow, sorted newest to oldest + */ + public SortedSet getFlowSnapshots(final String bucketIdentifier, final String flowIdentifier) { + if (StringUtils.isBlank(bucketIdentifier)) { + throw new IllegalArgumentException("Bucket identifier cannot be null or blank"); + } + + if (StringUtils.isBlank(flowIdentifier)) { + throw new IllegalArgumentException("Flow identifier cannot be null or blank"); + } + + // ensure the bucket exists + final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier); + if (existingBucket == null) { + LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier); + throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry."); + } + + // ensure the flow exists + final FlowEntity existingFlow = metadataService.getFlowById(flowIdentifier); + if (existingFlow == null) { + LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier); + throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket."); + } + + if (!existingBucket.getId().equals(existingFlow.getBucketId())) { + throw new IllegalStateException("The requested flow is not located in the given bucket"); + } + + // convert the set of FlowSnapshotEntity to set of VersionedFlowSnapshotMetadata, ordered by version descending + final SortedSet sortedSnapshots = new TreeSet<>(Collections.reverseOrder()); + final List existingFlowSnapshots = metadataService.getSnapshots(existingFlow.getId()); + if (existingFlowSnapshots != null) { + existingFlowSnapshots.stream().forEach(s -> sortedSnapshots.add(FlowMappings.map(existingBucket, s))); + } + + return sortedSnapshots; + } + + public VersionedFlowSnapshotMetadata getLatestFlowSnapshotMetadata(final String bucketIdentifier, final String flowIdentifier) { + if (StringUtils.isBlank(bucketIdentifier)) { + throw new IllegalArgumentException("Bucket identifier cannot be null or blank"); + } + + if (StringUtils.isBlank(flowIdentifier)) { + throw new IllegalArgumentException("Flow identifier cannot be null or blank"); + } + + // ensure the bucket exists + final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier); + if (existingBucket == null) { + LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier); + throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry."); + } + + // ensure the flow exists + final FlowEntity existingFlow = metadataService.getFlowById(flowIdentifier); + if (existingFlow == null) { + LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier); + throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket."); + } + + if (!existingBucket.getId().equals(existingFlow.getBucketId())) { + throw new IllegalStateException("The requested flow is not located in the given bucket"); + } + + // get latest snapshot for the flow + final FlowSnapshotEntity latestSnapshot = metadataService.getLatestSnapshot(existingFlow.getId()); + if (latestSnapshot == null) { + throw new ResourceNotFoundException("The specified flow ID has no versions"); + } + + return FlowMappings.map(existingBucket, latestSnapshot); + } + + public VersionedFlowSnapshotMetadata getLatestFlowSnapshotMetadata(final String flowIdentifier) { + if (StringUtils.isBlank(flowIdentifier)) { + throw new IllegalArgumentException("Flow identifier cannot be null or blank"); + } + + // ensure the flow exists + final FlowEntity existingFlow = metadataService.getFlowById(flowIdentifier); + if (existingFlow == null) { + LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier); + throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket."); + } + + // ensure the bucket exists + final BucketEntity existingBucket = metadataService.getBucketById(existingFlow.getBucketId()); + if (existingBucket == null) { + LOGGER.warn("The specified bucket id [{}] does not exist.", existingFlow.getBucketId()); + throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry."); + } + + // get latest snapshot for the flow + final FlowSnapshotEntity latestSnapshot = metadataService.getLatestSnapshot(existingFlow.getId()); + if (latestSnapshot == null) { + throw new ResourceNotFoundException("The specified flow ID has no versions"); + } + + return FlowMappings.map(existingBucket, latestSnapshot); + } + + public VersionedFlowSnapshotMetadata deleteFlowSnapshot(final String bucketIdentifier, final String flowIdentifier, final Integer version) { + if (StringUtils.isBlank(bucketIdentifier)) { + throw new IllegalArgumentException("Bucket identifier cannot be null or blank"); + } + + if (StringUtils.isBlank(flowIdentifier)) { + throw new IllegalArgumentException("Flow identifier cannot be null or blank"); + } + + if (version == null) { + throw new IllegalArgumentException("Version cannot be null or blank"); + } + + // ensure the bucket exists + final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier); + if (existingBucket == null) { + LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier); + throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry."); + } + + // ensure the flow exists + final FlowEntity existingFlow = metadataService.getFlowById(flowIdentifier); + if (existingFlow == null) { + LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier); + throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket."); + } + + if (!existingBucket.getId().equals(existingFlow.getBucketId())) { + throw new IllegalStateException("The requested flow is not located in the given bucket"); + } + + // ensure the snapshot exists + final FlowSnapshotEntity snapshotEntity = metadataService.getFlowSnapshot(flowIdentifier, version); + if (snapshotEntity == null) { + throw new ResourceNotFoundException("Versioned flow snapshot does not exist for flow " + + flowIdentifier + " and version " + version); + } + + // delete the content of the snapshot + flowPersistenceProvider.deleteFlowContent(bucketIdentifier, flowIdentifier, version); + + // delete the snapshot itself + metadataService.deleteFlowSnapshot(snapshotEntity); + return FlowMappings.map(existingBucket, snapshotEntity); + } + + /** + * Returns the differences between two specified versions of a flow. + * + * @param bucketIdentifier the id of the bucket the flow exists in + * @param flowIdentifier the flow to be examined + * @param versionA the first version of the comparison + * @param versionB the second version of the comparison + * @return The differences between two specified versions, grouped by component. + */ + public VersionedFlowDifference getFlowDiff(final String bucketIdentifier, final String flowIdentifier, + final Integer versionA, final Integer versionB) { + if (StringUtils.isBlank(bucketIdentifier)) { + throw new IllegalArgumentException("Bucket identifier cannot be null or blank"); + } + + if (StringUtils.isBlank(flowIdentifier)) { + throw new IllegalArgumentException("Flow identifier cannot be null or blank"); + } + + if (versionA == null || versionB == null) { + throw new IllegalArgumentException("Version cannot be null or blank"); + } + // older version is always the lower, regardless of the order supplied + final Integer older = Math.min(versionA, versionB); + final Integer newer = Math.max(versionA, versionB); + + // Get the content for both versions of the flow + final byte[] serializedSnapshotA = flowPersistenceProvider.getFlowContent(bucketIdentifier, flowIdentifier, older); + if (serializedSnapshotA == null || serializedSnapshotA.length == 0) { + throw new IllegalStateException("No serialized content found for snapshot with flow identifier " + + flowIdentifier + " and version " + older); + } + + final byte[] serializedSnapshotB = flowPersistenceProvider.getFlowContent(bucketIdentifier, flowIdentifier, newer); + if (serializedSnapshotB == null || serializedSnapshotB.length == 0) { + throw new IllegalStateException("No serialized content found for snapshot with flow identifier " + + flowIdentifier + " and version " + newer); + } + + // deserialize the contents + final InputStream inputA = new ByteArrayInputStream(serializedSnapshotA); + final VersionedFlowSnapshot snapshotA = deserializeFlowContent(inputA); + final VersionedProcessGroup flowContentsA = snapshotA.getFlowContents(); + + final InputStream inputB = new ByteArrayInputStream(serializedSnapshotB); + final VersionedFlowSnapshot snapshotB = deserializeFlowContent(inputB); + final VersionedProcessGroup flowContentsB = snapshotB.getFlowContents(); + + final ComparableDataFlow comparableFlowA = new StandardComparableDataFlow(String.format("Version %d", older), flowContentsA); + final ComparableDataFlow comparableFlowB = new StandardComparableDataFlow(String.format("Version %d", newer), flowContentsB); + + // Compare the two versions of the flow + final FlowComparator flowComparator = new StandardFlowComparator(comparableFlowA, comparableFlowB, + null, new ConciseEvolvingDifferenceDescriptor()); + final FlowComparison flowComparison = flowComparator.compare(); + + final VersionedFlowDifference result = new VersionedFlowDifference(); + result.setBucketId(bucketIdentifier); + result.setFlowId(flowIdentifier); + result.setVersionA(older); + result.setVersionB(newer); + + final Set differenceGroups = getStringComponentDifferenceGroupMap(flowComparison.getDifferences()); + result.setComponentDifferenceGroups(differenceGroups); + + return result; + } + + /** + * Group the differences in the comparison by component + * @param flowDifferences The differences to group together by component + * @return A set of componentDifferenceGroups where each entry contains a set of differences specific to that group + */ + private Set getStringComponentDifferenceGroupMap(Set flowDifferences) { + Map differenceGroups = new HashMap<>(); + for (FlowDifference diff : flowDifferences) { + ComponentDifferenceGroup group; + // A component may only exist on only one version for new/removed components + VersionedComponent component = ObjectUtils.firstNonNull(diff.getComponentA(), diff.getComponentB()); + if(differenceGroups.containsKey(component.getIdentifier())){ + group = differenceGroups.get(component.getIdentifier()); + }else{ + group = FlowMappings.map(component); + differenceGroups.put(component.getIdentifier(), group); + } + group.getDifferences().add(FlowMappings.map(diff)); + } + return differenceGroups.values().stream().collect(Collectors.toSet()); + } + + // ---------------------- Field methods --------------------------------------------- + + public Set getBucketFields() { + return metadataService.getBucketFields(); + } + + public Set getBucketItemFields() { + return metadataService.getBucketItemFields(); + } + + public Set getFlowFields() { + return metadataService.getFlowFields(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/alias/RegistryUrlAliasService.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/alias/RegistryUrlAliasService.java new file mode 100644 index 0000000000..b444e83dbb --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/alias/RegistryUrlAliasService.java @@ -0,0 +1,171 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.service.alias; + +import org.apache.nifi.registry.flow.VersionedFlowCoordinates; +import org.apache.nifi.registry.flow.VersionedProcessGroup; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.apache.nifi.registry.provider.ProviderFactoryException; +import org.apache.nifi.registry.provider.StandardProviderFactory; +import org.apache.nifi.registry.security.util.XmlUtils; +import org.apache.nifi.registry.url.aliaser.generated.Alias; +import org.apache.nifi.registry.url.aliaser.generated.Aliases; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.xml.sax.SAXException; + +import javax.xml.XMLConstants; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBElement; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; +import javax.xml.stream.XMLStreamException; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import java.io.File; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * Allows aliasing of registry url(s) without modifying the flows on disk. + */ +@Service +public class RegistryUrlAliasService { + private static final String ALIASES_XSD = "/aliases.xsd"; + private static final String JAXB_GENERATED_PATH = "org.apache.nifi.registry.url.aliaser.generated"; + private static final JAXBContext JAXB_CONTEXT = initializeJaxbContext(); + + /** + * Load the JAXBContext. + */ + private static JAXBContext initializeJaxbContext() { + try { + return JAXBContext.newInstance(JAXB_GENERATED_PATH, RegistryUrlAliasService.class.getClassLoader()); + } catch (JAXBException e) { + throw new RuntimeException("Unable to create JAXBContext.", e); + } + } + + // Will be LinkedHashMap to preserve insertion order. + private final Map aliases; + + @Autowired + public RegistryUrlAliasService(NiFiRegistryProperties niFiRegistryProperties) { + this(createAliases(niFiRegistryProperties)); + } + + private static List createAliases(NiFiRegistryProperties niFiRegistryProperties) { + File configurationFile = niFiRegistryProperties.getRegistryAliasConfigurationFile(); + if (configurationFile.exists()) { + try { + // find the schema + final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + final Schema schema = schemaFactory.newSchema(StandardProviderFactory.class.getResource(ALIASES_XSD)); + + // attempt to unmarshal + final Unmarshaller unmarshaller = JAXB_CONTEXT.createUnmarshaller(); + unmarshaller.setSchema(schema); + + final JAXBElement element = unmarshaller.unmarshal(XmlUtils.createSafeReader(new StreamSource(configurationFile)), Aliases.class); + return element.getValue().getAlias(); + } catch (SAXException | JAXBException | XMLStreamException e) { + throw new ProviderFactoryException("Unable to load the registry alias configuration file at: " + configurationFile.getAbsolutePath(), e); + } + } else { + return Collections.emptyList(); + } + } + + protected RegistryUrlAliasService(List aliases) { + Pattern urlStart = Pattern.compile("^https?://"); + + this.aliases = new LinkedHashMap<>(); + + for (Alias alias : aliases) { + String internal = alias.getInternal(); + String external = alias.getExternal(); + + if (!urlStart.matcher(external).find()) { + throw new IllegalArgumentException("Expected " + external + " to start with http:// or https://"); + } + + if (this.aliases.put(internal, external) != null) { + throw new IllegalArgumentException("Duplicate internal token " + internal); + } + } + } + + /** + * Recursively replaces the aliases with the external url for a process group and children. + */ + public void setExternal(VersionedProcessGroup processGroup) { + processGroup.getProcessGroups().forEach(this::setExternal); + + VersionedFlowCoordinates coordinates = processGroup.getVersionedFlowCoordinates(); + if (coordinates != null) { + coordinates.setRegistryUrl(getExternal(coordinates.getRegistryUrl())); + } + } + + /** + * Recursively replaces the external url with the aliases for a process group and children. + */ + public void setInternal(VersionedProcessGroup processGroup) { + processGroup.getProcessGroups().forEach(this::setInternal); + + VersionedFlowCoordinates coordinates = processGroup.getVersionedFlowCoordinates(); + if (coordinates != null) { + coordinates.setRegistryUrl(getInternal(coordinates.getRegistryUrl())); + } + } + + protected String getExternal(String url) { + for (Map.Entry alias : aliases.entrySet()) { + String internal = alias.getKey(); + String external = alias.getValue(); + + if (url.startsWith(internal)) { + int internalLength = internal.length(); + if (url.length() == internalLength) { + return external; + } + return external + url.substring(internalLength); + } + } + return url; + } + + protected String getInternal(String url) { + for (Map.Entry alias : aliases.entrySet()) { + String internal = alias.getKey(); + String external = alias.getValue(); + + if (url.startsWith(external)) { + int externalLength = external.length(); + if (url.length() == externalLength) { + return internal; + } + return internal + url.substring(externalLength); + } + } + return url; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/BundleMetadataExtractors.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/BundleMetadataExtractors.java new file mode 100644 index 0000000000..0afa7e77bd --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/BundleMetadataExtractors.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.service.extension; + +import org.apache.nifi.registry.bundle.extract.BundleExtractor; +import org.apache.nifi.registry.extension.bundle.BundleType; +import org.apache.nifi.registry.bundle.extract.minificpp.MiNiFiCppBundleExtractor; +import org.apache.nifi.registry.bundle.extract.nar.NarBundleExtractor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +public class BundleMetadataExtractors { + + private Map extractors; + + @Bean + public synchronized Map getExtractors() { + if (extractors == null) { + extractors = new HashMap<>(); + extractors.put(BundleType.NIFI_NAR, new NarBundleExtractor()); + extractors.put(BundleType.MINIFI_CPP, new MiNiFiCppBundleExtractor()); + } + + return extractors; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/ExtensionService.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/ExtensionService.java new file mode 100644 index 0000000000..e8276a6e35 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/ExtensionService.java @@ -0,0 +1,254 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.service.extension; + +import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.extension.bundle.Bundle; +import org.apache.nifi.registry.extension.bundle.BundleFilterParams; +import org.apache.nifi.registry.extension.bundle.BundleType; +import org.apache.nifi.registry.extension.bundle.BundleVersion; +import org.apache.nifi.registry.extension.bundle.BundleVersionFilterParams; +import org.apache.nifi.registry.extension.bundle.BundleVersionMetadata; +import org.apache.nifi.registry.extension.component.manifest.Extension; +import org.apache.nifi.registry.extension.component.ExtensionFilterParams; +import org.apache.nifi.registry.extension.component.ExtensionMetadata; +import org.apache.nifi.registry.extension.component.TagCount; +import org.apache.nifi.registry.extension.component.manifest.ProvidedServiceAPI; +import org.apache.nifi.registry.extension.repo.ExtensionRepoArtifact; +import org.apache.nifi.registry.extension.repo.ExtensionRepoBucket; +import org.apache.nifi.registry.extension.repo.ExtensionRepoGroup; +import org.apache.nifi.registry.extension.repo.ExtensionRepoVersionSummary; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; +import java.util.Set; +import java.util.SortedSet; + +public interface ExtensionService { + + // ----- Extension Bundles ----- + + /** + * Creates a version of an extension bundle. + * + * The InputStream is expected to contain the binary contents of a bundle in the format specified by bundleType. + * + * The metadata will be extracted from the bundle and used to determine if this is a new version of an existing bundle, + * or it will create a new bundle and this as the first version if one doesn't already exist. + * + * @param bucketIdentifier the bucket id + * @param bundleType the type of bundle + * @param inputStream the binary content of the bundle + * @param clientSha256 the SHA-256 hex supplied by the client + * @return the BundleVersion representing all of the information about the bundle + * @throws IOException if an error occurs processing the InputStream + */ + BundleVersion createBundleVersion(String bucketIdentifier, BundleType bundleType, + InputStream inputStream, String clientSha256) throws IOException; + + /** + * Retrieves the extension bundles in the given buckets. + * + * @param bucketIdentifiers the bucket identifiers + * @param filterParams the optional filter params + * @return the bundles in the given buckets + */ + List getBundles(Set bucketIdentifiers, BundleFilterParams filterParams); + + /** + * Retrieves the extension bundles in the given bucket. + * + * @param bucketIdentifier the bucket identifier + * @return the bundles in the given bucket + */ + List getBundlesByBucket(String bucketIdentifier); + + /** + * Retrieve the extension bundle with the given id. + * + * @param extensionBundleIdentifier the extension bundle id + * @return the bundle + */ + Bundle getBundle(String extensionBundleIdentifier); + + /** + * Deletes the given extension bundle and all it's versions. + * + * @param bundle the extension bundle to delete + * @return the deleted bundle + */ + Bundle deleteBundle(Bundle bundle); + + // ----- Extension Bundle Versions ----- + + /** + * Retrieves the extension bundle versions in the given buckets. + * + * @param bucketIdentifiers the bucket identifiers + * @param filterParams the optional filter params + * @return the set of extension bundle versions + */ + SortedSet getBundleVersions(Set bucketIdentifiers, BundleVersionFilterParams filterParams); + + /** + * Retrieves the versions of the given extension bundle. + * + * @param extensionBundleIdentifier the extension bundle id + * @return the sorted set of versions for the given bundle + */ + SortedSet getBundleVersions(String extensionBundleIdentifier); + + /** + * Retrieves the full BundleVersion object, including version metadata, bundle metadata, and bucket metadata. + * + * @param bundleId the bundle id + * @param version the version + * @return the BundleVersion + */ + BundleVersion getBundleVersion(String bucketId, String bundleId, String version); + + /** + * Retrieves the full BundleVersion object, including version metadata, bundle metadata, and bucket metadata. + * + * @param bucketId the bucket id where the bundle is located + * @param groupId the group id of the bundle + * @param artifactId the artifact id of the bundle + * @param version the version of the bundle + * @return the extension bundle version + */ + BundleVersion getBundleVersion(String bucketId, String groupId, String artifactId, String version); + + /** + * Writes the binary content of the extension bundle version to the given OutputStream. + * + * @param bundleVersion the version to write the content for + * @param out the output stream to write to + */ + void writeBundleVersionContent(BundleVersion bundleVersion, OutputStream out); + + /** + * Deletes the given version of the extension bundle. + * + * @param bundleVersion the version to delete + * @return the deleted extension bundle version + */ + BundleVersion deleteBundleVersion(BundleVersion bundleVersion); + + // ----- Extension Methods ------ + + /** + * Retrieves all extensions in the given buckets, sorted by name. + * + * @param bucketIdentifiers the identifiers of the buckets + * @param filterParams the filter params + * @return extensions in the given buckets matching the filter params + */ + SortedSet getExtensionMetadata(Set bucketIdentifiers, ExtensionFilterParams filterParams); + + /** + * Retrieves the extensions in the given buckets that provided the given service API. + * + * This would be used when a processor has a property specifying a service API and we want to look up implementations. + * + * @param bucketIdentifiers the identifiers of the buckets + * @param providedServiceAPI the provided service API + * @return the extensions providing the given service + */ + SortedSet getExtensionMetadata(Set bucketIdentifiers, ProvidedServiceAPI providedServiceAPI); + + /** + * Retrieves the set of extensions for the given bundle version. + * + * @param bundleVersion the bundle version to retrieve extensions for + * @return the set of extensions for the given bundle version + */ + SortedSet getExtensionMetadata(BundleVersion bundleVersion); + + /** + * Retrieves the extension with the given name in the given bundle version. + * + * @param bundleVersion the bundle version + * @param name the extension name + * @return the extension + */ + Extension getExtension(BundleVersion bundleVersion, String name); + + /** + * Writes the documentation for the extension with the given name and bundle to the given output stream. + * + * @param bundleVersion the bundle version + * @param name the name of the extension + * @param outputStream the output stream to write to + * @throws IOException if an error occurs writing to the output stream + */ + void writeExtensionDocs(BundleVersion bundleVersion, String name, OutputStream outputStream) throws IOException; + + /** + * Writes the additional details documentation for the extension with the given name and bundle to the given output stream. + * + * @param bundleVersion the bundle version + * @param name the name of the extension + * @param outputStream the output stream to write to + * @throws IOException if an error occurs writing to the output stream + */ + void writeAdditionalDetailsDocs(BundleVersion bundleVersion, String name, OutputStream outputStream) throws IOException; + + /** + * @return all know tags + */ + SortedSet getExtensionTags(); + + // ----- Extension Repo Methods ----- + + /** + * Retrieves the extension repo buckets for the given bucket ids. + * + * @param bucketIds the bucket ids + * @return the set of buckets + */ + SortedSet getExtensionRepoBuckets(Set bucketIds); + + /** + * Retrieves the extension repo groups for the given bucket. + * + * @param bucket the bucket + * @return the groups for the bucket + */ + SortedSet getExtensionRepoGroups(Bucket bucket); + + /** + * Retrieves the extension repo artifacts for the given bucket and group. + * + * @param bucket the bucket + * @param groupId the group id + * @return the artifacts for the bucket and group + */ + SortedSet getExtensionRepoArtifacts(Bucket bucket, String groupId); + + /** + * Retrieves the extension repo version summaries for the given bucket, group, and artifact. + * + * @param bucket the bucket + * @param groupId the group id + * @param artifactId the artifact id + * @return the version summaries for the bucket, group, and artifact + */ + SortedSet getExtensionRepoVersions(Bucket bucket, String groupId, String artifactId); + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/StandardExtensionService.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/StandardExtensionService.java new file mode 100644 index 0000000000..0dddbfca0f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/StandardExtensionService.java @@ -0,0 +1,962 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.service.extension; + +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.bundle.extract.BundleExtractor; +import org.apache.nifi.registry.bundle.model.BundleDetails; +import org.apache.nifi.registry.bundle.model.BundleIdentifier; +import org.apache.nifi.registry.db.entity.BucketEntity; +import org.apache.nifi.registry.db.entity.BundleEntity; +import org.apache.nifi.registry.db.entity.BundleVersionDependencyEntity; +import org.apache.nifi.registry.db.entity.BundleVersionEntity; +import org.apache.nifi.registry.db.entity.ExtensionAdditionalDetailsEntity; +import org.apache.nifi.registry.db.entity.ExtensionEntity; +import org.apache.nifi.registry.exception.ResourceNotFoundException; +import org.apache.nifi.registry.extension.BundleCoordinate; +import org.apache.nifi.registry.extension.BundlePersistenceContext; +import org.apache.nifi.registry.extension.BundlePersistenceProvider; +import org.apache.nifi.registry.extension.BundleVersionCoordinate; +import org.apache.nifi.registry.extension.BundleVersionType; +import org.apache.nifi.registry.extension.bundle.BuildInfo; +import org.apache.nifi.registry.extension.bundle.Bundle; +import org.apache.nifi.registry.extension.bundle.BundleFilterParams; +import org.apache.nifi.registry.extension.bundle.BundleType; +import org.apache.nifi.registry.extension.bundle.BundleVersion; +import org.apache.nifi.registry.extension.bundle.BundleVersionDependency; +import org.apache.nifi.registry.extension.bundle.BundleVersionFilterParams; +import org.apache.nifi.registry.extension.bundle.BundleVersionMetadata; +import org.apache.nifi.registry.extension.component.ExtensionFilterParams; +import org.apache.nifi.registry.extension.component.ExtensionMetadata; +import org.apache.nifi.registry.extension.component.TagCount; +import org.apache.nifi.registry.extension.component.manifest.Extension; +import org.apache.nifi.registry.extension.component.manifest.ProvidedServiceAPI; +import org.apache.nifi.registry.extension.repo.ExtensionRepoArtifact; +import org.apache.nifi.registry.extension.repo.ExtensionRepoBucket; +import org.apache.nifi.registry.extension.repo.ExtensionRepoGroup; +import org.apache.nifi.registry.extension.repo.ExtensionRepoVersionSummary; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.apache.nifi.registry.provider.extension.StandardBundleCoordinate; +import org.apache.nifi.registry.provider.extension.StandardBundlePersistenceContext; +import org.apache.nifi.registry.provider.extension.StandardBundleVersionCoordinate; +import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils; +import org.apache.nifi.registry.serialization.Serializer; +import org.apache.nifi.registry.service.MetadataService; +import org.apache.nifi.registry.service.extension.docs.DocumentationConstants; +import org.apache.nifi.registry.service.extension.docs.ExtensionDocWriter; +import org.apache.nifi.registry.service.mapper.BucketMappings; +import org.apache.nifi.registry.service.mapper.ExtensionMappings; +import org.apache.nifi.registry.util.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.Validator; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +public class StandardExtensionService implements ExtensionService { + + private static final Logger LOGGER = LoggerFactory.getLogger(StandardExtensionService.class); + + static final String SNAPSHOT_VERSION_SUFFIX = "SNAPSHOT"; + + private final Serializer extensionSerializer; + private final ExtensionDocWriter extensionDocWriter; + private final MetadataService metadataService; + private final Map extractors; + private final BundlePersistenceProvider bundlePersistenceProvider; + private final Validator validator; + private final File extensionsWorkingDir; + + @Autowired + public StandardExtensionService(final Serializer extensionSerializer, + final ExtensionDocWriter extensionDocWriter, + final MetadataService metadataService, + final Map extractors, + final BundlePersistenceProvider bundlePersistenceProvider, + final Validator validator, + final NiFiRegistryProperties properties) { + this.extensionSerializer = extensionSerializer; + this.extensionDocWriter = extensionDocWriter; + this.metadataService = metadataService; + this.extractors = extractors; + this.bundlePersistenceProvider = bundlePersistenceProvider; + this.validator = validator; + this.extensionsWorkingDir = properties.getExtensionsWorkingDirectory(); + Validate.notNull(this.extensionSerializer); + Validate.notNull(this.metadataService); + Validate.notNull(this.extractors); + Validate.notNull(this.bundlePersistenceProvider); + Validate.notNull(this.validator); + Validate.notNull(this.extensionsWorkingDir); + } + + private void validate(T t, String invalidMessage) { + final Set> violations = validator.validate(t); + if (violations.size() > 0) { + throw new ConstraintViolationException(invalidMessage, violations); + } + } + + @Override + public BundleVersion createBundleVersion(final String bucketIdentifier, final BundleType bundleType, + final InputStream inputStream, final String clientSha256) throws IOException { + if (StringUtils.isBlank(bucketIdentifier)) { + throw new IllegalArgumentException("Bucket identifier cannot be null or blank"); + } + + if (bundleType == null) { + throw new IllegalArgumentException("Bundle type cannot be null"); + } + + if (inputStream == null) { + throw new IllegalArgumentException("Extension bundle input stream cannot be null"); + } + + if (!extractors.containsKey(bundleType)) { + throw new IllegalArgumentException("No metadata extractor is registered for bundle-type: " + bundleType); + } + + // ensure the bucket exists + final BucketEntity existingBucket = getBucketEntity(bucketIdentifier); + + // ensure the extensions directory exists and we can read and write to it + FileUtils.ensureDirectoryExistAndCanReadAndWrite(extensionsWorkingDir); + + final String extensionWorkingFilename = UUID.randomUUID().toString(); + final File extensionWorkingFile = new File(extensionsWorkingDir, extensionWorkingFilename); + LOGGER.debug("Writing bundle contents to working directory at {}", new Object[]{extensionWorkingFile.getAbsolutePath()}); + + try { + // write the contents of the input stream to a temporary file in the extensions working directory + final MessageDigest sha256Digest = DigestUtils.getSha256Digest(); + try (final DigestInputStream digestInputStream = new DigestInputStream(inputStream, sha256Digest); + final OutputStream out = new FileOutputStream(extensionWorkingFile)) { + IOUtils.copy(digestInputStream, out); + } + + // get the hex of the SHA-256 computed by the server and compare to the client provided SHA-256, if one was provided + final String sha256Hex = Hex.encodeHexString(sha256Digest.digest()); + final boolean sha256Supplied = !StringUtils.isBlank(clientSha256); + if (sha256Supplied && !sha256Hex.equalsIgnoreCase(clientSha256)) { + LOGGER.error("Client provided SHA-256 of '{}', but server calculated '{}'", new Object[]{clientSha256, sha256Hex}); + throw new IllegalStateException("The SHA-256 of the received extension bundle does not match the SHA-256 provided by the client"); + } + + // extract the details of the bundle from the temp file in the working directory + final BundleDetails bundleDetails; + try (final InputStream in = new FileInputStream(extensionWorkingFile)) { + final BundleExtractor extractor = extractors.get(bundleType); + bundleDetails = extractor.extract(in); + } + + final BundleIdentifier bundleIdentifier = bundleDetails.getBundleIdentifier(); + final BuildInfo buildInfo = bundleDetails.getBuildInfo(); + + final String groupId = bundleIdentifier.getGroupId(); + final String artifactId = bundleIdentifier.getArtifactId(); + final String version = bundleIdentifier.getVersion(); + + final boolean isSnapshotVersion = version.endsWith(SNAPSHOT_VERSION_SUFFIX); + final boolean overwriteBundleVersion = isSnapshotVersion || existingBucket.isAllowExtensionBundleRedeploy(); + + LOGGER.debug("Extracted bundle details - '{}:{}:{}'", new Object[]{groupId, artifactId, version}); + + // a bundle with the same group, artifact, and version can exist in multiple buckets, but only if it contains the same binary content, or if its a snapshot version + // we can determine that by comparing the SHA-256 digest of the incoming bundle against existing bundles with the same group, artifact, version + final List allExistingVersions = metadataService.getBundleVersionsGlobal(groupId, artifactId, version); + for (final BundleVersionEntity existingVersionEntity : allExistingVersions) { + if (!existingVersionEntity.getSha256Hex().equals(sha256Hex) && !isSnapshotVersion) { + throw new IllegalStateException("Found existing extension bundle with same group, artifact, and version, but different SHA-256 checksums"); + } + } + + // get the existing extension bundle entity, or create a new one if one does not exist in the bucket with the group + artifact + final long currentTime = System.currentTimeMillis(); + final BundleEntity bundleEntity = getOrCreateExtensionBundle(bucketIdentifier, groupId, artifactId, bundleType, currentTime); + + // check if the version of incoming bundle already exists in the bucket + // if it exists and it is a snapshot version or the bucket allows redeploying, then first delete the row in the extension_bundle_version table so we can create a new one + // otherwise we throw an exception because we don't allow the same version in the same bucket + final BundleVersionEntity existingVersion = metadataService.getBundleVersion(bucketIdentifier, groupId, artifactId, version); + if (existingVersion != null) { + if (overwriteBundleVersion) { + LOGGER.debug("Bundle overwriting allowed, deleting existing version..."); + metadataService.deleteBundleVersion(existingVersion); + } else { + LOGGER.warn("The specified version [{}] already exists for extension bundle [{}].", new Object[]{version, bundleEntity.getId()}); + throw new IllegalStateException("The specified version already exists for the given extension bundle"); + } + } + + // create the version metadata instance and validate it has all the required fields + final String userIdentity = NiFiUserUtils.getNiFiUserIdentity(); + final BundleVersionMetadata versionMetadata = new BundleVersionMetadata(); + versionMetadata.setId(UUID.randomUUID().toString()); + versionMetadata.setBundleId(bundleEntity.getId()); + versionMetadata.setBucketId(bucketIdentifier); + versionMetadata.setVersion(version); + versionMetadata.setTimestamp(currentTime); + versionMetadata.setAuthor(userIdentity); + versionMetadata.setSha256(sha256Hex); + versionMetadata.setSha256Supplied(sha256Supplied); + versionMetadata.setContentSize(extensionWorkingFile.length()); + versionMetadata.setSystemApiVersion(bundleDetails.getSystemApiVersion()); + versionMetadata.setBuildInfo(buildInfo); + + validate(versionMetadata, "Cannot create extension bundle version"); + + // create the bundle version in the metadata db + final BundleVersionEntity versionEntity = ExtensionMappings.map(versionMetadata); + metadataService.createBundleVersion(versionEntity); + + // create and persist the version dependencies in the metadata db + final Set dependencyEntities = getDependencyEntities(versionEntity, bundleDetails); + dependencyEntities.forEach(d -> metadataService.createDependency(d)); + + // create and persist extensions in the metadata db + final Set extensionEntities = getExtensionEntities(versionEntity, bundleDetails); + extensionEntities.forEach(e -> metadataService.createExtension(e)); + + // persist the content of the bundle to the persistence provider + persistBundleVersionContent(bundleType, bundleEntity, versionEntity, extensionWorkingFile, overwriteBundleVersion); + + // get the updated extension bundle so it contains the correct version count + final BundleEntity updatedBundle = metadataService.getBundle(bucketIdentifier, groupId, artifactId); + + // create the full BundleVersion instance to return + final BundleVersion bundleVersion = new BundleVersion(); + bundleVersion.setVersionMetadata(versionMetadata); + bundleVersion.setBundle(ExtensionMappings.map(existingBucket, updatedBundle)); + bundleVersion.setBucket(BucketMappings.map(existingBucket)); + + final Set dependencies = new HashSet<>(); + dependencyEntities.forEach(d -> dependencies.add(ExtensionMappings.map(d))); + bundleVersion.setDependencies(dependencies); + + LOGGER.debug("Created bundle - '{}:{}:{}'", new Object[]{groupId, artifactId, version}); + return bundleVersion; + + } finally { + if (extensionWorkingFile.exists()) { + try { + extensionWorkingFile.delete(); + } catch (Exception e) { + LOGGER.warn("Error removing temporary extension bundle file at {}", + new Object[]{extensionWorkingFile.getAbsolutePath()}); + } + } + } + } + + private Set getDependencyEntities(final BundleVersionEntity versionEntity, final BundleDetails bundleDetails) { + final Set dependencyCoordinates = bundleDetails.getDependencies(); + if (dependencyCoordinates == null) { + return Collections.emptySet(); + } + + final Set versionDependencies = new HashSet<>(); + + for (final BundleIdentifier dependencyCoordinate : dependencyCoordinates) { + final BundleVersionDependency versionDependency = new BundleVersionDependency(); + versionDependency.setGroupId(dependencyCoordinate.getGroupId()); + versionDependency.setArtifactId(dependencyCoordinate.getArtifactId()); + versionDependency.setVersion(dependencyCoordinate.getVersion()); + validate(versionDependency, "Cannot create extension bundle version dependency"); + + final BundleVersionDependencyEntity versionDependencyEntity = ExtensionMappings.map(versionDependency); + versionDependencyEntity.setId(UUID.randomUUID().toString()); + versionDependencyEntity.setExtensionBundleVersionId(versionEntity.getId()); + + versionDependencies.add(versionDependencyEntity); + } + + return versionDependencies; + } + + private Set getExtensionEntities(final BundleVersionEntity versionEntity, final BundleDetails bundleDetails) { + final Set extensions = bundleDetails.getExtensions(); + if (extensions == null) { + return Collections.emptySet(); + } + + final Set extensionEntities = new HashSet<>(); + final Map additionalDetails = bundleDetails.getAdditionalDetails(); + + for (final Extension extension : extensions) { + validate(extension, "Invalid extension due to one or more constraint violations"); + + // Convert Extension to ExtensionEntity and populate ids + final ExtensionEntity extensionEntity = ExtensionMappings.map(extension, extensionSerializer); + extensionEntity.setId(UUID.randomUUID().toString()); + extensionEntity.setBundleVersionId(versionEntity.getId()); + + extensionEntity.getRestrictions().forEach(r -> { + r.setId(UUID.randomUUID().toString()); + r.setExtensionId(extensionEntity.getId()); + }); + + extensionEntity.getProvidedServiceApis().forEach(p -> { + p.setId(UUID.randomUUID().toString()); + p.setExtensionId(extensionEntity.getId()); + }); + + // Check the additionalDetails map to see if there is an entry, and if so populate it + final String additionalDetailsContent = additionalDetails.get(extensionEntity.getName()); + if (!StringUtils.isBlank(additionalDetailsContent)) { + LOGGER.debug("Found additional details documentation for extension '{}'", new Object[]{extensionEntity.getName()}); + extensionEntity.setAdditionalDetails(additionalDetailsContent); + } + + extensionEntities.add(extensionEntity); + } + + return extensionEntities; + } + + + private BundleEntity getOrCreateExtensionBundle(final String bucketId, final String groupId, final String artifactId, + final BundleType bundleType, final long currentTime) { + + BundleEntity existingBundleEntity = metadataService.getBundle(bucketId, groupId, artifactId); + if (existingBundleEntity == null) { + final Bundle bundle = new Bundle(); + bundle.setIdentifier(UUID.randomUUID().toString()); + bundle.setBucketIdentifier(bucketId); + bundle.setName(groupId + ":" + artifactId); + bundle.setGroupId(groupId); + bundle.setArtifactId(artifactId); + bundle.setBundleType(bundleType); + bundle.setCreatedTimestamp(currentTime); + bundle.setModifiedTimestamp(currentTime); + + validate(bundle, "Cannot create extension bundle"); + existingBundleEntity = metadataService.createBundle(ExtensionMappings.map(bundle)); + } else { + if (bundleType != existingBundleEntity.getBundleType()) { + throw new IllegalStateException("A bundle already exists with the same group id and artifact id, but a different bundle type"); + } + } + + return existingBundleEntity; + } + + private void persistBundleVersionContent(final BundleType bundleType, final BundleEntity bundle, final BundleVersionEntity bundleVersion, + final File extensionWorkingFile, final boolean overwriteBundleVersion) throws IOException { + + final BundleVersionCoordinate versionCoordinate = new StandardBundleVersionCoordinate.Builder() + .bucketId(bundle.getBucketId()) + .groupId(bundle.getGroupId()) + .artifactId(bundle.getArtifactId()) + .version(bundleVersion.getVersion()) + .type(getProviderBundleType(bundleType)) + .build(); + + final BundlePersistenceContext context = new StandardBundlePersistenceContext.Builder() + .coordinate(versionCoordinate) + .bundleSize(bundleVersion.getContentSize()) + .author(bundleVersion.getCreatedBy()) + .timestamp(bundleVersion.getCreated().getTime()) + .build(); + + try (final InputStream in = new FileInputStream(extensionWorkingFile); + final InputStream bufIn = new BufferedInputStream(in)) { + if (overwriteBundleVersion) { + bundlePersistenceProvider.updateBundleVersion(context, bufIn); + LOGGER.debug("Bundle version updated in persistence provider - {}", new Object[]{versionCoordinate.toString()}); + } else { + bundlePersistenceProvider.createBundleVersion(context, bufIn); + LOGGER.debug("Bundle version created in persistence provider - {}", new Object[]{versionCoordinate.toString()}); + } + } + } + + private BundleVersionType getProviderBundleType(final BundleType bundleType) { + switch (bundleType) { + case NIFI_NAR: + return BundleVersionType.NIFI_NAR; + case MINIFI_CPP: + return BundleVersionType.MINIFI_CPP; + default: + throw new IllegalArgumentException("Unknown bundle type: " + bundleType.toString()); + } + } + + @Override + public List getBundles(final Set bucketIdentifiers, final BundleFilterParams filterParams) { + if (bucketIdentifiers == null) { + throw new IllegalArgumentException("Bucket identifiers cannot be null"); + } + + final List bundleEntities = metadataService.getBundles(bucketIdentifiers, + filterParams == null ? BundleFilterParams.empty() : filterParams); + return bundleEntities.stream().map(b -> ExtensionMappings.map(null, b)).collect(Collectors.toList()); + } + + @Override + public List getBundlesByBucket(final String bucketIdentifier) { + if (StringUtils.isBlank(bucketIdentifier)) { + throw new IllegalArgumentException("Bucket identifier cannot be null or blank"); + } + final BucketEntity existingBucket = getBucketEntity(bucketIdentifier); + + final List bundleEntities = metadataService.getBundlesByBucket(bucketIdentifier); + return bundleEntities.stream().map(b -> ExtensionMappings.map(existingBucket, b)).collect(Collectors.toList()); + } + + @Override + public Bundle getBundle(final String bundleIdentifier) { + if (StringUtils.isBlank(bundleIdentifier)) { + throw new IllegalArgumentException("Bundle identifier cannot be null or blank"); + } + + final BundleEntity existingBundle = getBundleEntity(bundleIdentifier); + final BucketEntity existingBucket = getBucketEntity(existingBundle.getBucketId()); + return ExtensionMappings.map(existingBucket, existingBundle); + } + + @Override + public Bundle deleteBundle(final Bundle bundle) { + if (bundle == null) { + throw new IllegalArgumentException("Extension bundle cannot be null"); + } + + // delete the bundle from the database + metadataService.deleteBundle(bundle.getIdentifier()); + + // delete all content associated with the bundle in the persistence provider + final BundleCoordinate bundleCoordinate = new StandardBundleCoordinate.Builder() + .bucketId(bundle.getBucketIdentifier()) + .groupId(bundle.getGroupId()) + .artifactId(bundle.getArtifactId()) + .build(); + + bundlePersistenceProvider.deleteAllBundleVersions(bundleCoordinate); + + return bundle; + } + + // ---- BundleVersion methods ----- + + @Override + public SortedSet getBundleVersions(final Set bucketIdentifiers, + final BundleVersionFilterParams filterParams) { + if (bucketIdentifiers == null) { + throw new IllegalArgumentException("Bucket identifiers cannot be null"); + } + + final SortedSet sortedVersions = new TreeSet<>( + Comparator.comparing(BundleVersionMetadata::getBundleId) + .thenComparing(BundleVersionMetadata::getVersion) + ); + + final List bundleVersionEntities = metadataService.getBundleVersions(bucketIdentifiers, + filterParams == null ? BundleVersionFilterParams.empty() : filterParams); + if (bundleVersionEntities != null) { + bundleVersionEntities.forEach(bv -> sortedVersions.add(ExtensionMappings.map(bv))); + } + return sortedVersions; + } + + @Override + public SortedSet getBundleVersions(final String bundleIdentifier) { + if (StringUtils.isBlank(bundleIdentifier)) { + throw new IllegalArgumentException("Extension bundle identifier cannot be null or blank"); + } + + // ensure the bundle exists + final BundleEntity existingBundle = getBundleEntity(bundleIdentifier); + + return getExtensionBundleVersionsSet(existingBundle); + } + + private SortedSet getExtensionBundleVersionsSet(final BundleEntity existingBundle) { + final SortedSet sortedVersions = new TreeSet<>(Collections.reverseOrder()); + + final List existingVersions = metadataService.getBundleVersions(existingBundle.getId()); + if (existingVersions != null) { + existingVersions.stream().forEach(s -> sortedVersions.add(ExtensionMappings.map(s))); + } + return sortedVersions; + } + + @Override + public BundleVersion getBundleVersion(final String bucketId, final String bundleId, final String version) { + if (StringUtils.isBlank(bucketId)) { + throw new IllegalArgumentException("Bucket id cannot be null or blank"); + } + + if (StringUtils.isBlank(bundleId)) { + throw new IllegalArgumentException("Bundle id cannot be null or blank"); + } + + if (StringUtils.isBlank(version)) { + throw new IllegalArgumentException("Version cannot be null or blank"); + } + + // ensure the bucket exists + final BucketEntity existingBucket = getBucketEntity(bucketId); + + // ensure the bundle exists + final BundleEntity existingBundle = getBundleEntity(bundleId); + + if (!existingBucket.getId().equals(existingBundle.getBucketId())) { + throw new IllegalStateException("The requested bundle is not located in the given bucket"); + } + + // retrieve the version of the bundle... + final BundleVersionEntity existingVersion = metadataService.getBundleVersion(bundleId, version); + if (existingVersion == null) { + LOGGER.warn("The specified version [{}] does not exist for extension bundle [{}].", new Object[]{version, bundleId}); + throw new ResourceNotFoundException("The specified extension bundle version does not exist."); + } + return getBundleVersion(existingBucket, existingBundle, existingVersion); + + } + + @Override + public BundleVersion getBundleVersion(final String bucketId, final String groupId, final String artifactId, final String version) { + if (StringUtils.isBlank(bucketId)) { + throw new IllegalArgumentException("Bucket id cannot be null or blank"); + } + + if (StringUtils.isBlank(groupId)) { + throw new IllegalArgumentException("Group id cannot be null or blank"); + } + + if (StringUtils.isBlank(artifactId)) { + throw new IllegalArgumentException("Artifact id cannot be null or blank"); + } + + if (StringUtils.isBlank(version)) { + throw new IllegalArgumentException("Version cannot be null or blank"); + } + + // ensure the bucket exists + final BucketEntity existingBucket = getBucketEntity(bucketId); + + // ensure the bundle exists + final BundleEntity existingBundle = metadataService.getBundle(bucketId, groupId, artifactId); + if (existingBundle == null) { + LOGGER.warn("The specified extension bundle [{}-{}-{}] does not exist.", new Object[]{bucketId, groupId, artifactId}); + throw new ResourceNotFoundException("The specified extension bundle does not exist in this bucket."); + } + + //ensure the version of the bundle exists + final BundleVersionEntity existingVersion = metadataService.getBundleVersion(bucketId, groupId, artifactId, version); + if (existingVersion == null) { + LOGGER.warn("The specified extension bundle version [{}-{}-{}-{}] does not exist.", new Object[]{bucketId, groupId, artifactId, version}); + throw new ResourceNotFoundException("The specified extension bundle version does not exist in this bucket."); + } + + // get the dependencies for the bundle version + return getBundleVersion(existingBucket, existingBundle, existingVersion); + } + + private BundleVersion getBundleVersion(final BucketEntity existingBucket, final BundleEntity existingBundle, final BundleVersionEntity existingVersion) { + // get the dependencies for the bundle version + final List existingVersionDependencies = metadataService + .getDependenciesForBundleVersion(existingVersion.getId()); + + // convert the dependency db entities + final Set dependencies = existingVersionDependencies.stream() + .map(d -> ExtensionMappings.map(d)) + .collect(Collectors.toSet()); + + // create the full BundleVersion instance to return + final BundleVersion bundleVersion = new BundleVersion(); + bundleVersion.setVersionMetadata(ExtensionMappings.map(existingVersion)); + bundleVersion.setBundle(ExtensionMappings.map(existingBucket, existingBundle)); + bundleVersion.setBucket(BucketMappings.map(existingBucket)); + bundleVersion.setDependencies(dependencies); + return bundleVersion; + } + + @Override + public void writeBundleVersionContent(final BundleVersion bundleVersion, final OutputStream out) { + // get the content from the persistence provider and write it to the output stream + final BundleVersionCoordinate versionCoordinate = getVersionCoordinate(bundleVersion); + bundlePersistenceProvider.getBundleVersionContent(versionCoordinate, out); + } + + @Override + public BundleVersion deleteBundleVersion(final BundleVersion bundleVersion) { + if (bundleVersion == null) { + throw new IllegalArgumentException("Extension bundle version cannot be null"); + } + + // delete from the metadata db + final String extensionBundleVersionId = bundleVersion.getVersionMetadata().getId(); + metadataService.deleteBundleVersion(extensionBundleVersionId); + + // delete content associated with the bundle version in the persistence provider + final BundleVersionCoordinate versionCoordinate = getVersionCoordinate(bundleVersion); + bundlePersistenceProvider.deleteBundleVersion(versionCoordinate); + + return bundleVersion; + } + + // ------ Extension Methods ---- + + @Override + public SortedSet getExtensionMetadata(final Set bucketIdentifiers, final ExtensionFilterParams filterParams) { + if (bucketIdentifiers == null) { + throw new IllegalArgumentException("Bucket identifiers cannot be null"); + } + + // retrieve the extension entities + final List extensionEntities = metadataService.getExtensions(bucketIdentifiers, filterParams); + return getExtensionMetadata(extensionEntities); + } + + @Override + public SortedSet getExtensionMetadata(final Set bucketIdentifiers, final ProvidedServiceAPI serviceAPI) { + if (bucketIdentifiers == null) { + throw new IllegalArgumentException("Bucket identifiers cannot be null"); + } + + if (serviceAPI == null + || StringUtils.isBlank(serviceAPI.getClassName()) + || StringUtils.isBlank(serviceAPI.getGroupId()) + || StringUtils.isBlank(serviceAPI.getArtifactId()) + || StringUtils.isBlank(serviceAPI.getVersion())) { + throw new IllegalArgumentException("Provided service API must be specified with a class, group, artifact, and version"); + } + + // retrieve the extension entities + final List extensionEntities = metadataService.getExtensionsByProvidedServiceApi(bucketIdentifiers, serviceAPI); + return getExtensionMetadata(extensionEntities); + } + + private SortedSet getExtensionMetadata(List extensionEntities) { + // map to extension metadata and sort by extension name + final SortedSet extensions = new TreeSet<>(); + extensionEntities.forEach(e -> { + final ExtensionMetadata metadata = ExtensionMappings.mapToMetadata(e, extensionSerializer); + extensions.add(metadata); + }); + return extensions; + } + + @Override + public SortedSet getExtensionMetadata(final BundleVersion bundleVersion) { + if (bundleVersion == null) { + throw new IllegalArgumentException("Extension bundle version cannot be null"); + } + + // ensure the bundle version exists + final BundleVersionEntity existingBundleVersion = metadataService.getBundleVersion( + bundleVersion.getVersionMetadata().getBucketId(), + bundleVersion.getBundle().getGroupId(), + bundleVersion.getBundle().getArtifactId(), + bundleVersion.getVersionMetadata().getVersion()); + + if (existingBundleVersion == null) { + LOGGER.warn("The specified extension bundle version does not exist for [{}] - [{}] - [{}] - [{}]", + new Object[]{ + bundleVersion.getVersionMetadata().getBucketId(), + bundleVersion.getBundle().getGroupId(), + bundleVersion.getBundle().getArtifactId(), + bundleVersion.getVersionMetadata().getVersion()}); + throw new ResourceNotFoundException("The specified extension bundle version does not exist."); + } + + // retrieve the extension entities + final List extensionEntities = metadataService.getExtensionsByBundleVersionId(existingBundleVersion.getId()); + + // map to extension and sort by extension name + final SortedSet extensions = new TreeSet<>(); + extensionEntities.forEach(e -> extensions.add(ExtensionMappings.mapToMetadata(e, extensionSerializer))); + return extensions; + } + + @Override + public Extension getExtension(final BundleVersion bundleVersion, final String name) { + if (bundleVersion == null) { + throw new IllegalArgumentException("Bundle version cannot be null"); + } + + if (bundleVersion.getVersionMetadata() == null || StringUtils.isBlank(bundleVersion.getVersionMetadata().getId())) { + throw new IllegalArgumentException("Bundle version must contain a version metadata with a bundle version id"); + } + + if (StringUtils.isBlank(name)) { + throw new IllegalArgumentException("Extension name cannot be null or blank"); + } + + final ExtensionEntity entity = metadataService.getExtensionByName(bundleVersion.getVersionMetadata().getId(), name); + if (entity == null) { + LOGGER.warn("The specified extension [{}] does not exist in the specified bundle version [{}].", + new Object[]{name, bundleVersion.getVersionMetadata().getId()}); + throw new ResourceNotFoundException("The specified extension does not exist in this registry."); + } + + return ExtensionMappings.map(entity, extensionSerializer); + } + + @Override + public void writeExtensionDocs(final BundleVersion bundleVersion, final String name, final OutputStream outputStream) + throws IOException { + if (bundleVersion == null) { + throw new IllegalArgumentException("Bundle version cannot be null"); + } + + if (bundleVersion.getVersionMetadata() == null || StringUtils.isBlank(bundleVersion.getVersionMetadata().getId())) { + throw new IllegalArgumentException("Bundle version must contain a version metadata with a bundle version id"); + } + + if (StringUtils.isBlank(name)) { + throw new IllegalArgumentException("Extension name cannot be null or blank"); + } + + if (outputStream == null) { + throw new IllegalArgumentException("Output stream cannot be null"); + } + + final ExtensionEntity entity = metadataService.getExtensionByName(bundleVersion.getVersionMetadata().getId(), name); + if (entity == null) { + LOGGER.warn("The specified extension [{}] does not exist in the specified bundle version [{}].", + new Object[]{name, bundleVersion.getVersionMetadata().getId()}); + throw new ResourceNotFoundException("The specified extension does not exist in this registry."); + } + + final ExtensionMetadata extensionMetadata = ExtensionMappings.mapToMetadata(entity, extensionSerializer); + final Extension extension = ExtensionMappings.map(entity, extensionSerializer); + extensionDocWriter.write(extensionMetadata, extension, outputStream); + } + + @Override + public void writeAdditionalDetailsDocs(final BundleVersion bundleVersion, final String name, final OutputStream outputStream) throws IOException { + if (bundleVersion == null) { + throw new IllegalArgumentException("Bundle version cannot be null"); + } + + if (bundleVersion.getVersionMetadata() == null || StringUtils.isBlank(bundleVersion.getVersionMetadata().getId())) { + throw new IllegalArgumentException("Bundle version must contain a version metadata with a bundle version id"); + } + + if (StringUtils.isBlank(name)) { + throw new IllegalArgumentException("Extension name cannot be null or blank"); + } + + if (outputStream == null) { + throw new IllegalArgumentException("Output stream cannot be null"); + } + + final ExtensionAdditionalDetailsEntity additionalDetailsEntity = metadataService.getExtensionAdditionalDetails( + bundleVersion.getVersionMetadata().getId(), name); + + if (additionalDetailsEntity == null) { + LOGGER.warn("The specified extension [{}] does not exist in the specified bundle version [{}].", + new Object[]{name, bundleVersion.getVersionMetadata().getId()}); + throw new ResourceNotFoundException("The specified extension does not exist in this registry."); + } + + if (!additionalDetailsEntity.getAdditionalDetails().isPresent()) { + LOGGER.warn("The specified extension [{}] does not have additional details in the specified bundle version [{}].", + new Object[]{name, bundleVersion.getVersionMetadata().getId()}); + throw new IllegalStateException("The specified extension does not have additional details."); + } + + final String additionalDetailsContent = additionalDetailsEntity.getAdditionalDetails().get(); + + // The additional details content may have come from NiFi which has a different path to the css so we need to fix the location + final String componentUsageCssRef = DocumentationConstants.CSS_PATH + "component-usage.css"; + final String updatedContent = additionalDetailsContent.replace("../../../../../css/component-usage.css", componentUsageCssRef); + + IOUtils.write(updatedContent, outputStream, StandardCharsets.UTF_8); + } + + @Override + public SortedSet getExtensionTags() { + final SortedSet tagCounts = new TreeSet<>(); + metadataService.getAllExtensionTags().forEach(tc -> tagCounts.add(ExtensionMappings.map(tc))); + return tagCounts; + } + + // ------ Extension Repository Methods ------- + + @Override + public SortedSet getExtensionRepoBuckets(final Set bucketIds) { + if (bucketIds == null) { + throw new IllegalArgumentException("Bucket ids cannot be null"); + } + + if (bucketIds.isEmpty()) { + return new TreeSet<>(); + } + + final SortedSet repoBuckets = new TreeSet<>(); + + final List buckets = metadataService.getBuckets(bucketIds); + buckets.forEach(b -> { + final ExtensionRepoBucket repoBucket = new ExtensionRepoBucket(); + repoBucket.setBucketName(b.getName()); + repoBuckets.add(repoBucket); + }); + + return repoBuckets; + } + + @Override + public SortedSet getExtensionRepoGroups(final Bucket bucket) { + if (bucket == null) { + throw new IllegalArgumentException("Bucket cannot be null"); + } + + final SortedSet repoGroups = new TreeSet<>(); + + final List bundleEntities = metadataService.getBundlesByBucket(bucket.getIdentifier()); + bundleEntities.forEach(b -> { + final ExtensionRepoGroup repoGroup = new ExtensionRepoGroup(); + repoGroup.setBucketName(bucket.getName()); + repoGroup.setGroupId(b.getGroupId()); + repoGroups.add(repoGroup); + }); + + return repoGroups; + } + + @Override + public SortedSet getExtensionRepoArtifacts(final Bucket bucket, final String groupId) { + if (bucket == null) { + throw new IllegalArgumentException("Bucket cannot be null"); + } + + if (StringUtils.isBlank(groupId)) { + throw new IllegalArgumentException("Group id cannot be null or blank"); + } + + final SortedSet repoArtifacts = new TreeSet<>(); + + final List bundleEntities = metadataService.getBundlesByBucketAndGroup(bucket.getIdentifier(), groupId); + bundleEntities.forEach(b -> { + final ExtensionRepoArtifact repoArtifact = new ExtensionRepoArtifact(); + repoArtifact.setBucketName(bucket.getName()); + repoArtifact.setGroupId(b.getGroupId()); + repoArtifact.setArtifactId(b.getArtifactId()); + repoArtifacts.add(repoArtifact); + }); + + return repoArtifacts; + } + + @Override + public SortedSet getExtensionRepoVersions(final Bucket bucket, final String groupId, final String artifactId) { + if (bucket == null) { + throw new IllegalArgumentException("Bucket cannot be null"); + } + + if (StringUtils.isBlank(groupId)) { + throw new IllegalArgumentException("Group id cannot be null or blank"); + } + + if (StringUtils.isBlank(artifactId)) { + throw new IllegalArgumentException("Artifact id cannot be null or blank"); + } + + final SortedSet repoVersions = new TreeSet<>(); + + final List versionEntities = metadataService.getBundleVersions(bucket.getIdentifier(), groupId, artifactId); + if (!versionEntities.isEmpty()) { + final BundleEntity bundleEntity = metadataService.getBundle(bucket.getIdentifier(), groupId, artifactId); + if (bundleEntity == null) { + // should never happen if the list of versions is not empty, but just in case + throw new ResourceNotFoundException("The specified extension bundle does not exist in this bucket"); + } + + versionEntities.forEach(v -> { + final ExtensionRepoVersionSummary repoVersion = new ExtensionRepoVersionSummary(); + repoVersion.setBucketName(bucket.getName()); + repoVersion.setGroupId(bundleEntity.getGroupId()); + repoVersion.setArtifactId(bundleEntity.getArtifactId()); + repoVersion.setVersion(v.getVersion()); + repoVersion.setAuthor(v.getCreatedBy()); + repoVersion.setTimestamp(v.getCreated().getTime()); + repoVersions.add(repoVersion); + }); + } + + return repoVersions; + } + + // ------ Helper Methods ------- + + private BundleVersionCoordinate getVersionCoordinate(final BundleVersion bundleVersion) { + return getVersionCoordinate(bundleVersion.getBundle(), bundleVersion.getVersionMetadata()); + } + + private BundleVersionCoordinate getVersionCoordinate(final Bundle bundle, final BundleVersionMetadata bundleVersionMetadata) { + final BundleVersionCoordinate versionCoordinate = new StandardBundleVersionCoordinate.Builder() + .bucketId(bundle.getBucketIdentifier()) + .groupId(bundle.getGroupId()) + .artifactId(bundle.getArtifactId()) + .version(bundleVersionMetadata.getVersion()) + .type(getProviderBundleType(bundle.getBundleType())) + .build(); + + return versionCoordinate; + } + + private BucketEntity getBucketEntity(final String bucketIdentifier) { + // ensure the bucket exists + final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier); + if (existingBucket == null) { + LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier); + throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry."); + } + return existingBucket; + } + + private BundleEntity getBundleEntity(final String bundleId) { + final BundleEntity existingBundle = metadataService.getBundle(bundleId); + if (existingBundle == null) { + LOGGER.warn("The specified extension bundle id [{}] does not exist.", bundleId); + throw new ResourceNotFoundException("The specified extension bundle ID does not exist."); + } + return existingBundle; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/docs/DocumentationConstants.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/docs/DocumentationConstants.java new file mode 100644 index 0000000000..8504b247f9 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/docs/DocumentationConstants.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.service.extension.docs; + +public interface DocumentationConstants { + + /** + * The context path for the nifi-registry-docs webapp. + */ + String RESOURCE_PATH = "/nifi-registry-docs"; + + /** + * The path for images in the nifi-registry-docs webapp. + */ + String IMAGE_PATH = RESOURCE_PATH + "/images/"; + + /** + * The path for css in the nifi-registry-docs webapp. + */ + String CSS_PATH = RESOURCE_PATH + "/css/"; + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/docs/ExtensionDocWriter.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/docs/ExtensionDocWriter.java new file mode 100644 index 0000000000..b94da9fd36 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/docs/ExtensionDocWriter.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.service.extension.docs; + +import org.apache.nifi.registry.extension.component.ExtensionMetadata; +import org.apache.nifi.registry.extension.component.manifest.Extension; + +import java.io.IOException; +import java.io.OutputStream; + +public interface ExtensionDocWriter { + + /** + * Generates the documentation for the given Extension and writes it to the given OutputStream. + * + * @param extensionMetadata the metadata for the extension + * @param extension the extension descriptor + * @param outputStream the output stream to write the docs to + * @throws IOException if an error occurs writing the documentation to the given output stream + */ + void write(ExtensionMetadata extensionMetadata, Extension extension, OutputStream outputStream) throws IOException; + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/docs/HtmlExtensionDocWriter.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/docs/HtmlExtensionDocWriter.java new file mode 100644 index 0000000000..63528463cd --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/docs/HtmlExtensionDocWriter.java @@ -0,0 +1,769 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.service.extension.docs; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.extension.bundle.BundleInfo; +import org.apache.nifi.registry.extension.component.ExtensionMetadata; +import org.apache.nifi.registry.extension.component.manifest.AllowableValue; +import org.apache.nifi.registry.extension.component.manifest.ControllerServiceDefinition; +import org.apache.nifi.registry.extension.component.manifest.DeprecationNotice; +import org.apache.nifi.registry.extension.component.manifest.DynamicProperty; +import org.apache.nifi.registry.extension.component.manifest.ExpressionLanguageScope; +import org.apache.nifi.registry.extension.component.manifest.Extension; +import org.apache.nifi.registry.extension.component.manifest.InputRequirement; +import org.apache.nifi.registry.extension.component.manifest.Property; +import org.apache.nifi.registry.extension.component.manifest.ProvidedServiceAPI; +import org.apache.nifi.registry.extension.component.manifest.Restricted; +import org.apache.nifi.registry.extension.component.manifest.Restriction; +import org.apache.nifi.registry.extension.component.manifest.Stateful; +import org.apache.nifi.registry.extension.component.manifest.SystemResourceConsideration; +import org.springframework.stereotype.Service; + +import javax.xml.stream.FactoryConfigurationError; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.apache.nifi.registry.service.extension.docs.DocumentationConstants.CSS_PATH; + +@Service +public class HtmlExtensionDocWriter implements ExtensionDocWriter { + + @Override + public void write(final ExtensionMetadata extensionMetadata, final Extension extension, final OutputStream outputStream) throws IOException { + try { + final XMLStreamWriter xmlStreamWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(outputStream, "UTF-8"); + xmlStreamWriter.writeDTD(""); + xmlStreamWriter.writeStartElement("html"); + xmlStreamWriter.writeAttribute("lang", "en"); + writeHead(extensionMetadata, xmlStreamWriter); + writeBody(extensionMetadata, extension, xmlStreamWriter); + xmlStreamWriter.writeEndElement(); + xmlStreamWriter.close(); + outputStream.flush(); + } catch (XMLStreamException | FactoryConfigurationError e) { + throw new IOException("Unable to create XMLOutputStream", e); + } + } + + private void writeHead(final ExtensionMetadata extensionMetadata, final XMLStreamWriter xmlStreamWriter) throws XMLStreamException { + xmlStreamWriter.writeStartElement("head"); + xmlStreamWriter.writeStartElement("meta"); + xmlStreamWriter.writeAttribute("charset", "utf-8"); + xmlStreamWriter.writeEndElement(); + writeSimpleElement(xmlStreamWriter, "title", extensionMetadata.getDisplayName()); + + final String componentUsageCss = CSS_PATH + "component-usage.css"; + xmlStreamWriter.writeStartElement("link"); + xmlStreamWriter.writeAttribute("rel", "stylesheet"); + xmlStreamWriter.writeAttribute("href", componentUsageCss); + xmlStreamWriter.writeAttribute("type", "text/css"); + xmlStreamWriter.writeEndElement(); + xmlStreamWriter.writeEndElement(); + + xmlStreamWriter.writeStartElement("script"); + xmlStreamWriter.writeAttribute("type", "text/javascript"); + xmlStreamWriter.writeCharacters("window.onload = function(){if(self==top) { " + + "document.getElementById('nameHeader').style.display = \"inherit\"; } }" ); + xmlStreamWriter.writeEndElement(); + } + + private void writeBody(final ExtensionMetadata extensionMetadata, final Extension extension, + final XMLStreamWriter xmlStreamWriter) throws XMLStreamException { + xmlStreamWriter.writeStartElement("body"); + + writeHeader(extensionMetadata, extension, xmlStreamWriter); + writeBundleInfo(extensionMetadata, xmlStreamWriter); + writeDeprecationWarning(extension, xmlStreamWriter); + writeDescription(extensionMetadata, extension, xmlStreamWriter); + writeTags(extension, xmlStreamWriter); + writeProperties(extension, xmlStreamWriter); + writeDynamicProperties(extension, xmlStreamWriter); + writeAdditionalBodyInfo(extension, xmlStreamWriter); + writeStatefulInfo(extension, xmlStreamWriter); + writeRestrictedInfo(extension, xmlStreamWriter); + writeInputRequirementInfo(extension, xmlStreamWriter); + writeSystemResourceConsiderationInfo(extension, xmlStreamWriter); + writeProvidedServiceApis(extension, xmlStreamWriter); + writeSeeAlso(extension, xmlStreamWriter); + + // end body + xmlStreamWriter.writeEndElement(); + } + + /** + * This method may be overridden by sub classes to write additional + * information to the body of the documentation. + * + * @param extension the component to describe + * @param xmlStreamWriter the stream writer + * @throws XMLStreamException thrown if there was a problem writing to the XML stream + */ + protected void writeAdditionalBodyInfo(final Extension extension, final XMLStreamWriter xmlStreamWriter) throws XMLStreamException { + + } + + private void writeHeader(final ExtensionMetadata extensionMetadata, final Extension extension, + final XMLStreamWriter xmlStreamWriter) throws XMLStreamException { + xmlStreamWriter.writeStartElement("h1"); + xmlStreamWriter.writeAttribute("id", "nameHeader"); + xmlStreamWriter.writeAttribute("style", "display: none;"); + xmlStreamWriter.writeCharacters(extensionMetadata.getDisplayName()); + xmlStreamWriter.writeEndElement(); + } + + private void writeBundleInfoString(final ExtensionMetadata extensionMetadata, final XMLStreamWriter xmlStreamWriter) throws XMLStreamException { + final BundleInfo bundleInfo = extensionMetadata.getBundleInfo(); + final String bundleInfoText = bundleInfo.getGroupId() + "-" + bundleInfo.getArtifactId() + "-" + bundleInfo.getVersion(); + xmlStreamWriter.writeStartElement("p"); + xmlStreamWriter.writeStartElement("i"); + xmlStreamWriter.writeCharacters(bundleInfoText); + xmlStreamWriter.writeEndElement(); + xmlStreamWriter.writeEndElement(); + } + + private void writeBundleInfo(final ExtensionMetadata extensionMetadata, final XMLStreamWriter xmlStreamWriter) throws XMLStreamException { + final BundleInfo bundleInfo = extensionMetadata.getBundleInfo(); + + final String extenstionType; + switch (extensionMetadata.getType()) { + case PROCESSOR: + extenstionType = "Processor"; + break; + case CONTROLLER_SERVICE: + extenstionType = "Controller Service"; + break; + case REPORTING_TASK: + extenstionType = "Reporting Task"; + break; + default: + throw new IllegalArgumentException("Unknown extension type: " + extensionMetadata.getType()); + } + + xmlStreamWriter.writeStartElement("table"); + + xmlStreamWriter.writeStartElement("tr"); + writeSimpleElement(xmlStreamWriter, "th", "Extension Info"); + writeSimpleElement(xmlStreamWriter, "th", "Value"); + xmlStreamWriter.writeEndElement(); + + xmlStreamWriter.writeStartElement("tr"); + writeSimpleElement(xmlStreamWriter, "td", "Full Name", true, "bundle-info"); + writeSimpleElement(xmlStreamWriter, "td", extensionMetadata.getName()); + xmlStreamWriter.writeEndElement(); + + xmlStreamWriter.writeStartElement("tr"); + writeSimpleElement(xmlStreamWriter, "td", "Type", true, "bundle-info"); + writeSimpleElement(xmlStreamWriter, "td", extenstionType); + xmlStreamWriter.writeEndElement(); + + xmlStreamWriter.writeStartElement("tr"); + writeSimpleElement(xmlStreamWriter, "td", "Bundle Group", true, "bundle-info"); + writeSimpleElement(xmlStreamWriter, "td", bundleInfo.getGroupId()); + xmlStreamWriter.writeEndElement(); + + xmlStreamWriter.writeStartElement("tr"); + writeSimpleElement(xmlStreamWriter, "td", "Bundle Artifact", true, "bundle-info"); + writeSimpleElement(xmlStreamWriter, "td", bundleInfo.getArtifactId()); + xmlStreamWriter.writeEndElement(); + + xmlStreamWriter.writeStartElement("tr"); + writeSimpleElement(xmlStreamWriter, "td", "Bundle Version", true, "bundle-info"); + writeSimpleElement(xmlStreamWriter, "td", bundleInfo.getVersion()); + xmlStreamWriter.writeEndElement(); + + xmlStreamWriter.writeStartElement("tr"); + writeSimpleElement(xmlStreamWriter, "td", "Bundle Type", true, "bundle-info"); + writeSimpleElement(xmlStreamWriter, "td", bundleInfo.getBundleType().toString()); + xmlStreamWriter.writeEndElement(); + + xmlStreamWriter.writeStartElement("tr"); + writeSimpleElement(xmlStreamWriter, "td", "System API Version", true, "bundle-info"); + writeSimpleElement(xmlStreamWriter, "td", bundleInfo.getSystemApiVersion()); + xmlStreamWriter.writeEndElement(); + + xmlStreamWriter.writeEndElement(); // end table + } + + private void writeDeprecationWarning(final Extension extension, final XMLStreamWriter xmlStreamWriter) throws XMLStreamException { + final DeprecationNotice deprecationNotice = extension.getDeprecationNotice(); + if (deprecationNotice != null) { + xmlStreamWriter.writeStartElement("h2"); + xmlStreamWriter.writeCharacters("Deprecation notice: "); + xmlStreamWriter.writeEndElement(); + + xmlStreamWriter.writeStartElement("p"); + xmlStreamWriter.writeCharacters(""); + if (!StringUtils.isEmpty(deprecationNotice.getReason())) { + xmlStreamWriter.writeCharacters(deprecationNotice.getReason()); + } else { + xmlStreamWriter.writeCharacters("Please be aware this processor is deprecated and may be removed in the near future."); + } + xmlStreamWriter.writeEndElement(); + + xmlStreamWriter.writeStartElement("p"); + xmlStreamWriter.writeCharacters("Please consider using one the following alternatives: "); + + final List alternatives = deprecationNotice.getAlternatives(); + if (alternatives != null && alternatives.size() > 0) { + xmlStreamWriter.writeStartElement("ul"); + for (final String alternative : alternatives) { + xmlStreamWriter.writeStartElement("li"); + xmlStreamWriter.writeCharacters(alternative); + xmlStreamWriter.writeEndElement(); + } + xmlStreamWriter.writeEndElement(); + } else { + xmlStreamWriter.writeCharacters("No alternative components suggested."); + } + + xmlStreamWriter.writeEndElement(); + } + } + + private void writeDescription(final ExtensionMetadata extensionMetadata, final Extension extension, final XMLStreamWriter xmlStreamWriter) + throws XMLStreamException { + final String description = StringUtils.isBlank(extension.getDescription()) + ? "No description provided." : extension.getDescription(); + writeSimpleElement(xmlStreamWriter, "h2", "Description: "); + writeSimpleElement(xmlStreamWriter, "p", description); + + if (extensionMetadata.getHasAdditionalDetails()) { + xmlStreamWriter.writeStartElement("p"); + final BundleInfo bundleInfo = extensionMetadata.getBundleInfo(); + final String bucketName = bundleInfo.getBucketName(); + final String groupId = bundleInfo.getGroupId(); + final String artifactId = bundleInfo.getArtifactId(); + final String version = bundleInfo.getVersion(); + final String extensionName = extensionMetadata.getName(); + + final String additionalDetailsPath = "/nifi-registry-api/extension-repository/" + + bucketName + "/" + groupId + "/" + artifactId + "/" + version + + "/extensions/" + extensionName + "/docs/additional-details"; + + writeLink(xmlStreamWriter, "Additional Details...", additionalDetailsPath); + xmlStreamWriter.writeEndElement(); + } + } + + private void writeTags(final Extension extension, final XMLStreamWriter xmlStreamWriter) throws XMLStreamException { + final List tags = extension.getTags(); + xmlStreamWriter.writeStartElement("h3"); + xmlStreamWriter.writeCharacters("Tags: "); + xmlStreamWriter.writeEndElement(); + xmlStreamWriter.writeStartElement("p"); + if (tags != null) { + final String tagString = StringUtils.join(tags, ", "); + xmlStreamWriter.writeCharacters(tagString); + } else { + xmlStreamWriter.writeCharacters("No tags provided."); + } + xmlStreamWriter.writeEndElement(); + } + + protected void writeProperties(final Extension extension, final XMLStreamWriter xmlStreamWriter) throws XMLStreamException { + + final List properties = extension.getProperties(); + writeSimpleElement(xmlStreamWriter, "h3", "Properties: "); + + if (properties != null && properties.size() > 0) { + final boolean containsExpressionLanguage = containsExpressionLanguage(extension); + final boolean containsSensitiveProperties = containsSensitiveProperties(extension); + xmlStreamWriter.writeStartElement("p"); + xmlStreamWriter.writeCharacters("In the list below, the names of required properties appear in "); + writeSimpleElement(xmlStreamWriter, "strong", "bold"); + xmlStreamWriter.writeCharacters(". Any other properties (not in bold) are considered optional. " + + "The table also indicates any default values"); + if (containsExpressionLanguage) { + if (!containsSensitiveProperties) { + xmlStreamWriter.writeCharacters(", and "); + } else { + xmlStreamWriter.writeCharacters(", "); + } + xmlStreamWriter.writeCharacters("whether a property supports the NiFi Expression Language"); + } + if (containsSensitiveProperties) { + xmlStreamWriter.writeCharacters(", and whether a property is considered " + "\"sensitive\", meaning that its value will be encrypted"); + } + xmlStreamWriter.writeCharacters("."); + xmlStreamWriter.writeEndElement(); + + xmlStreamWriter.writeStartElement("table"); + xmlStreamWriter.writeAttribute("id", "properties"); + + // write the header row + xmlStreamWriter.writeStartElement("tr"); + writeSimpleElement(xmlStreamWriter, "th", "Name"); + writeSimpleElement(xmlStreamWriter, "th", "Default Value"); + writeSimpleElement(xmlStreamWriter, "th", "Allowable Values"); + writeSimpleElement(xmlStreamWriter, "th", "Description"); + xmlStreamWriter.writeEndElement(); + + // write the individual properties + for (Property property : properties) { + xmlStreamWriter.writeStartElement("tr"); + xmlStreamWriter.writeStartElement("td"); + xmlStreamWriter.writeAttribute("id", "name"); + if (property.isRequired()) { + writeSimpleElement(xmlStreamWriter, "strong", property.getDisplayName()); + } else { + xmlStreamWriter.writeCharacters(property.getDisplayName()); + } + + xmlStreamWriter.writeEndElement(); + writeSimpleElement(xmlStreamWriter, "td", property.getDefaultValue(), false, "default-value"); + xmlStreamWriter.writeStartElement("td"); + xmlStreamWriter.writeAttribute("id", "allowable-values"); + writeValidValues(xmlStreamWriter, property); + xmlStreamWriter.writeEndElement(); + xmlStreamWriter.writeStartElement("td"); + xmlStreamWriter.writeAttribute("id", "description"); + if (property.getDescription() != null && property.getDescription().trim().length() > 0) { + xmlStreamWriter.writeCharacters(property.getDescription()); + } else { + xmlStreamWriter.writeCharacters("No Description Provided."); + } + + if (property.isSensitive()) { + xmlStreamWriter.writeEmptyElement("br"); + writeSimpleElement(xmlStreamWriter, "strong", "Sensitive Property: true"); + } + + if (property.isExpressionLanguageSupported()) { + xmlStreamWriter.writeEmptyElement("br"); + String text = "Supports Expression Language: true"; + final String perFF = " (will be evaluated using flow file attributes and variable registry)"; + final String registry = " (will be evaluated using variable registry only)"; + final InputRequirement inputRequirement = extension.getInputRequirement(); + + switch(property.getExpressionLanguageScope()) { + case FLOWFILE_ATTRIBUTES: + if(inputRequirement != null && inputRequirement.equals(InputRequirement.INPUT_FORBIDDEN)) { + text += registry; + } else { + text += perFF; + } + break; + case VARIABLE_REGISTRY: + text += registry; + break; + case NONE: + default: + // in case legacy/deprecated method has been used to specify EL support + text += " (undefined scope)"; + break; + } + + writeSimpleElement(xmlStreamWriter, "strong", text); + } + xmlStreamWriter.writeEndElement(); + + xmlStreamWriter.writeEndElement(); + } + + xmlStreamWriter.writeEndElement(); + + } else { + writeSimpleElement(xmlStreamWriter, "p", "This component has no required or optional properties."); + } + } + + private boolean containsExpressionLanguage(final Extension extension) { + for (Property property : extension.getProperties()) { + if (property.isExpressionLanguageSupported()) { + return true; + } + } + return false; + } + + private boolean containsSensitiveProperties(final Extension extension) { + for (Property property : extension.getProperties()) { + if (property.isSensitive()) { + return true; + } + } + return false; + } + + protected void writeValidValues(final XMLStreamWriter xmlStreamWriter, final Property property) throws XMLStreamException { + if (property.getAllowableValues() != null && property.getAllowableValues().size() > 0) { + xmlStreamWriter.writeStartElement("ul"); + for (AllowableValue value : property.getAllowableValues()) { + xmlStreamWriter.writeStartElement("li"); + xmlStreamWriter.writeCharacters(value.getDisplayName()); + + if (!StringUtils.isBlank(value.getDescription())) { + writeValidValueDescription(xmlStreamWriter, value.getDescription()); + } + xmlStreamWriter.writeEndElement(); + } + xmlStreamWriter.writeEndElement(); + } else if (property.getControllerServiceDefinition() != null) { + final ControllerServiceDefinition serviceDefinition = property.getControllerServiceDefinition(); + final String controllerServiceClass = getSimpleName(serviceDefinition.getClassName()); + + final String group = serviceDefinition.getGroupId() == null ? "unknown" : serviceDefinition.getGroupId(); + final String artifact = serviceDefinition.getArtifactId() == null ? "unknown" : serviceDefinition.getArtifactId(); + final String version = serviceDefinition.getVersion() == null ? "unknown" : serviceDefinition.getVersion(); + + writeSimpleElement(xmlStreamWriter, "strong", "Controller Service API: "); + xmlStreamWriter.writeEmptyElement("br"); + xmlStreamWriter.writeCharacters(controllerServiceClass); + + writeValidValueDescription(xmlStreamWriter, group + "-" + artifact + "-" + version); + +// xmlStreamWriter.writeEmptyElement("br"); +// xmlStreamWriter.writeCharacters(group); +// xmlStreamWriter.writeEmptyElement("br"); +// xmlStreamWriter.writeCharacters(artifact); +// xmlStreamWriter.writeEmptyElement("br"); +// xmlStreamWriter.writeCharacters(version); + } + } + + private String getSimpleName(final String extensionName) { + int index = extensionName.lastIndexOf('.'); + if (index > 0 && (index < (extensionName.length() - 1))) { + return extensionName.substring(index + 1); + } else { + return extensionName; + } + } + + private void writeValidValueDescription(final XMLStreamWriter xmlStreamWriter, final String description) throws XMLStreamException { + xmlStreamWriter.writeCharacters(" "); + xmlStreamWriter.writeStartElement("img"); + xmlStreamWriter.writeAttribute("src", "/nifi-registry-docs/images/iconInfo.png"); + xmlStreamWriter.writeAttribute("alt", description); + xmlStreamWriter.writeAttribute("title", description); + xmlStreamWriter.writeEndElement(); + } + + private void writeDynamicProperties(final Extension extension, final XMLStreamWriter xmlStreamWriter) throws XMLStreamException { + + final List dynamicProperties = extension.getDynamicProperties(); + + if (dynamicProperties != null && dynamicProperties.size() > 0) { + writeSimpleElement(xmlStreamWriter, "h3", "Dynamic Properties: "); + xmlStreamWriter.writeStartElement("p"); + xmlStreamWriter.writeCharacters("Dynamic Properties allow the user to specify both the name and value of a property."); + xmlStreamWriter.writeStartElement("table"); + xmlStreamWriter.writeAttribute("id", "dynamic-properties"); + xmlStreamWriter.writeStartElement("tr"); + writeSimpleElement(xmlStreamWriter, "th", "Name"); + writeSimpleElement(xmlStreamWriter, "th", "Value"); + writeSimpleElement(xmlStreamWriter, "th", "Description"); + xmlStreamWriter.writeEndElement(); + + for (final DynamicProperty dynamicProperty : dynamicProperties) { + final String name = StringUtils.isBlank(dynamicProperty.getName()) ? "Not Specified" : dynamicProperty.getName(); + final String value = StringUtils.isBlank(dynamicProperty.getValue()) ? "Not Specified" : dynamicProperty.getValue(); + final String description = StringUtils.isBlank(dynamicProperty.getDescription()) ? "Not Specified" : dynamicProperty.getDescription(); + + xmlStreamWriter.writeStartElement("tr"); + writeSimpleElement(xmlStreamWriter, "td", name, false, "name"); + writeSimpleElement(xmlStreamWriter, "td", value, false, "value"); + xmlStreamWriter.writeStartElement("td"); + xmlStreamWriter.writeCharacters(description); + xmlStreamWriter.writeEmptyElement("br"); + + final ExpressionLanguageScope elScope = dynamicProperty.getExpressionLanguageScope() == null + ? ExpressionLanguageScope.NONE : dynamicProperty.getExpressionLanguageScope(); + + String text; + if(elScope.equals(ExpressionLanguageScope.NONE)) { + if(dynamicProperty.isExpressionLanguageSupported()) { + text = "Supports Expression Language: true (undefined scope)"; + } else { + text = "Supports Expression Language: false"; + } + } else { + switch(elScope) { + case FLOWFILE_ATTRIBUTES: + text = "Supports Expression Language: true (will be evaluated using flow file attributes and variable registry)"; + break; + case VARIABLE_REGISTRY: + text = "Supports Expression Language: true (will be evaluated using variable registry only)"; + break; + case NONE: + default: + text = "Supports Expression Language: false"; + break; + } + } + + writeSimpleElement(xmlStreamWriter, "strong", text); + xmlStreamWriter.writeEndElement(); + xmlStreamWriter.writeEndElement(); + } + + xmlStreamWriter.writeEndElement(); + xmlStreamWriter.writeEndElement(); + } + } + + private void writeStatefulInfo(final Extension extension, final XMLStreamWriter xmlStreamWriter) + throws XMLStreamException { + final Stateful stateful = extension.getStateful(); + writeSimpleElement(xmlStreamWriter, "h3", "State management: "); + + if(stateful != null) { + final List scopes = Optional.ofNullable(stateful.getScopes()) + .map(List::stream) + .orElseGet(Stream::empty) + .map(s -> s.toString()) + .collect(Collectors.toList()); + + final String description = StringUtils.isBlank(stateful.getDescription()) ? "Not Specified" : stateful.getDescription(); + + xmlStreamWriter.writeStartElement("table"); + xmlStreamWriter.writeAttribute("id", "stateful"); + xmlStreamWriter.writeStartElement("tr"); + writeSimpleElement(xmlStreamWriter, "th", "Scope"); + writeSimpleElement(xmlStreamWriter, "th", "Description"); + xmlStreamWriter.writeEndElement(); + + xmlStreamWriter.writeStartElement("tr"); + writeSimpleElement(xmlStreamWriter, "td", StringUtils.join(scopes, ", ")); + writeSimpleElement(xmlStreamWriter, "td", description); + xmlStreamWriter.writeEndElement(); + + xmlStreamWriter.writeEndElement(); + } else { + xmlStreamWriter.writeCharacters("This component does not store state."); + } + } + + private void writeRestrictedInfo(final Extension extension, final XMLStreamWriter xmlStreamWriter) + throws XMLStreamException { + final Restricted restricted = extension.getRestricted(); + writeSimpleElement(xmlStreamWriter, "h3", "Restricted: "); + + if(restricted != null) { + final String generalRestrictionExplanation = restricted.getGeneralRestrictionExplanation(); + if (!StringUtils.isBlank(generalRestrictionExplanation)) { + xmlStreamWriter.writeCharacters(generalRestrictionExplanation); + } + + final List restrictions = restricted.getRestrictions(); + if (restrictions != null && restrictions.size() > 0) { + xmlStreamWriter.writeStartElement("table"); + xmlStreamWriter.writeAttribute("id", "restrictions"); + xmlStreamWriter.writeStartElement("tr"); + writeSimpleElement(xmlStreamWriter, "th", "Required Permission"); + writeSimpleElement(xmlStreamWriter, "th", "Explanation"); + xmlStreamWriter.writeEndElement(); + + for (Restriction restriction : restrictions) { + final String permission = StringUtils.isBlank(restriction.getRequiredPermission()) + ? "Not Specified" : restriction.getRequiredPermission(); + + final String explanation = StringUtils.isBlank(restriction.getExplanation()) + ? "Not Specified" : restriction.getExplanation(); + + xmlStreamWriter.writeStartElement("tr"); + writeSimpleElement(xmlStreamWriter, "td", permission); + writeSimpleElement(xmlStreamWriter, "td", explanation); + xmlStreamWriter.writeEndElement(); + } + + xmlStreamWriter.writeEndElement(); + } else { + xmlStreamWriter.writeCharacters("This component requires access to restricted components regardless of restriction."); + } + } else { + xmlStreamWriter.writeCharacters("This component is not restricted."); + } + } + + private void writeInputRequirementInfo(final Extension extension, final XMLStreamWriter xmlStreamWriter) + throws XMLStreamException { + final InputRequirement inputRequirement = extension.getInputRequirement(); + if(inputRequirement != null) { + writeSimpleElement(xmlStreamWriter, "h3", "Input requirement: "); + switch (inputRequirement) { + case INPUT_FORBIDDEN: + xmlStreamWriter.writeCharacters("This component does not allow an incoming relationship."); + break; + case INPUT_ALLOWED: + xmlStreamWriter.writeCharacters("This component allows an incoming relationship."); + break; + case INPUT_REQUIRED: + xmlStreamWriter.writeCharacters("This component requires an incoming relationship."); + break; + default: + xmlStreamWriter.writeCharacters("This component does not have input requirement."); + break; + } + } + } + + private void writeSystemResourceConsiderationInfo(final Extension extension, final XMLStreamWriter xmlStreamWriter) + throws XMLStreamException { + + List systemResourceConsiderations = extension.getSystemResourceConsiderations(); + + writeSimpleElement(xmlStreamWriter, "h3", "System Resource Considerations:"); + if (systemResourceConsiderations != null && systemResourceConsiderations.size() > 0) { + xmlStreamWriter.writeStartElement("table"); + xmlStreamWriter.writeAttribute("id", "system-resource-considerations"); + xmlStreamWriter.writeStartElement("tr"); + writeSimpleElement(xmlStreamWriter, "th", "Resource"); + writeSimpleElement(xmlStreamWriter, "th", "Description"); + xmlStreamWriter.writeEndElement(); + + for (SystemResourceConsideration systemResourceConsideration : systemResourceConsiderations) { + final String resource = StringUtils.isBlank(systemResourceConsideration.getResource()) + ? "Not Specified" : systemResourceConsideration.getResource(); + final String description = StringUtils.isBlank(systemResourceConsideration.getDescription()) + ? "Not Specified" : systemResourceConsideration.getDescription(); + + xmlStreamWriter.writeStartElement("tr"); + writeSimpleElement(xmlStreamWriter, "td", resource); + writeSimpleElement(xmlStreamWriter, "td", description); + xmlStreamWriter.writeEndElement(); + } + xmlStreamWriter.writeEndElement(); + + } else { + xmlStreamWriter.writeCharacters("None specified."); + } + } + + private void writeProvidedServiceApis(final Extension extension, final XMLStreamWriter xmlStreamWriter) throws XMLStreamException { + final List serviceAPIS = extension.getProvidedServiceAPIs(); + if (serviceAPIS != null && serviceAPIS.size() > 0) { + writeSimpleElement(xmlStreamWriter, "h3", "Provided Service APIs:"); + + xmlStreamWriter.writeStartElement("ul"); + + for (final ProvidedServiceAPI serviceAPI : serviceAPIS) { + final String name = getSimpleName(serviceAPI.getClassName()); + final String bundleInfo = " (" + serviceAPI.getGroupId() + "-" + serviceAPI.getArtifactId() + "-" + serviceAPI.getVersion() + ")"; + + xmlStreamWriter.writeStartElement("li"); + xmlStreamWriter.writeCharacters(name); + xmlStreamWriter.writeStartElement("i"); + xmlStreamWriter.writeCharacters(bundleInfo); + xmlStreamWriter.writeEndElement(); + xmlStreamWriter.writeEndElement(); + } + + xmlStreamWriter.writeEndElement(); + } + } + + private void writeSeeAlso(final Extension extension, final XMLStreamWriter xmlStreamWriter) + throws XMLStreamException { + final List seeAlsos = extension.getSeeAlso(); + if (seeAlsos != null && seeAlsos.size() > 0) { + writeSimpleElement(xmlStreamWriter, "h3", "See Also:"); + + xmlStreamWriter.writeStartElement("ul"); + for (final String seeAlso : seeAlsos) { + writeSimpleElement(xmlStreamWriter, "li", seeAlso); + } + xmlStreamWriter.writeEndElement(); + } + } + + /** + * Writes a begin element, then text, then end element for the element of a + * users choosing. Example: <p>text</p> + * + * @param writer the stream writer to use + * @param elementName the name of the element + * @param characters the characters to insert into the element + * @throws XMLStreamException thrown if there was a problem writing to the + * stream + */ + protected final static void writeSimpleElement(final XMLStreamWriter writer, final String elementName, + final String characters) throws XMLStreamException { + writeSimpleElement(writer, elementName, characters, false); + } + + /** + * Writes a begin element, then text, then end element for the element of a + * users choosing. Example: <p>text</p> + * + * @param writer the stream writer to use + * @param elementName the name of the element + * @param characters the characters to insert into the element + * @param strong whether the characters should be strong or not. + * @throws XMLStreamException thrown if there was a problem writing to the + * stream. + */ + protected final static void writeSimpleElement(final XMLStreamWriter writer, final String elementName, + final String characters, boolean strong) throws XMLStreamException { + writeSimpleElement(writer, elementName, characters, strong, null); + } + + /** + * Writes a begin element, an id attribute(if specified), then text, then + * end element for element of the users choosing. Example: <p + * id="p-id">text</p> + * + * @param writer the stream writer to use + * @param elementName the name of the element + * @param characters the text of the element + * @param strong whether to bold the text of the element or not + * @param id the id of the element. specifying null will cause no element to + * be written. + * @throws XMLStreamException xse + */ + protected final static void writeSimpleElement(final XMLStreamWriter writer, final String elementName, + final String characters, boolean strong, String id) throws XMLStreamException { + writer.writeStartElement(elementName); + if (id != null) { + writer.writeAttribute("id", id); + } + if (strong) { + writer.writeStartElement("strong"); + } + writer.writeCharacters(characters); + if (strong) { + writer.writeEndElement(); + } + writer.writeEndElement(); + } + + /** + * A helper method to write a link + * + * @param xmlStreamWriter the stream to write to + * @param text the text of the link + * @param location the location of the link + * @throws XMLStreamException thrown if there was a problem writing to the + * stream + */ + protected void writeLink(final XMLStreamWriter xmlStreamWriter, final String text, final String location) + throws XMLStreamException { + xmlStreamWriter.writeStartElement("a"); + xmlStreamWriter.writeAttribute("href", location); + xmlStreamWriter.writeCharacters(text); + xmlStreamWriter.writeEndElement(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/mapper/BucketMappings.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/mapper/BucketMappings.java new file mode 100644 index 0000000000..542cc1b452 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/mapper/BucketMappings.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.service.mapper; + +import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.db.entity.BucketEntity; + +import java.util.Date; + +/** + * Mappings between bucket DB entities and data model. + */ +public class BucketMappings { + + public static BucketEntity map(final Bucket bucket) { + final BucketEntity bucketEntity = new BucketEntity(); + bucketEntity.setId(bucket.getIdentifier()); + bucketEntity.setName(bucket.getName()); + bucketEntity.setDescription(bucket.getDescription()); + bucketEntity.setCreated(new Date(bucket.getCreatedTimestamp())); + bucketEntity.setAllowExtensionBundleRedeploy(bucket.isAllowBundleRedeploy()); + bucketEntity.setAllowPublicRead(bucket.isAllowPublicRead()); + return bucketEntity; + } + + public static Bucket map(final BucketEntity bucketEntity) { + final Bucket bucket = new Bucket(); + bucket.setIdentifier(bucketEntity.getId()); + bucket.setName(bucketEntity.getName()); + bucket.setDescription(bucketEntity.getDescription()); + bucket.setCreatedTimestamp(bucketEntity.getCreated().getTime()); + bucket.setAllowBundleRedeploy(bucketEntity.isAllowExtensionBundleRedeploy()); + bucket.setAllowPublicRead(bucketEntity.isAllowPublicRead()); + return bucket; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/mapper/ExtensionMappings.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/mapper/ExtensionMappings.java new file mode 100644 index 0000000000..c6559517df --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/mapper/ExtensionMappings.java @@ -0,0 +1,308 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.service.mapper; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.db.entity.BucketEntity; +import org.apache.nifi.registry.db.entity.BucketItemEntityType; +import org.apache.nifi.registry.db.entity.BundleEntity; +import org.apache.nifi.registry.db.entity.BundleVersionDependencyEntity; +import org.apache.nifi.registry.db.entity.BundleVersionEntity; +import org.apache.nifi.registry.db.entity.ExtensionEntity; +import org.apache.nifi.registry.db.entity.ExtensionProvidedServiceApiEntity; +import org.apache.nifi.registry.db.entity.ExtensionRestrictionEntity; +import org.apache.nifi.registry.db.entity.TagCountEntity; +import org.apache.nifi.registry.extension.bundle.BuildInfo; +import org.apache.nifi.registry.extension.bundle.Bundle; +import org.apache.nifi.registry.extension.bundle.BundleInfo; +import org.apache.nifi.registry.extension.bundle.BundleVersionDependency; +import org.apache.nifi.registry.extension.bundle.BundleVersionMetadata; +import org.apache.nifi.registry.extension.component.ExtensionMetadata; +import org.apache.nifi.registry.extension.component.TagCount; +import org.apache.nifi.registry.extension.component.manifest.Extension; +import org.apache.nifi.registry.extension.component.manifest.ProvidedServiceAPI; +import org.apache.nifi.registry.extension.component.manifest.Restriction; +import org.apache.nifi.registry.serialization.SerializationException; +import org.apache.nifi.registry.serialization.Serializer; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Date; +import java.util.stream.Collectors; + +/** + * Mappings between Extension related DB entities and data model. + */ +public class ExtensionMappings { + + // -- Map Bundle + + public static BundleEntity map(final Bundle bundle) { + final BundleEntity entity = new BundleEntity(); + entity.setId(bundle.getIdentifier()); + entity.setName(bundle.getName()); + entity.setDescription(bundle.getDescription()); + entity.setCreated(new Date(bundle.getCreatedTimestamp())); + entity.setModified(new Date(bundle.getModifiedTimestamp())); + entity.setType(BucketItemEntityType.BUNDLE); + entity.setBucketId(bundle.getBucketIdentifier()); + + entity.setGroupId(bundle.getGroupId()); + entity.setArtifactId(bundle.getArtifactId()); + entity.setBundleType(bundle.getBundleType()); + return entity; + } + + public static Bundle map(final BucketEntity bucketEntity, final BundleEntity bundleEntity) { + final Bundle bundle = new Bundle(); + bundle.setIdentifier(bundleEntity.getId()); + bundle.setName(bundleEntity.getName()); + bundle.setDescription(bundleEntity.getDescription()); + bundle.setCreatedTimestamp(bundleEntity.getCreated().getTime()); + bundle.setModifiedTimestamp(bundleEntity.getModified().getTime()); + bundle.setBucketIdentifier(bundleEntity.getBucketId()); + + if (bucketEntity != null) { + bundle.setBucketName(bucketEntity.getName()); + } else { + bundle.setBucketName(bundleEntity.getBucketName()); + } + + bundle.setGroupId(bundleEntity.getGroupId()); + bundle.setArtifactId(bundleEntity.getArtifactId()); + bundle.setBundleType(bundleEntity.getBundleType()); + bundle.setVersionCount(bundleEntity.getVersionCount()); + return bundle; + } + + // -- Map BundleVersion + + public static BundleVersionEntity map(final BundleVersionMetadata bundleVersionMetadata) { + final BundleVersionEntity entity = new BundleVersionEntity(); + entity.setId(bundleVersionMetadata.getId()); + entity.setBundleId(bundleVersionMetadata.getBundleId()); + entity.setBucketId(bundleVersionMetadata.getBucketId()); + entity.setVersion(bundleVersionMetadata.getVersion()); + entity.setCreated(new Date(bundleVersionMetadata.getTimestamp())); + entity.setCreatedBy(bundleVersionMetadata.getAuthor()); + entity.setDescription(bundleVersionMetadata.getDescription()); + entity.setSha256Hex(bundleVersionMetadata.getSha256()); + entity.setSha256Supplied(bundleVersionMetadata.getSha256Supplied()); + entity.setContentSize(bundleVersionMetadata.getContentSize()); + entity.setSystemApiVersion(bundleVersionMetadata.getSystemApiVersion()); + + final BuildInfo buildInfo = bundleVersionMetadata.getBuildInfo(); + entity.setBuildTool(buildInfo.getBuildTool()); + entity.setBuildFlags(buildInfo.getBuildFlags()); + entity.setBuildBranch(buildInfo.getBuildBranch()); + entity.setBuildTag(buildInfo.getBuildTag()); + entity.setBuildRevision(buildInfo.getBuildRevision()); + entity.setBuiltBy(buildInfo.getBuiltBy()); + entity.setBuilt(new Date(buildInfo.getBuilt())); + + return entity; + } + + public static BundleVersionMetadata map(final BundleVersionEntity bundleVersionEntity) { + final BundleVersionMetadata bundleVersionMetadata = new BundleVersionMetadata(); + bundleVersionMetadata.setId(bundleVersionEntity.getId()); + bundleVersionMetadata.setBundleId(bundleVersionEntity.getBundleId()); + bundleVersionMetadata.setBucketId(bundleVersionEntity.getBucketId()); + bundleVersionMetadata.setGroupId(bundleVersionEntity.getGroupId()); + bundleVersionMetadata.setArtifactId(bundleVersionEntity.getArtifactId()); + bundleVersionMetadata.setVersion(bundleVersionEntity.getVersion()); + bundleVersionMetadata.setTimestamp(bundleVersionEntity.getCreated().getTime()); + bundleVersionMetadata.setAuthor(bundleVersionEntity.getCreatedBy()); + bundleVersionMetadata.setDescription(bundleVersionEntity.getDescription()); + bundleVersionMetadata.setSha256(bundleVersionEntity.getSha256Hex()); + bundleVersionMetadata.setSha256Supplied(bundleVersionEntity.getSha256Supplied()); + bundleVersionMetadata.setContentSize(bundleVersionEntity.getContentSize()); + bundleVersionMetadata.setSystemApiVersion(bundleVersionEntity.getSystemApiVersion()); + + final BuildInfo buildInfo = new BuildInfo(); + buildInfo.setBuildTool(bundleVersionEntity.getBuildTool()); + buildInfo.setBuildFlags(bundleVersionEntity.getBuildFlags()); + buildInfo.setBuildBranch(bundleVersionEntity.getBuildBranch()); + buildInfo.setBuildTag(bundleVersionEntity.getBuildTag()); + buildInfo.setBuildRevision(bundleVersionEntity.getBuildRevision()); + buildInfo.setBuiltBy(bundleVersionEntity.getBuiltBy()); + buildInfo.setBuilt(bundleVersionEntity.getBuilt().getTime()); + bundleVersionMetadata.setBuildInfo(buildInfo); + + return bundleVersionMetadata; + } + + // -- Map BundleVersionDependency + + public static BundleVersionDependencyEntity map(final BundleVersionDependency bundleVersionDependency) { + final BundleVersionDependencyEntity entity = new BundleVersionDependencyEntity(); + entity.setGroupId(bundleVersionDependency.getGroupId()); + entity.setArtifactId(bundleVersionDependency.getArtifactId()); + entity.setVersion(bundleVersionDependency.getVersion()); + return entity; + } + + public static BundleVersionDependency map(final BundleVersionDependencyEntity dependencyEntity) { + final BundleVersionDependency dependency = new BundleVersionDependency(); + dependency.setGroupId(dependencyEntity.getGroupId()); + dependency.setArtifactId(dependencyEntity.getArtifactId()); + dependency.setVersion(dependencyEntity.getVersion()); + return dependency; + } + + // -- Map Extension + + public static ExtensionEntity map(final Extension extension, final Serializer extensionSerializer) { + final String extensionContent; + try (final ByteArrayOutputStream out = new ByteArrayOutputStream()) { + extensionSerializer.serialize(extension, out); + extensionContent = new String(out.toByteArray(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new SerializationException("Unable to serialize extension", e); + } + + final ExtensionEntity entity = new ExtensionEntity(); + entity.setName(extension.getName()); + + // determine the display name which is the last part of the name after the last separator + final String fullName = entity.getName(); + if (fullName != null) { + final int index = fullName.lastIndexOf('.'); + if (index > 0 && (index < (fullName.length() - 1))) { + entity.setDisplayName(fullName.substring(index + 1)); + } + } + + // if displayName still isn't set, then set it to the full name + if (StringUtils.isBlank(entity.getDisplayName())) { + entity.setDisplayName(extension.getName()); + } + + entity.setExtensionType(extension.getType()); + entity.setContent(extensionContent); + + if (extension.getTags() != null) { + entity.setTags(extension.getTags().stream().collect(Collectors.toSet())); + } + + if (extension.getProvidedServiceAPIs() != null) { + entity.setProvidedServiceApis(extension.getProvidedServiceAPIs().stream() + .map(p -> map(p)) + .collect(Collectors.toSet())); + } else { + entity.setProvidedServiceApis(Collections.emptySet()); + } + + if (extension.getRestricted() != null) { + if (extension.getRestricted().getRestrictions() != null) { + entity.setRestrictions(extension.getRestricted().getRestrictions().stream() + .map(r -> map(r)) + .collect(Collectors.toSet())); + } + } else { + entity.setRestrictions(Collections.emptySet()); + } + + return entity; + } + + public static Extension map(final ExtensionEntity entity, final Serializer extensionSerializer) { + final byte[] content = entity.getContent().getBytes(StandardCharsets.UTF_8); + try (final ByteArrayInputStream input = new ByteArrayInputStream(content)) { + return extensionSerializer.deserialize(input); + } catch (IOException e) { + throw new SerializationException("Unable to deserialize extension", e); + } + } + + // -- Map ExtensionMetadata + + public static ExtensionMetadata mapToMetadata(final ExtensionEntity entity, final Serializer extensionSerializer) { + final Extension extension = map(entity, extensionSerializer); + + final BundleInfo bundleInfo = new BundleInfo(); + bundleInfo.setBucketId(entity.getBucketId()); + bundleInfo.setBucketName(entity.getBucketName()); + bundleInfo.setBundleId(entity.getBundleId()); + bundleInfo.setGroupId(entity.getGroupId()); + bundleInfo.setArtifactId(entity.getArtifactId()); + bundleInfo.setVersion(entity.getVersion()); + bundleInfo.setBundleType(entity.getBundleType()); + bundleInfo.setSystemApiVersion(entity.getSystemApiVersion()); + + final ExtensionMetadata metadata = new ExtensionMetadata(); + metadata.setName(extension.getName()); + metadata.setDisplayName(entity.getDisplayName()); + metadata.setType(extension.getType()); + metadata.setDescription(extension.getDescription()); + metadata.setDeprecationNotice(extension.getDeprecationNotice()); + metadata.setRestricted(extension.getRestricted()); + metadata.setProvidedServiceAPIs(extension.getProvidedServiceAPIs()); + metadata.setTags(extension.getTags()); + metadata.setBundleInfo(bundleInfo); + metadata.setHasAdditionalDetails(entity.getHasAdditionalDetails()); + return metadata; + } + + // -- Map ProvidedServiceAPI + + public static ExtensionProvidedServiceApiEntity map(final ProvidedServiceAPI providedServiceApi) { + final ExtensionProvidedServiceApiEntity entity = new ExtensionProvidedServiceApiEntity(); + entity.setClassName(providedServiceApi.getClassName()); + entity.setGroupId(providedServiceApi.getGroupId()); + entity.setArtifactId(providedServiceApi.getArtifactId()); + entity.setVersion(providedServiceApi.getVersion()); + return entity; + } + + public static ProvidedServiceAPI map(final ExtensionProvidedServiceApiEntity entity) { + final ProvidedServiceAPI providedServiceApi = new ProvidedServiceAPI(); + providedServiceApi.setClassName(entity.getClassName()); + providedServiceApi.setGroupId(entity.getGroupId()); + providedServiceApi.setArtifactId(entity.getArtifactId()); + providedServiceApi.setVersion(entity.getVersion()); + return providedServiceApi; + } + + // -- Map Restriction + + public static ExtensionRestrictionEntity map(final Restriction restriction) { + final ExtensionRestrictionEntity restrictionEntity = new ExtensionRestrictionEntity(); + restrictionEntity.setRequiredPermission(restriction.getRequiredPermission()); + restrictionEntity.setExplanation(restriction.getExplanation()); + return restrictionEntity; + } + + public static Restriction map(final ExtensionRestrictionEntity restrictionEntity) { + final Restriction restriction = new Restriction(); + restriction.setRequiredPermission(restrictionEntity.getRequiredPermission()); + restriction.setExplanation(restrictionEntity.getExplanation()); + return restriction; + } + + // -- Map TagCount + + public static TagCount map(final TagCountEntity entity) { + final TagCount tagCount = new TagCount(); + tagCount.setTag(entity.getTag()); + tagCount.setCount(entity.getCount()); + return tagCount; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/mapper/FlowMappings.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/mapper/FlowMappings.java new file mode 100644 index 0000000000..ed67a56fd3 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/mapper/FlowMappings.java @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.service.mapper; + +import org.apache.nifi.registry.db.entity.BucketEntity; +import org.apache.nifi.registry.db.entity.BucketItemEntityType; +import org.apache.nifi.registry.db.entity.FlowEntity; +import org.apache.nifi.registry.db.entity.FlowSnapshotEntity; +import org.apache.nifi.registry.diff.ComponentDifference; +import org.apache.nifi.registry.diff.ComponentDifferenceGroup; +import org.apache.nifi.registry.flow.VersionedComponent; +import org.apache.nifi.registry.flow.VersionedFlow; +import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata; +import org.apache.nifi.registry.flow.diff.FlowDifference; + +import java.util.Date; + +/** + * Mappings between flow related DB entities and data model. + */ +public class FlowMappings { + + // --- Map flows + + public static FlowEntity map(final VersionedFlow versionedFlow) { + final FlowEntity flowEntity = new FlowEntity(); + flowEntity.setId(versionedFlow.getIdentifier()); + flowEntity.setName(versionedFlow.getName()); + flowEntity.setDescription(versionedFlow.getDescription()); + flowEntity.setCreated(new Date(versionedFlow.getCreatedTimestamp())); + flowEntity.setModified(new Date(versionedFlow.getModifiedTimestamp())); + flowEntity.setType(BucketItemEntityType.FLOW); + return flowEntity; + } + + public static VersionedFlow map(final BucketEntity bucketEntity, final FlowEntity flowEntity) { + final VersionedFlow versionedFlow = new VersionedFlow(); + versionedFlow.setIdentifier(flowEntity.getId()); + versionedFlow.setBucketIdentifier(flowEntity.getBucketId()); + versionedFlow.setName(flowEntity.getName()); + versionedFlow.setDescription(flowEntity.getDescription()); + versionedFlow.setCreatedTimestamp(flowEntity.getCreated().getTime()); + versionedFlow.setModifiedTimestamp(flowEntity.getModified().getTime()); + versionedFlow.setVersionCount(flowEntity.getSnapshotCount()); + + if (bucketEntity != null) { + versionedFlow.setBucketName(bucketEntity.getName()); + } else { + versionedFlow.setBucketName(flowEntity.getBucketName()); + } + + return versionedFlow; + } + + // --- Map snapshots + + public static FlowSnapshotEntity map(final VersionedFlowSnapshotMetadata versionedFlowSnapshot) { + final FlowSnapshotEntity flowSnapshotEntity = new FlowSnapshotEntity(); + flowSnapshotEntity.setFlowId(versionedFlowSnapshot.getFlowIdentifier()); + flowSnapshotEntity.setVersion(versionedFlowSnapshot.getVersion()); + flowSnapshotEntity.setComments(versionedFlowSnapshot.getComments()); + flowSnapshotEntity.setCreated(new Date(versionedFlowSnapshot.getTimestamp())); + flowSnapshotEntity.setCreatedBy(versionedFlowSnapshot.getAuthor()); + return flowSnapshotEntity; + } + + public static VersionedFlowSnapshotMetadata map(final BucketEntity bucketEntity, final FlowSnapshotEntity flowSnapshotEntity) { + final VersionedFlowSnapshotMetadata metadata = new VersionedFlowSnapshotMetadata(); + metadata.setFlowIdentifier(flowSnapshotEntity.getFlowId()); + metadata.setVersion(flowSnapshotEntity.getVersion()); + metadata.setComments(flowSnapshotEntity.getComments()); + metadata.setTimestamp(flowSnapshotEntity.getCreated().getTime()); + metadata.setAuthor(flowSnapshotEntity.getCreatedBy()); + + if (bucketEntity != null) { + metadata.setBucketIdentifier(bucketEntity.getId()); + } + + return metadata; + } + + // --- Flow Differences + + public static ComponentDifference map(final FlowDifference flowDifference){ + ComponentDifference diff = new ComponentDifference(); + diff.setChangeDescription(flowDifference.getDescription()); + diff.setDifferenceType(flowDifference.getDifferenceType().toString()); + diff.setDifferenceTypeDescription(flowDifference.getDifferenceType().getDescription()); + diff.setValueA(getValueDescription(flowDifference.getValueA())); + diff.setValueB(getValueDescription(flowDifference.getValueB())); + return diff; + } + + public static ComponentDifferenceGroup map(VersionedComponent versionedComponent){ + ComponentDifferenceGroup grouping = new ComponentDifferenceGroup(); + grouping.setComponentId(versionedComponent.getIdentifier()); + grouping.setComponentName(versionedComponent.getName()); + grouping.setProcessGroupId(versionedComponent.getGroupIdentifier()); + grouping.setComponentType(versionedComponent.getComponentType().getTypeName()); + return grouping; + } + + private static String getValueDescription(Object valueA){ + if(valueA instanceof VersionedComponent){ + return ((VersionedComponent) valueA).getIdentifier(); + } + if(valueA!= null){ + return valueA.toString(); + } + return null; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/mapper/KeyMappings.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/mapper/KeyMappings.java new file mode 100644 index 0000000000..362c9c47dd --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/mapper/KeyMappings.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.service.mapper; + +import org.apache.nifi.registry.db.entity.KeyEntity; +import org.apache.nifi.registry.security.key.Key; + +/** + * Mappings between key DB entities and data model. + */ +public class KeyMappings { + + public static Key map(final KeyEntity keyEntity) { + final Key key = new Key(); + key.setId(keyEntity.getId()); + key.setIdentity(keyEntity.getTenantIdentity()); + key.setKey(keyEntity.getKeyValue()); + return key; + } + + public static KeyEntity map(final Key key) { + final KeyEntity keyEntity = new KeyEntity(); + keyEntity.setId(key.getId()); + keyEntity.setTenantIdentity(key.getIdentity()); + keyEntity.setKeyValue(key.getKey()); + return keyEntity; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.extension.BundlePersistenceProvider b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.extension.BundlePersistenceProvider new file mode 100644 index 0000000000..9f762fee0f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.extension.BundlePersistenceProvider @@ -0,0 +1,15 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +org.apache.nifi.registry.provider.extension.FileSystemBundlePersistenceProvider \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.flow.FlowPersistenceProvider b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.flow.FlowPersistenceProvider new file mode 100644 index 0000000000..df57a73bca --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.flow.FlowPersistenceProvider @@ -0,0 +1,17 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +org.apache.nifi.registry.provider.flow.FileSystemFlowPersistenceProvider +org.apache.nifi.registry.provider.flow.git.GitFlowPersistenceProvider +org.apache.nifi.registry.provider.flow.DatabaseFlowPersistenceProvider \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.hook.EventHookProvider b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.hook.EventHookProvider new file mode 100644 index 0000000000..2676a35de2 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.hook.EventHookProvider @@ -0,0 +1,16 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +org.apache.nifi.registry.provider.hook.ScriptEventHookProvider +org.apache.nifi.registry.provider.hook.LoggingEventHookProvider \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authentication.IdentityProvider b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authentication.IdentityProvider new file mode 100644 index 0000000000..530528f615 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authentication.IdentityProvider @@ -0,0 +1,15 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +org.apache.nifi.registry.security.ldap.LdapIdentityProvider \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authorization.AccessPolicyProvider b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authorization.AccessPolicyProvider new file mode 100644 index 0000000000..29d7175ef5 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authorization.AccessPolicyProvider @@ -0,0 +1,16 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider +org.apache.nifi.registry.security.authorization.database.DatabaseAccessPolicyProvider \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authorization.Authorizer b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authorization.Authorizer new file mode 100644 index 0000000000..b564fbb62b --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authorization.Authorizer @@ -0,0 +1,16 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer +org.apache.nifi.registry.security.authorization.file.FileAuthorizer \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authorization.UserGroupProvider b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authorization.UserGroupProvider new file mode 100644 index 0000000000..73ec0cf54c --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authorization.UserGroupProvider @@ -0,0 +1,20 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +org.apache.nifi.registry.security.authorization.CompositeUserGroupProvider +org.apache.nifi.registry.security.authorization.CompositeConfigurableUserGroupProvider +org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider +org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider +org.apache.nifi.registry.security.authorization.database.DatabaseUserGroupProvider +org.apache.nifi.registry.security.authorization.shell.ShellUserGroupProvider \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/default/V2__Initial.sql b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/default/V2__Initial.sql new file mode 100644 index 0000000000..b992d236a2 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/default/V2__Initial.sql @@ -0,0 +1,60 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You 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. + +-- The NAME column has a max size of 768 because this is the largest size that MySQL allows when using a unique constraint. +CREATE TABLE BUCKET ( + ID VARCHAR(50) NOT NULL, + NAME VARCHAR(1000) NOT NULL, + DESCRIPTION TEXT, + CREATED TIMESTAMP NOT NULL, + CONSTRAINT PK__BUCKET_ID PRIMARY KEY (ID), + CONSTRAINT UNIQUE__BUCKET_NAME UNIQUE (NAME) +); + +CREATE TABLE BUCKET_ITEM ( + ID VARCHAR(50) NOT NULL, + NAME VARCHAR(1000) NOT NULL, + DESCRIPTION TEXT, + CREATED TIMESTAMP NOT NULL, + MODIFIED TIMESTAMP NOT NULL, + ITEM_TYPE VARCHAR(50) NOT NULL, + BUCKET_ID VARCHAR(50) NOT NULL, + CONSTRAINT PK__BUCKET_ITEM_ID PRIMARY KEY (ID), + CONSTRAINT FK__BUCKET_ITEM_BUCKET_ID FOREIGN KEY (BUCKET_ID) REFERENCES BUCKET(ID) +); + +CREATE TABLE FLOW ( + ID VARCHAR(50) NOT NULL, + CONSTRAINT PK__FLOW_ID PRIMARY KEY (ID), + CONSTRAINT FK__FLOW_BUCKET_ITEM_ID FOREIGN KEY (ID) REFERENCES BUCKET_ITEM(ID) +); + +CREATE TABLE FLOW_SNAPSHOT ( + FLOW_ID VARCHAR(50) NOT NULL, + VERSION INT NOT NULL, + CREATED TIMESTAMP NOT NULL, + CREATED_BY VARCHAR(4096) NOT NULL, + COMMENTS TEXT, + CONSTRAINT PK__FLOW_SNAPSHOT_FLOW_ID_AND_VERSION PRIMARY KEY (FLOW_ID, VERSION), + CONSTRAINT FK__FLOW_SNAPSHOT_FLOW_ID FOREIGN KEY (FLOW_ID) REFERENCES FLOW(ID) +); + +CREATE TABLE SIGNING_KEY ( + ID VARCHAR(50) NOT NULL, + TENANT_IDENTITY VARCHAR(4096) NOT NULL, + KEY_VALUE VARCHAR(50) NOT NULL, + CONSTRAINT PK__SIGNING_KEY_ID PRIMARY KEY (ID), + CONSTRAINT UNIQUE__SIGNING_KEY_TENANT_IDENTITY UNIQUE (TENANT_IDENTITY) +); \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/default/V3__AddExtensions.sql b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/default/V3__AddExtensions.sql new file mode 100644 index 0000000000..3bd982031c --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/default/V3__AddExtensions.sql @@ -0,0 +1,105 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You 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. + +CREATE TABLE BUNDLE ( + ID VARCHAR(50) NOT NULL, + BUCKET_ID VARCHAR(50) NOT NULL, + BUNDLE_TYPE VARCHAR(200) NOT NULL, + GROUP_ID VARCHAR(500) NOT NULL, + ARTIFACT_ID VARCHAR(500) NOT NULL, + CONSTRAINT PK__EXTENSION_BUNDLE_ID PRIMARY KEY (ID), + CONSTRAINT FK__EXTENSION_BUNDLE_BUCKET_ITEM_ID FOREIGN KEY (ID) REFERENCES BUCKET_ITEM(ID) ON DELETE CASCADE, + CONSTRAINT FK__EXTENSION_BUNDLE_BUCKET_ID FOREIGN KEY(BUCKET_ID) REFERENCES BUCKET(ID) ON DELETE CASCADE, + CONSTRAINT UNIQUE__EXTENSION_BUNDLE_BUCKET_GROUP_ARTIFACT UNIQUE (BUCKET_ID, GROUP_ID, ARTIFACT_ID) +); + +CREATE TABLE BUNDLE_VERSION ( + ID VARCHAR(50) NOT NULL, + BUNDLE_ID VARCHAR(50) NOT NULL, + VERSION VARCHAR(100) NOT NULL, + CREATED TIMESTAMP NOT NULL, + CREATED_BY VARCHAR(4096) NOT NULL, + DESCRIPTION TEXT, + SHA_256_HEX VARCHAR(512) NOT NULL, + SHA_256_SUPPLIED INT NOT NULL, + CONTENT_SIZE BIGINT NOT NULL, + SYSTEM_API_VERSION VARCHAR(50), + BUILD_TOOL VARCHAR(100), + BUILD_FLAGS VARCHAR(100), + BUILD_BRANCH VARCHAR(200), + BUILD_TAG VARCHAR(200), + BUILD_REVISION VARCHAR(100), + BUILT TIMESTAMP, + BUILT_BY VARCHAR(4096), + CONSTRAINT PK__BUNDLE_VERSION_ID PRIMARY KEY (ID), + CONSTRAINT FK__BUNDLE_VERSION_BUNDLE_ID FOREIGN KEY (BUNDLE_ID) REFERENCES BUNDLE(ID) ON DELETE CASCADE, + CONSTRAINT UNIQUE__BUNDLE_VERSION_BUNDLE_ID_VERSION UNIQUE (BUNDLE_ID, VERSION) +); + +CREATE TABLE BUNDLE_VERSION_DEPENDENCY ( + ID VARCHAR(50) NOT NULL, + BUNDLE_VERSION_ID VARCHAR(50) NOT NULL, + GROUP_ID VARCHAR(500) NOT NULL, + ARTIFACT_ID VARCHAR(500) NOT NULL, + VERSION VARCHAR(100) NOT NULL, + CONSTRAINT PK__BUNDLE_VERSION_DEPENDENCY_ID PRIMARY KEY (ID), + CONSTRAINT FK__BUNDLE_VERSION_DEPENDENCY_BUNDLE_VERSION_ID FOREIGN KEY (BUNDLE_VERSION_ID) REFERENCES BUNDLE_VERSION(ID) ON DELETE CASCADE, + CONSTRAINT UNIQUE__BUNDLE_VERSION_DEPENDENCY_BUNDLE_ID_GROUP_ARTIFACT_VERSION UNIQUE (BUNDLE_VERSION_ID, GROUP_ID, ARTIFACT_ID, VERSION) +); + +CREATE TABLE EXTENSION ( + ID VARCHAR(50) NOT NULL, + BUNDLE_VERSION_ID VARCHAR(50) NOT NULL, + NAME VARCHAR(500) NOT NULL, + DISPLAY_NAME VARCHAR(500) NOT NULL, + TYPE VARCHAR(100) NOT NULL, + CONTENT TEXT NOT NULL, + ADDITIONAL_DETAILS TEXT, + HAS_ADDITIONAL_DETAILS INT NOT NULL, + CONSTRAINT PK__EXTENSION_ID PRIMARY KEY (ID), + CONSTRAINT FK__EXTENSION_BUNDLE_VERSION_ID FOREIGN KEY (BUNDLE_VERSION_ID) REFERENCES BUNDLE_VERSION(ID) ON DELETE CASCADE, + CONSTRAINT UNIQUE__EXTENSION_BUNDLE_VERSION_ID_AND_NAME UNIQUE (BUNDLE_VERSION_ID, NAME) +); + +CREATE TABLE EXTENSION_PROVIDED_SERVICE_API ( + ID VARCHAR(50) NOT NULL, + EXTENSION_ID VARCHAR(50) NOT NULL, + CLASS_NAME VARCHAR (500) NOT NULL, + GROUP_ID VARCHAR(500) NOT NULL, + ARTIFACT_ID VARCHAR(500) NOT NULL, + VERSION VARCHAR(100) NOT NULL, + CONSTRAINT PK__EXTENSION_PROVIDED_SERVICE_API_ID PRIMARY KEY (ID), + CONSTRAINT FK__EXTENSION_PROVIDED_SERVICE_API_EXTENSION_ID FOREIGN KEY (EXTENSION_ID) REFERENCES EXTENSION(ID) ON DELETE CASCADE, + CONSTRAINT UNIQUE__EXTENSION_PROVIDED_SERVICE_API UNIQUE (EXTENSION_ID, CLASS_NAME, GROUP_ID, ARTIFACT_ID, VERSION) +); + +CREATE TABLE EXTENSION_RESTRICTION ( + ID VARCHAR(50) NOT NULL, + EXTENSION_ID VARCHAR(50) NOT NULL, + REQUIRED_PERMISSION VARCHAR(200) NOT NULL, + EXPLANATION VARCHAR (4096) NOT NULL, + CONSTRAINT PK__EXTENSION_RESTRICTION_ID PRIMARY KEY (ID), + CONSTRAINT FK__EXTENSION_RESTRICTION_EXTENSION_ID FOREIGN KEY (EXTENSION_ID) REFERENCES EXTENSION(ID) ON DELETE CASCADE, + CONSTRAINT UNIQUE__EXTENSION_RESTRICTION_EXTENSION_ID_REQUIRED_PERMISSION UNIQUE (EXTENSION_ID, REQUIRED_PERMISSION) +); + +CREATE TABLE EXTENSION_TAG ( + EXTENSION_ID VARCHAR(50) NOT NULL, + TAG VARCHAR(200) NOT NULL, + CONSTRAINT PK__EXTENSION_TAG_EXTENSION_ID_AND_TAG PRIMARY KEY (EXTENSION_ID, TAG), + CONSTRAINT FK__EXTENSION_TAG_EXTENSION_ID FOREIGN KEY (EXTENSION_ID) REFERENCES EXTENSION(ID) ON DELETE CASCADE +); + +ALTER TABLE BUCKET ADD ALLOW_EXTENSION_BUNDLE_REDEPLOY INT NOT NULL DEFAULT (0); \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/default/V4__AddCascadeOnDelete.sql b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/default/V4__AddCascadeOnDelete.sql new file mode 100644 index 0000000000..5b0e6c65be --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/default/V4__AddCascadeOnDelete.sql @@ -0,0 +1,23 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You 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. + +ALTER TABLE BUCKET_ITEM DROP CONSTRAINT FK__BUCKET_ITEM_BUCKET_ID; +ALTER TABLE BUCKET_ITEM ADD CONSTRAINT FK__BUCKET_ITEM_BUCKET_ID FOREIGN KEY (BUCKET_ID) REFERENCES BUCKET(ID) ON DELETE CASCADE; + +ALTER TABLE FLOW DROP CONSTRAINT FK__FLOW_BUCKET_ITEM_ID; +ALTER TABLE FLOW ADD CONSTRAINT FK__FLOW_BUCKET_ITEM_ID FOREIGN KEY (ID) REFERENCES BUCKET_ITEM(ID) ON DELETE CASCADE; + +ALTER TABLE FLOW_SNAPSHOT DROP CONSTRAINT FK__FLOW_SNAPSHOT_FLOW_ID; +ALTER TABLE FLOW_SNAPSHOT ADD CONSTRAINT FK__FLOW_SNAPSHOT_FLOW_ID FOREIGN KEY (FLOW_ID) REFERENCES FLOW(ID) ON DELETE CASCADE; \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/default/V5__AddBucketPublicFlags.sql b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/default/V5__AddBucketPublicFlags.sql new file mode 100644 index 0000000000..ef7478b3eb --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/default/V5__AddBucketPublicFlags.sql @@ -0,0 +1,16 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You 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. + +ALTER TABLE BUCKET ADD ALLOW_PUBLIC_READ INT NOT NULL DEFAULT (0); diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/default/V6__AddFlowPersistence.sql b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/default/V6__AddFlowPersistence.sql new file mode 100644 index 0000000000..0765b7052d --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/default/V6__AddFlowPersistence.sql @@ -0,0 +1,22 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You 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. + +CREATE TABLE FLOW_PERSISTENCE_PROVIDER ( + BUCKET_ID VARCHAR(50) NOT NULL, + FLOW_ID VARCHAR(50) NOT NULL, + VERSION INT NOT NULL, + FLOW_CONTENT BLOB NOT NULL, + CONSTRAINT PK__FLOW_PERSISTENCE_PROVIDER PRIMARY KEY (BUCKET_ID, FLOW_ID, VERSION) +); \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/default/V7__AddRevision.sql b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/default/V7__AddRevision.sql new file mode 100644 index 0000000000..eb37fcdcf6 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/default/V7__AddRevision.sql @@ -0,0 +1,21 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You 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. + +CREATE TABLE REVISION ( + ENTITY_ID VARCHAR(200) NOT NULL, + VERSION BIGINT NOT NULL DEFAULT (0), + CLIENT_ID VARCHAR(100), + CONSTRAINT PK__REVISION_ENTITY_ID PRIMARY KEY (ENTITY_ID) +); \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/default/V8__AddUserGroupPolicy.sql b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/default/V8__AddUserGroupPolicy.sql new file mode 100644 index 0000000000..c7341080de --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/default/V8__AddUserGroupPolicy.sql @@ -0,0 +1,63 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You 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. + +-- UserGroupProvider tables + +CREATE TABLE UGP_USER ( + IDENTIFIER VARCHAR(50) NOT NULL, + IDENTITY VARCHAR(4096) NOT NULL, + CONSTRAINT PK__UGP_USER_IDENTIFIER PRIMARY KEY (IDENTIFIER), + CONSTRAINT UNIQUE__UGP_USER_IDENTITY UNIQUE (IDENTITY) +); + +CREATE TABLE UGP_GROUP ( + IDENTIFIER VARCHAR(50) NOT NULL, + IDENTITY VARCHAR(4096) NOT NULL, + CONSTRAINT PK__UGP_GROUP_IDENTIFIER PRIMARY KEY (IDENTIFIER), + CONSTRAINT UNIQUE__UGP_GROUP_IDENTITY UNIQUE (IDENTITY) +); + +-- There is no FK constraint from USER_IDENTIFIER to the UGP_USER table because users from multiple providers may be +-- put into a group here, so it may not always be a user from the UGP_USER table +CREATE TABLE UGP_USER_GROUP ( + USER_IDENTIFIER VARCHAR(50) NOT NULL, + GROUP_IDENTIFIER VARCHAR(50) NOT NULL, + CONSTRAINT PK__UGP_USER_GROUP PRIMARY KEY (USER_IDENTIFIER, GROUP_IDENTIFIER), + CONSTRAINT FK__UGP_USER_GROUP_GROUP_IDENTIFIER FOREIGN KEY (GROUP_IDENTIFIER) REFERENCES UGP_GROUP(IDENTIFIER) ON DELETE CASCADE +); + +-- AccessPolicyProvider tables + +CREATE TABLE APP_POLICY ( + IDENTIFIER VARCHAR(50) NOT NULL, + RESOURCE VARCHAR(1000) NOT NULL, + ACTION VARCHAR(50) NOT NULL, + CONSTRAINT PK__APP_POLICY_IDENTIFIER PRIMARY KEY (IDENTIFIER), + CONSTRAINT UNIQUE__APP_POLICY_RESOURCE_ACTION UNIQUE (RESOURCE, ACTION) +); + +CREATE TABLE APP_POLICY_USER ( + POLICY_IDENTIFIER VARCHAR(50) NOT NULL, + USER_IDENTIFIER VARCHAR(50) NOT NULL, + CONSTRAINT PK__APP_POLICY_USER PRIMARY KEY (POLICY_IDENTIFIER, USER_IDENTIFIER), + CONSTRAINT FK__APP_POLICY_USER_POLICY_IDENTIFIER FOREIGN KEY (POLICY_IDENTIFIER) REFERENCES APP_POLICY(IDENTIFIER) ON DELETE CASCADE +); + +CREATE TABLE APP_POLICY_GROUP ( + POLICY_IDENTIFIER VARCHAR(50) NOT NULL, + GROUP_IDENTIFIER VARCHAR(50) NOT NULL, + CONSTRAINT PK__APP_POLICY_GROUP PRIMARY KEY (POLICY_IDENTIFIER, GROUP_IDENTIFIER), + CONSTRAINT FK__APP_POLICY_GROUP_POLICY_IDENTIFIER FOREIGN KEY (POLICY_IDENTIFIER) REFERENCES APP_POLICY(IDENTIFIER) ON DELETE CASCADE +); \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/mysql/V2__Initial.sql b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/mysql/V2__Initial.sql new file mode 100644 index 0000000000..8e37618163 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/mysql/V2__Initial.sql @@ -0,0 +1,59 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You 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. + +CREATE TABLE BUCKET ( + ID VARCHAR(50) NOT NULL, + NAME VARCHAR(767) NOT NULL, + DESCRIPTION TEXT, + CREATED TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + CONSTRAINT PK__BUCKET_ID PRIMARY KEY (ID), + CONSTRAINT UNIQUE__BUCKET_NAME UNIQUE (NAME) +); + +CREATE TABLE BUCKET_ITEM ( + ID VARCHAR(50) NOT NULL, + NAME VARCHAR(1000) NOT NULL, + DESCRIPTION TEXT, + CREATED TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + MODIFIED TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + ITEM_TYPE VARCHAR(50) NOT NULL, + BUCKET_ID VARCHAR(50) NOT NULL, + CONSTRAINT PK__BUCKET_ITEM_ID PRIMARY KEY (ID), + CONSTRAINT FK__BUCKET_ITEM_BUCKET_ID FOREIGN KEY (BUCKET_ID) REFERENCES BUCKET(ID) ON DELETE CASCADE +); + +CREATE TABLE FLOW ( + ID VARCHAR(50) NOT NULL, + CONSTRAINT PK__FLOW_ID PRIMARY KEY (ID), + CONSTRAINT FK__FLOW_BUCKET_ITEM_ID FOREIGN KEY (ID) REFERENCES BUCKET_ITEM(ID) ON DELETE CASCADE +); + +CREATE TABLE FLOW_SNAPSHOT ( + FLOW_ID VARCHAR(50) NOT NULL, + VERSION INT NOT NULL, + CREATED TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + CREATED_BY VARCHAR(1000) NOT NULL, + COMMENTS TEXT, + CONSTRAINT PK__FLOW_SNAPSHOT_FLOW_ID_AND_VERSION PRIMARY KEY (FLOW_ID, VERSION), + CONSTRAINT FK__FLOW_SNAPSHOT_FLOW_ID FOREIGN KEY (FLOW_ID) REFERENCES FLOW(ID) ON DELETE CASCADE +); + +CREATE TABLE SIGNING_KEY ( + ID VARCHAR(50) NOT NULL, + TENANT_IDENTITY VARCHAR(767) NOT NULL, + KEY_VALUE VARCHAR(50) NOT NULL, + CONSTRAINT PK__SIGNING_KEY_ID PRIMARY KEY (ID), + CONSTRAINT UNIQUE__SIGNING_KEY_TENANT_IDENTITY UNIQUE (TENANT_IDENTITY) +); \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/mysql/V3__AddExtensions.sql b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/mysql/V3__AddExtensions.sql new file mode 100644 index 0000000000..081f86cf8e --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/mysql/V3__AddExtensions.sql @@ -0,0 +1,105 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You 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. + +CREATE TABLE BUNDLE ( + ID VARCHAR(50) NOT NULL, + BUCKET_ID VARCHAR(50) NOT NULL, + BUNDLE_TYPE VARCHAR(200) NOT NULL, + GROUP_ID VARCHAR(200) NOT NULL, + ARTIFACT_ID VARCHAR(200) NOT NULL, + CONSTRAINT PK__EXTENSION_BUNDLE_ID PRIMARY KEY (ID), + CONSTRAINT FK__EXTENSION_BUNDLE_BUCKET_ITEM_ID FOREIGN KEY (ID) REFERENCES BUCKET_ITEM(ID) ON DELETE CASCADE, + CONSTRAINT FK__EXTENSION_BUNDLE_BUCKET_ID FOREIGN KEY(BUCKET_ID) REFERENCES BUCKET(ID) ON DELETE CASCADE, + CONSTRAINT UNIQUE__EXTENSION_BUNDLE_BUCKET_GROUP_ARTIFACT UNIQUE (BUCKET_ID, GROUP_ID, ARTIFACT_ID) +); + +CREATE TABLE BUNDLE_VERSION ( + ID VARCHAR(50) NOT NULL, + BUNDLE_ID VARCHAR(50) NOT NULL, + VERSION VARCHAR(100) NOT NULL, + CREATED TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + CREATED_BY VARCHAR(767) NOT NULL, + DESCRIPTION TEXT, + SHA_256_HEX VARCHAR(512) NOT NULL, + SHA_256_SUPPLIED INT NOT NULL, + CONTENT_SIZE BIGINT NOT NULL, + SYSTEM_API_VERSION VARCHAR(50), + BUILD_TOOL VARCHAR(100), + BUILD_FLAGS VARCHAR(100), + BUILD_BRANCH VARCHAR(200), + BUILD_TAG VARCHAR(200), + BUILD_REVISION VARCHAR(100), + BUILT TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3), + BUILT_BY VARCHAR(767), + CONSTRAINT PK__BUNDLE_VERSION_ID PRIMARY KEY (ID), + CONSTRAINT FK__BUNDLE_VERSION_BUNDLE_ID FOREIGN KEY (BUNDLE_ID) REFERENCES BUNDLE(ID) ON DELETE CASCADE, + CONSTRAINT UNIQUE__BUNDLE_VERSION_BUNDLE_ID_VERSION UNIQUE (BUNDLE_ID, VERSION) +); + +CREATE TABLE BUNDLE_VERSION_DEPENDENCY ( + ID VARCHAR(50) NOT NULL, + BUNDLE_VERSION_ID VARCHAR(50) NOT NULL, + GROUP_ID VARCHAR(200) NOT NULL, + ARTIFACT_ID VARCHAR(200) NOT NULL, + VERSION VARCHAR(100) NOT NULL, + CONSTRAINT PK__BUNDLE_VERSION_DEPENDENCY_ID PRIMARY KEY (ID), + CONSTRAINT FK__BUNDLE_VERSION_DEPENDENCY_BUNDLE_VERSION_ID FOREIGN KEY (BUNDLE_VERSION_ID) REFERENCES BUNDLE_VERSION(ID) ON DELETE CASCADE, + CONSTRAINT UNIQUE__BUNDLE_VERSION_DEPENDENCY_BUNDLE_ID_GAV UNIQUE (BUNDLE_VERSION_ID, GROUP_ID, ARTIFACT_ID, VERSION) +); + +CREATE TABLE EXTENSION ( + ID VARCHAR(50) NOT NULL, + BUNDLE_VERSION_ID VARCHAR(50) NOT NULL, + NAME VARCHAR(500) NOT NULL, + DISPLAY_NAME VARCHAR(500) NOT NULL, + TYPE VARCHAR(100) NOT NULL, + CONTENT TEXT NOT NULL, + ADDITIONAL_DETAILS TEXT, + HAS_ADDITIONAL_DETAILS INT NOT NULL, + CONSTRAINT PK__EXTENSION_ID PRIMARY KEY (ID), + CONSTRAINT FK__EXTENSION_BUNDLE_VERSION_ID FOREIGN KEY (BUNDLE_VERSION_ID) REFERENCES BUNDLE_VERSION(ID) ON DELETE CASCADE, + CONSTRAINT UNIQUE__EXTENSION_BUNDLE_VERSION_ID_AND_NAME UNIQUE (BUNDLE_VERSION_ID, NAME) +); + +CREATE TABLE EXTENSION_PROVIDED_SERVICE_API ( + ID VARCHAR(50) NOT NULL, + EXTENSION_ID VARCHAR(50) NOT NULL, + CLASS_NAME VARCHAR (200) NOT NULL, + GROUP_ID VARCHAR(200) NOT NULL, + ARTIFACT_ID VARCHAR(200) NOT NULL, + VERSION VARCHAR(100) NOT NULL, + CONSTRAINT PK__EXTENSION_PROVIDED_SERVICE_API_ID PRIMARY KEY (ID), + CONSTRAINT FK__EXTENSION_PROVIDED_SERVICE_API_EXTENSION_ID FOREIGN KEY (EXTENSION_ID) REFERENCES EXTENSION(ID) ON DELETE CASCADE, + CONSTRAINT UNIQUE__EXTENSION_PROVIDED_SERVICE_API UNIQUE (EXTENSION_ID, CLASS_NAME, GROUP_ID, ARTIFACT_ID, VERSION) +); + +CREATE TABLE EXTENSION_RESTRICTION ( + ID VARCHAR(50) NOT NULL, + EXTENSION_ID VARCHAR(50) NOT NULL, + REQUIRED_PERMISSION VARCHAR(200) NOT NULL, + EXPLANATION VARCHAR (4096) NOT NULL, + CONSTRAINT PK__EXTENSION_RESTRICTION_ID PRIMARY KEY (ID), + CONSTRAINT FK__EXTENSION_RESTRICTION_EXTENSION_ID FOREIGN KEY (EXTENSION_ID) REFERENCES EXTENSION(ID) ON DELETE CASCADE, + CONSTRAINT UNIQUE__EXTENSION_RESTRICTION_EXTENSION_ID_REQUIRED_PERMISSION UNIQUE (EXTENSION_ID, REQUIRED_PERMISSION) +); + +CREATE TABLE EXTENSION_TAG ( + EXTENSION_ID VARCHAR(50) NOT NULL, + TAG VARCHAR(200) NOT NULL, + CONSTRAINT PK__EXTENSION_TAG_EXTENSION_ID_AND_TAG PRIMARY KEY (EXTENSION_ID, TAG), + CONSTRAINT FK__EXTENSION_TAG_EXTENSION_ID FOREIGN KEY (EXTENSION_ID) REFERENCES EXTENSION(ID) ON DELETE CASCADE +); + +ALTER TABLE BUCKET ADD ALLOW_EXTENSION_BUNDLE_REDEPLOY INT NOT NULL DEFAULT 0; \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/mysql/V4__AddCascadeOnDelete.sql b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/mysql/V4__AddCascadeOnDelete.sql new file mode 100644 index 0000000000..2bee820f9a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/mysql/V4__AddCascadeOnDelete.sql @@ -0,0 +1,26 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You 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. + +-- NOTE: This file is here to keep same version history as the default migrations, but since MySQL came +-- later, the cascades are part of the original constraints in mysql/V2_Initial.sql + +--ALTER TABLE BUCKET_ITEM DROP CONSTRAINT FK__BUCKET_ITEM_BUCKET_ID; +--ALTER TABLE BUCKET_ITEM ADD CONSTRAINT FK__BUCKET_ITEM_BUCKET_ID FOREIGN KEY (BUCKET_ID) REFERENCES BUCKET(ID) ON DELETE CASCADE; + +--ALTER TABLE FLOW DROP CONSTRAINT FK__FLOW_BUCKET_ITEM_ID; +--ALTER TABLE FLOW ADD CONSTRAINT FK__FLOW_BUCKET_ITEM_ID FOREIGN KEY (ID) REFERENCES BUCKET_ITEM(ID) ON DELETE CASCADE; + +--ALTER TABLE FLOW_SNAPSHOT DROP CONSTRAINT FK__FLOW_SNAPSHOT_FLOW_ID; +--ALTER TABLE FLOW_SNAPSHOT ADD CONSTRAINT FK__FLOW_SNAPSHOT_FLOW_ID FOREIGN KEY (FLOW_ID) REFERENCES FLOW(ID) ON DELETE CASCADE; \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/mysql/V5__AddBucketPublicFlags.sql b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/mysql/V5__AddBucketPublicFlags.sql new file mode 100644 index 0000000000..9ba1b80e43 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/mysql/V5__AddBucketPublicFlags.sql @@ -0,0 +1,16 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You 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. + +ALTER TABLE BUCKET ADD ALLOW_PUBLIC_READ INT NOT NULL DEFAULT 0; diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/mysql/V6__AddFlowPersistence.sql b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/mysql/V6__AddFlowPersistence.sql new file mode 100644 index 0000000000..ad3d53f197 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/mysql/V6__AddFlowPersistence.sql @@ -0,0 +1,22 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You 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. + +CREATE TABLE FLOW_PERSISTENCE_PROVIDER ( + BUCKET_ID VARCHAR(50) NOT NULL, + FLOW_ID VARCHAR(50) NOT NULL, + VERSION INT NOT NULL, + FLOW_CONTENT LONGBLOB NOT NULL, + CONSTRAINT PK__FLOW_PERSISTENCE_PROVIDER PRIMARY KEY (BUCKET_ID, FLOW_ID, VERSION) +); \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/mysql/V7__AddRevision.sql b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/mysql/V7__AddRevision.sql new file mode 100644 index 0000000000..e29a515b24 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/mysql/V7__AddRevision.sql @@ -0,0 +1,21 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You 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. + +CREATE TABLE REVISION ( + ENTITY_ID VARCHAR(200) NOT NULL, + VERSION BIGINT NOT NULL DEFAULT 0, + CLIENT_ID VARCHAR(100), + CONSTRAINT PK__REVISION_ENTITY_ID PRIMARY KEY (ENTITY_ID) +); \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/mysql/V8__AddUserGroupPolicy.sql b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/mysql/V8__AddUserGroupPolicy.sql new file mode 100644 index 0000000000..495dd7306d --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/mysql/V8__AddUserGroupPolicy.sql @@ -0,0 +1,63 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You 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. + +-- UserGroupProvider tables + +CREATE TABLE UGP_USER ( + IDENTIFIER VARCHAR(50) NOT NULL, + IDENTITY VARCHAR(767) NOT NULL, + CONSTRAINT PK__UGP_USER_IDENTIFIER PRIMARY KEY (IDENTIFIER), + CONSTRAINT UNIQUE__UGP_USER_IDENTITY UNIQUE (IDENTITY) +); + +CREATE TABLE UGP_GROUP ( + IDENTIFIER VARCHAR(50) NOT NULL, + IDENTITY VARCHAR(767) NOT NULL, + CONSTRAINT PK__UGP_GROUP_IDENTIFIER PRIMARY KEY (IDENTIFIER), + CONSTRAINT UNIQUE__UGP_GROUP_IDENTITY UNIQUE (IDENTITY) +); + +-- There is no FK constraint from USER_IDENTIFIER to the UGP_USER table because users from multiple providers may be +-- put into a group here, so it may not always be a user from the UGP_USER table +CREATE TABLE UGP_USER_GROUP ( + USER_IDENTIFIER VARCHAR(50) NOT NULL, + GROUP_IDENTIFIER VARCHAR(50) NOT NULL, + CONSTRAINT PK__UGP_USER_GROUP PRIMARY KEY (USER_IDENTIFIER, GROUP_IDENTIFIER), + CONSTRAINT FK__UGP_USER_GROUP_GROUP_IDENTIFIER FOREIGN KEY (GROUP_IDENTIFIER) REFERENCES UGP_GROUP(IDENTIFIER) ON DELETE CASCADE +); + +-- AccessPolicyProvider tables + +CREATE TABLE APP_POLICY ( + IDENTIFIER VARCHAR(50) NOT NULL, + RESOURCE VARCHAR(700) NOT NULL, + ACTION VARCHAR(50) NOT NULL, + CONSTRAINT PK__APP_POLICY_IDENTIFIER PRIMARY KEY (IDENTIFIER), + CONSTRAINT UNIQUE__APP_POLICY_RESOURCE_ACTION UNIQUE (RESOURCE, ACTION) +); + +CREATE TABLE APP_POLICY_USER ( + POLICY_IDENTIFIER VARCHAR(50) NOT NULL, + USER_IDENTIFIER VARCHAR(50) NOT NULL, + CONSTRAINT PK__APP_POLICY_USER PRIMARY KEY (POLICY_IDENTIFIER, USER_IDENTIFIER), + CONSTRAINT FK__APP_POLICY_USER_POLICY_IDENTIFIER FOREIGN KEY (POLICY_IDENTIFIER) REFERENCES APP_POLICY(IDENTIFIER) ON DELETE CASCADE +); + +CREATE TABLE APP_POLICY_GROUP ( + POLICY_IDENTIFIER VARCHAR(50) NOT NULL, + GROUP_IDENTIFIER VARCHAR(50) NOT NULL, + CONSTRAINT PK__APP_POLICY_GROUP PRIMARY KEY (POLICY_IDENTIFIER, GROUP_IDENTIFIER), + CONSTRAINT FK__APP_POLICY_GROUP_POLICY_IDENTIFIER FOREIGN KEY (POLICY_IDENTIFIER) REFERENCES APP_POLICY(IDENTIFIER) ON DELETE CASCADE +); \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/original/V1.2__IncreaseColumnSizes.sql b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/original/V1.2__IncreaseColumnSizes.sql new file mode 100644 index 0000000000..b2e92d5fe8 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/original/V1.2__IncreaseColumnSizes.sql @@ -0,0 +1,25 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You 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. + +ALTER TABLE BUCKET ALTER COLUMN NAME VARCHAR2(1000); +ALTER TABLE BUCKET ALTER COLUMN DESCRIPTION VARCHAR2(65535); + +ALTER TABLE BUCKET_ITEM ALTER COLUMN NAME VARCHAR2(1000); +ALTER TABLE BUCKET_ITEM ALTER COLUMN DESCRIPTION VARCHAR2(65535); + +ALTER TABLE FLOW_SNAPSHOT ALTER COLUMN CREATED_BY VARCHAR2(4096); +ALTER TABLE FLOW_SNAPSHOT ALTER COLUMN COMMENTS VARCHAR2(65535); + +ALTER TABLE SIGNING_KEY ALTER COLUMN TENANT_IDENTITY VARCHAR2(4096); \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/original/V1.3__DropBucketItemNameUniqueness.sql b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/original/V1.3__DropBucketItemNameUniqueness.sql new file mode 100644 index 0000000000..f29b4d0b98 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/original/V1.3__DropBucketItemNameUniqueness.sql @@ -0,0 +1,27 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You 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. + +CREATE ALIAS IF NOT EXISTS EXECUTE AS $$ void executeSql(Connection conn, String sql) +throws SQLException { conn.createStatement().executeUpdate(sql); } $$; + +call execute('ALTER TABLE BUCKET_ITEM DROP CONSTRAINT ' || + ( + SELECT DISTINCT CONSTRAINT_NAME + FROM INFORMATION_SCHEMA.CONSTRAINTS + WHERE TABLE_NAME = 'BUCKET_ITEM' + AND COLUMN_LIST = 'NAME' + AND CONSTRAINT_TYPE = 'UNIQUE' + ) +); diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/original/V1__Initial.sql b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/original/V1__Initial.sql new file mode 100644 index 0000000000..a6b49602cf --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/original/V1__Initial.sql @@ -0,0 +1,54 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You 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. + +CREATE TABLE BUCKET ( + ID VARCHAR2(50) NOT NULL PRIMARY KEY, + NAME VARCHAR2(200) NOT NULL UNIQUE, + DESCRIPTION VARCHAR(4096), + CREATED TIMESTAMP NOT NULL +); + +CREATE TABLE BUCKET_ITEM ( + ID VARCHAR2(50) NOT NULL PRIMARY KEY, + NAME VARCHAR2(200) NOT NULL UNIQUE, + DESCRIPTION VARCHAR(4096), + CREATED TIMESTAMP NOT NULL, + MODIFIED TIMESTAMP NOT NULL, + ITEM_TYPE VARCHAR(50) NOT NULL, + BUCKET_ID VARCHAR2(50) NOT NULL, + FOREIGN KEY (BUCKET_ID) REFERENCES BUCKET(ID) +); + +CREATE TABLE FLOW ( + ID VARCHAR2(50) NOT NULL PRIMARY KEY, + FOREIGN KEY (ID) REFERENCES BUCKET_ITEM(ID) +); + +CREATE TABLE FLOW_SNAPSHOT ( + FLOW_ID VARCHAR2(50) NOT NULL, + VERSION INT NOT NULL, + CREATED TIMESTAMP NOT NULL, + CREATED_BY VARCHAR2(200) NOT NULL, + COMMENTS VARCHAR(4096), + PRIMARY KEY (FLOW_ID, VERSION), + FOREIGN KEY (FLOW_ID) REFERENCES FLOW(ID) +); + +CREATE TABLE SIGNING_KEY ( + ID VARCHAR2(50) NOT NULL, + TENANT_IDENTITY VARCHAR2(50) NOT NULL UNIQUE, + KEY_VALUE VARCHAR2(50) NOT NULL, + PRIMARY KEY (ID) +); \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/postgres/V2__Initial.sql b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/postgres/V2__Initial.sql new file mode 100644 index 0000000000..b992d236a2 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/postgres/V2__Initial.sql @@ -0,0 +1,60 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You 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. + +-- The NAME column has a max size of 768 because this is the largest size that MySQL allows when using a unique constraint. +CREATE TABLE BUCKET ( + ID VARCHAR(50) NOT NULL, + NAME VARCHAR(1000) NOT NULL, + DESCRIPTION TEXT, + CREATED TIMESTAMP NOT NULL, + CONSTRAINT PK__BUCKET_ID PRIMARY KEY (ID), + CONSTRAINT UNIQUE__BUCKET_NAME UNIQUE (NAME) +); + +CREATE TABLE BUCKET_ITEM ( + ID VARCHAR(50) NOT NULL, + NAME VARCHAR(1000) NOT NULL, + DESCRIPTION TEXT, + CREATED TIMESTAMP NOT NULL, + MODIFIED TIMESTAMP NOT NULL, + ITEM_TYPE VARCHAR(50) NOT NULL, + BUCKET_ID VARCHAR(50) NOT NULL, + CONSTRAINT PK__BUCKET_ITEM_ID PRIMARY KEY (ID), + CONSTRAINT FK__BUCKET_ITEM_BUCKET_ID FOREIGN KEY (BUCKET_ID) REFERENCES BUCKET(ID) +); + +CREATE TABLE FLOW ( + ID VARCHAR(50) NOT NULL, + CONSTRAINT PK__FLOW_ID PRIMARY KEY (ID), + CONSTRAINT FK__FLOW_BUCKET_ITEM_ID FOREIGN KEY (ID) REFERENCES BUCKET_ITEM(ID) +); + +CREATE TABLE FLOW_SNAPSHOT ( + FLOW_ID VARCHAR(50) NOT NULL, + VERSION INT NOT NULL, + CREATED TIMESTAMP NOT NULL, + CREATED_BY VARCHAR(4096) NOT NULL, + COMMENTS TEXT, + CONSTRAINT PK__FLOW_SNAPSHOT_FLOW_ID_AND_VERSION PRIMARY KEY (FLOW_ID, VERSION), + CONSTRAINT FK__FLOW_SNAPSHOT_FLOW_ID FOREIGN KEY (FLOW_ID) REFERENCES FLOW(ID) +); + +CREATE TABLE SIGNING_KEY ( + ID VARCHAR(50) NOT NULL, + TENANT_IDENTITY VARCHAR(4096) NOT NULL, + KEY_VALUE VARCHAR(50) NOT NULL, + CONSTRAINT PK__SIGNING_KEY_ID PRIMARY KEY (ID), + CONSTRAINT UNIQUE__SIGNING_KEY_TENANT_IDENTITY UNIQUE (TENANT_IDENTITY) +); \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/postgres/V3__AddExtensions.sql b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/postgres/V3__AddExtensions.sql new file mode 100644 index 0000000000..3bd982031c --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/postgres/V3__AddExtensions.sql @@ -0,0 +1,105 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You 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. + +CREATE TABLE BUNDLE ( + ID VARCHAR(50) NOT NULL, + BUCKET_ID VARCHAR(50) NOT NULL, + BUNDLE_TYPE VARCHAR(200) NOT NULL, + GROUP_ID VARCHAR(500) NOT NULL, + ARTIFACT_ID VARCHAR(500) NOT NULL, + CONSTRAINT PK__EXTENSION_BUNDLE_ID PRIMARY KEY (ID), + CONSTRAINT FK__EXTENSION_BUNDLE_BUCKET_ITEM_ID FOREIGN KEY (ID) REFERENCES BUCKET_ITEM(ID) ON DELETE CASCADE, + CONSTRAINT FK__EXTENSION_BUNDLE_BUCKET_ID FOREIGN KEY(BUCKET_ID) REFERENCES BUCKET(ID) ON DELETE CASCADE, + CONSTRAINT UNIQUE__EXTENSION_BUNDLE_BUCKET_GROUP_ARTIFACT UNIQUE (BUCKET_ID, GROUP_ID, ARTIFACT_ID) +); + +CREATE TABLE BUNDLE_VERSION ( + ID VARCHAR(50) NOT NULL, + BUNDLE_ID VARCHAR(50) NOT NULL, + VERSION VARCHAR(100) NOT NULL, + CREATED TIMESTAMP NOT NULL, + CREATED_BY VARCHAR(4096) NOT NULL, + DESCRIPTION TEXT, + SHA_256_HEX VARCHAR(512) NOT NULL, + SHA_256_SUPPLIED INT NOT NULL, + CONTENT_SIZE BIGINT NOT NULL, + SYSTEM_API_VERSION VARCHAR(50), + BUILD_TOOL VARCHAR(100), + BUILD_FLAGS VARCHAR(100), + BUILD_BRANCH VARCHAR(200), + BUILD_TAG VARCHAR(200), + BUILD_REVISION VARCHAR(100), + BUILT TIMESTAMP, + BUILT_BY VARCHAR(4096), + CONSTRAINT PK__BUNDLE_VERSION_ID PRIMARY KEY (ID), + CONSTRAINT FK__BUNDLE_VERSION_BUNDLE_ID FOREIGN KEY (BUNDLE_ID) REFERENCES BUNDLE(ID) ON DELETE CASCADE, + CONSTRAINT UNIQUE__BUNDLE_VERSION_BUNDLE_ID_VERSION UNIQUE (BUNDLE_ID, VERSION) +); + +CREATE TABLE BUNDLE_VERSION_DEPENDENCY ( + ID VARCHAR(50) NOT NULL, + BUNDLE_VERSION_ID VARCHAR(50) NOT NULL, + GROUP_ID VARCHAR(500) NOT NULL, + ARTIFACT_ID VARCHAR(500) NOT NULL, + VERSION VARCHAR(100) NOT NULL, + CONSTRAINT PK__BUNDLE_VERSION_DEPENDENCY_ID PRIMARY KEY (ID), + CONSTRAINT FK__BUNDLE_VERSION_DEPENDENCY_BUNDLE_VERSION_ID FOREIGN KEY (BUNDLE_VERSION_ID) REFERENCES BUNDLE_VERSION(ID) ON DELETE CASCADE, + CONSTRAINT UNIQUE__BUNDLE_VERSION_DEPENDENCY_BUNDLE_ID_GROUP_ARTIFACT_VERSION UNIQUE (BUNDLE_VERSION_ID, GROUP_ID, ARTIFACT_ID, VERSION) +); + +CREATE TABLE EXTENSION ( + ID VARCHAR(50) NOT NULL, + BUNDLE_VERSION_ID VARCHAR(50) NOT NULL, + NAME VARCHAR(500) NOT NULL, + DISPLAY_NAME VARCHAR(500) NOT NULL, + TYPE VARCHAR(100) NOT NULL, + CONTENT TEXT NOT NULL, + ADDITIONAL_DETAILS TEXT, + HAS_ADDITIONAL_DETAILS INT NOT NULL, + CONSTRAINT PK__EXTENSION_ID PRIMARY KEY (ID), + CONSTRAINT FK__EXTENSION_BUNDLE_VERSION_ID FOREIGN KEY (BUNDLE_VERSION_ID) REFERENCES BUNDLE_VERSION(ID) ON DELETE CASCADE, + CONSTRAINT UNIQUE__EXTENSION_BUNDLE_VERSION_ID_AND_NAME UNIQUE (BUNDLE_VERSION_ID, NAME) +); + +CREATE TABLE EXTENSION_PROVIDED_SERVICE_API ( + ID VARCHAR(50) NOT NULL, + EXTENSION_ID VARCHAR(50) NOT NULL, + CLASS_NAME VARCHAR (500) NOT NULL, + GROUP_ID VARCHAR(500) NOT NULL, + ARTIFACT_ID VARCHAR(500) NOT NULL, + VERSION VARCHAR(100) NOT NULL, + CONSTRAINT PK__EXTENSION_PROVIDED_SERVICE_API_ID PRIMARY KEY (ID), + CONSTRAINT FK__EXTENSION_PROVIDED_SERVICE_API_EXTENSION_ID FOREIGN KEY (EXTENSION_ID) REFERENCES EXTENSION(ID) ON DELETE CASCADE, + CONSTRAINT UNIQUE__EXTENSION_PROVIDED_SERVICE_API UNIQUE (EXTENSION_ID, CLASS_NAME, GROUP_ID, ARTIFACT_ID, VERSION) +); + +CREATE TABLE EXTENSION_RESTRICTION ( + ID VARCHAR(50) NOT NULL, + EXTENSION_ID VARCHAR(50) NOT NULL, + REQUIRED_PERMISSION VARCHAR(200) NOT NULL, + EXPLANATION VARCHAR (4096) NOT NULL, + CONSTRAINT PK__EXTENSION_RESTRICTION_ID PRIMARY KEY (ID), + CONSTRAINT FK__EXTENSION_RESTRICTION_EXTENSION_ID FOREIGN KEY (EXTENSION_ID) REFERENCES EXTENSION(ID) ON DELETE CASCADE, + CONSTRAINT UNIQUE__EXTENSION_RESTRICTION_EXTENSION_ID_REQUIRED_PERMISSION UNIQUE (EXTENSION_ID, REQUIRED_PERMISSION) +); + +CREATE TABLE EXTENSION_TAG ( + EXTENSION_ID VARCHAR(50) NOT NULL, + TAG VARCHAR(200) NOT NULL, + CONSTRAINT PK__EXTENSION_TAG_EXTENSION_ID_AND_TAG PRIMARY KEY (EXTENSION_ID, TAG), + CONSTRAINT FK__EXTENSION_TAG_EXTENSION_ID FOREIGN KEY (EXTENSION_ID) REFERENCES EXTENSION(ID) ON DELETE CASCADE +); + +ALTER TABLE BUCKET ADD ALLOW_EXTENSION_BUNDLE_REDEPLOY INT NOT NULL DEFAULT (0); \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/postgres/V4__AddCascadeOnDelete.sql b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/postgres/V4__AddCascadeOnDelete.sql new file mode 100644 index 0000000000..5b0e6c65be --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/postgres/V4__AddCascadeOnDelete.sql @@ -0,0 +1,23 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You 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. + +ALTER TABLE BUCKET_ITEM DROP CONSTRAINT FK__BUCKET_ITEM_BUCKET_ID; +ALTER TABLE BUCKET_ITEM ADD CONSTRAINT FK__BUCKET_ITEM_BUCKET_ID FOREIGN KEY (BUCKET_ID) REFERENCES BUCKET(ID) ON DELETE CASCADE; + +ALTER TABLE FLOW DROP CONSTRAINT FK__FLOW_BUCKET_ITEM_ID; +ALTER TABLE FLOW ADD CONSTRAINT FK__FLOW_BUCKET_ITEM_ID FOREIGN KEY (ID) REFERENCES BUCKET_ITEM(ID) ON DELETE CASCADE; + +ALTER TABLE FLOW_SNAPSHOT DROP CONSTRAINT FK__FLOW_SNAPSHOT_FLOW_ID; +ALTER TABLE FLOW_SNAPSHOT ADD CONSTRAINT FK__FLOW_SNAPSHOT_FLOW_ID FOREIGN KEY (FLOW_ID) REFERENCES FLOW(ID) ON DELETE CASCADE; \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/postgres/V5__AddBucketPublicFlags.sql b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/postgres/V5__AddBucketPublicFlags.sql new file mode 100644 index 0000000000..ef7478b3eb --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/postgres/V5__AddBucketPublicFlags.sql @@ -0,0 +1,16 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You 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. + +ALTER TABLE BUCKET ADD ALLOW_PUBLIC_READ INT NOT NULL DEFAULT (0); diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/postgres/V6__AddFlowPersistence.sql b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/postgres/V6__AddFlowPersistence.sql new file mode 100644 index 0000000000..395760b6f2 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/postgres/V6__AddFlowPersistence.sql @@ -0,0 +1,22 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You 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. + +CREATE TABLE FLOW_PERSISTENCE_PROVIDER ( + BUCKET_ID VARCHAR(50) NOT NULL, + FLOW_ID VARCHAR(50) NOT NULL, + VERSION INT NOT NULL, + FLOW_CONTENT BYTEA NOT NULL, + CONSTRAINT PK__FLOW_PERSISTENCE_PROVIDER PRIMARY KEY (BUCKET_ID, FLOW_ID, VERSION) +); \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/postgres/V7__AddRevision.sql b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/postgres/V7__AddRevision.sql new file mode 100644 index 0000000000..eb37fcdcf6 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/postgres/V7__AddRevision.sql @@ -0,0 +1,21 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You 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. + +CREATE TABLE REVISION ( + ENTITY_ID VARCHAR(200) NOT NULL, + VERSION BIGINT NOT NULL DEFAULT (0), + CLIENT_ID VARCHAR(100), + CONSTRAINT PK__REVISION_ENTITY_ID PRIMARY KEY (ENTITY_ID) +); \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/postgres/V8__AddUserGroupPolicy.sql b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/postgres/V8__AddUserGroupPolicy.sql new file mode 100644 index 0000000000..344069ebcc --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/postgres/V8__AddUserGroupPolicy.sql @@ -0,0 +1,63 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You 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. + +-- UserGroupProvider tables + +CREATE TABLE UGP_USER ( + IDENTIFIER VARCHAR(50) NOT NULL, + IDENTITY VARCHAR(4096) NOT NULL, + CONSTRAINT PK__UGP_USER_IDENTIFIER PRIMARY KEY (IDENTIFIER), + CONSTRAINT UNIQUE__UGP_USER_IDENTITY UNIQUE (IDENTITY) +); + +CREATE TABLE UGP_GROUP ( + IDENTIFIER VARCHAR(50) NOT NULL, + IDENTITY VARCHAR(4096) NOT NULL, + CONSTRAINT PK__UGP_GROUP_IDENTIFIER PRIMARY KEY (IDENTIFIER), + CONSTRAINT UNIQUE__UGP_GROUP_IDENTITY UNIQUE (IDENTITY) +); + +-- There is no FK constraint from USER_IDENTIFIER to the UGP_USER table because users from multiple providers may be +-- put into a group here, so it may not always be a user from the UGP_USER table +CREATE TABLE UGP_USER_GROUP ( + USER_IDENTIFIER VARCHAR(50) NOT NULL, + GROUP_IDENTIFIER VARCHAR(50) NOT NULL, + CONSTRAINT PK__UGP_USER_GROUP PRIMARY KEY (USER_IDENTIFIER, GROUP_IDENTIFIER), + CONSTRAINT FK__UGP_USER_GROUP_GROUP_IDENTIFIER FOREIGN KEY (GROUP_IDENTIFIER) REFERENCES UGP_GROUP(IDENTIFIER) ON DELETE CASCADE +); + +-- AccessPolicyProvider tables + +CREATE TABLE APP_POLICY ( + IDENTIFIER VARCHAR(50) NOT NULL, + RESOURCE VARCHAR(1000) NOT NULL, + ACTION VARCHAR(50) NOT NULL, + CONSTRAINT PK__APP_POLICY_IDENTIFIER PRIMARY KEY (IDENTIFIER), + CONSTRAINT UNIQUE__APP_POLICY_RESOURCE_ACTION UNIQUE (RESOURCE, ACTION) +); + +CREATE TABLE APP_POLICY_USER ( + POLICY_IDENTIFIER VARCHAR(50) NOT NULL, + USER_IDENTIFIER VARCHAR(50) NOT NULL, + CONSTRAINT PK__APP_POLICY_USER PRIMARY KEY (POLICY_IDENTIFIER, USER_IDENTIFIER), + CONSTRAINT FK__APP_POLICY_POLICY_IDENTIFIER FOREIGN KEY (POLICY_IDENTIFIER) REFERENCES APP_POLICY(IDENTIFIER) ON DELETE CASCADE +); + +CREATE TABLE APP_POLICY_GROUP ( + POLICY_IDENTIFIER VARCHAR(50) NOT NULL, + GROUP_IDENTIFIER VARCHAR(50) NOT NULL, + CONSTRAINT PK__APP_POLICY_GROUP PRIMARY KEY (POLICY_IDENTIFIER, GROUP_IDENTIFIER), + CONSTRAINT FK__APP_POLICY_POLICY_IDENTIFIER FOREIGN KEY (POLICY_IDENTIFIER) REFERENCES APP_POLICY(IDENTIFIER) ON DELETE CASCADE +); \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/xsd/aliases.xsd b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/xsd/aliases.xsd new file mode 100644 index 0000000000..70c9d4fdfc --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/xsd/aliases.xsd @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/xsd/authorizations.xsd b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/xsd/authorizations.xsd new file mode 100644 index 0000000000..2c8f805385 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/xsd/authorizations.xsd @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/xsd/authorizers.xsd b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/xsd/authorizers.xsd new file mode 100644 index 0000000000..ed2a293669 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/xsd/authorizers.xsd @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/xsd/identity-providers.xsd b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/xsd/identity-providers.xsd new file mode 100644 index 0000000000..bcca014fc8 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/xsd/identity-providers.xsd @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/xsd/providers.xsd b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/xsd/providers.xsd new file mode 100644 index 0000000000..4e9f5d1faa --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/xsd/providers.xsd @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/xsd/tenants.xsd b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/xsd/tenants.xsd new file mode 100644 index 0000000000..c1193c3a86 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/xsd/tenants.xsd @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/security/authorization/AuthorizerFactorySpec.groovy b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/security/authorization/AuthorizerFactorySpec.groovy new file mode 100644 index 0000000000..60a1084433 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/security/authorization/AuthorizerFactorySpec.groovy @@ -0,0 +1,135 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization + +import org.apache.nifi.registry.extension.ExtensionClassLoader +import org.apache.nifi.registry.extension.ExtensionManager +import org.apache.nifi.registry.properties.NiFiRegistryProperties +import org.apache.nifi.registry.security.authorization.resource.ResourceFactory +import org.apache.nifi.registry.security.identity.IdentityMapper +import org.apache.nifi.registry.service.RegistryService +import spock.lang.Specification + +import javax.sql.DataSource + +class AuthorizerFactorySpec extends Specification { + + def mockProperties = Mock(NiFiRegistryProperties) + def mockExtensionManager = Mock(ExtensionManager) + def mockRegistryService = Mock(RegistryService) + def mockDataSource = Mock(DataSource) + def mockIdentityMapper = Mock(IdentityMapper) + + AuthorizerFactory authorizerFactory + + // runs before every feature method + def setup() { + mockExtensionManager.getExtensionClassLoader(_) >> new ExtensionClassLoader("/tmp", new URL[0],this.getClass().getClassLoader()) + mockProperties.getPropertyKeys() >> new HashSet() // Called by IdentityMappingUtil.getIdentityMappings() + + authorizerFactory = new AuthorizerFactory(mockProperties, mockExtensionManager, null, mockRegistryService, mockDataSource, mockIdentityMapper) + } + + // runs after every feature method + def cleanup() { + authorizerFactory = null + } + + // runs before the first feature method + def setupSpec() {} + + // runs after the last feature method + def cleanupSpec() {} + + def "create default authorizer"() { + + setup: "properties indicate nifi-registry is unsecured" + mockProperties.getProperty(NiFiRegistryProperties.WEB_HTTPS_PORT) >> "" + + when: "getAuthorizer() is first called" + def authorizer = authorizerFactory.getAuthorizer() + + then: "the default authorizer is returned" + authorizer != null + + and: "any authorization request made to that authorizer is approved" + def authorizationResult = authorizer.authorize(getTestAuthorizationRequest()) + authorizationResult.result == AuthorizationResult.Result.Approved + + } + + def "create file-backed authorizer"() { + + setup: + setMockPropsAuthorizersConfig("src/test/resources/security/authorizers-good-file-providers.xml", "managed-authorizer") + + when: "getAuthorizer() is first called" + def authorizer = authorizerFactory.getAuthorizer() + + then: "an authorizer is returned with the expected providers" + authorizer != null + authorizer instanceof ManagedAuthorizer + def apProvider = ((ManagedAuthorizer) authorizer).getAccessPolicyProvider() + apProvider instanceof ConfigurableAccessPolicyProvider + def ugProvider = ((ConfigurableAccessPolicyProvider) apProvider).getUserGroupProvider() + ugProvider instanceof ConfigurableUserGroupProvider + + } + + def "invalid authorizer configuration fails"() { + + when: "a bad configuration is provided and getAuthorizer() is called" + setMockPropsAuthorizersConfig(authorizersConfigFile, selectedAuthorizer) + authorizerFactory = new AuthorizerFactory(mockProperties, mockExtensionManager, null, mockRegistryService, mockDataSource, mockIdentityMapper) + authorizerFactory.getAuthorizer() + + then: "expect an exception" + def e = thrown AuthorizerFactoryException + e.message =~ expectedExceptionMessage || e.getCause().getMessage() =~ expectedExceptionMessage + + where: + authorizersConfigFile | selectedAuthorizer | expectedExceptionMessage + "src/test/resources/security/authorizers-good-file-providers.xml" | "" | "When running securely, the authorizer identifier must be specified in the nifi-registry.properties file." + "src/test/resources/security/authorizers-good-file-providers.xml" | "non-existent-authorizer" | "The specified authorizer 'non-existent-authorizer' could not be found." + "src/test/resources/security/authorizers-bad-ug-provider-ids.xml" | "managed-authorizer" | "Duplicate User Group Provider identifier in Authorizers configuration" + "src/test/resources/security/authorizers-bad-ap-provider-ids.xml" | "managed-authorizer" | "Duplicate Access Policy Provider identifier in Authorizers configuration" + "src/test/resources/security/authorizers-bad-authorizer-ids.xml" | "managed-authorizer" | "Duplicate Authorizer identifier in Authorizers configuration" + "src/test/resources/security/authorizers-bad-composite.xml" | "managed-authorizer" | "Duplicate provider in Composite User Group Provider configuration" + "src/test/resources/security/authorizers-bad-configurable-composite.xml" | "managed-authorizer" | "Duplicate provider in Composite Configurable User Group Provider configuration" + + } + + // Helper methods + + private void setMockPropsAuthorizersConfig(String filePath, String authorizer = "managed-authorizer") { + mockProperties.getProperty(NiFiRegistryProperties.WEB_HTTPS_PORT) >> "443" + mockProperties.getSslPort() >> 443 // required to be non-null to create authorizer + mockProperties.getProperty(NiFiRegistryProperties.SECURITY_AUTHORIZERS_CONFIGURATION_FILE) >> filePath + mockProperties.getAuthorizersConfigurationFile() >> new File(filePath) + mockProperties.getProperty(NiFiRegistryProperties.SECURITY_AUTHORIZER) >> authorizer + } + + private static AuthorizationRequest getTestAuthorizationRequest() { + return new AuthorizationRequest.Builder() + .resource(ResourceFactory.getBucketsResource()) + .action(RequestAction.WRITE) + .accessAttempt(false) + .anonymous(true) + .build() + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/service/AuthorizationServiceSpec.groovy b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/service/AuthorizationServiceSpec.groovy new file mode 100644 index 0000000000..8388262c94 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/service/AuthorizationServiceSpec.groovy @@ -0,0 +1,630 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.service + +import org.apache.nifi.registry.authorization.AccessPolicy +import org.apache.nifi.registry.authorization.User +import org.apache.nifi.registry.authorization.UserGroup +import org.apache.nifi.registry.bucket.Bucket +import org.apache.nifi.registry.exception.ResourceNotFoundException +import org.apache.nifi.registry.security.authorization.* +import org.apache.nifi.registry.security.authorization.AccessPolicy as AuthAccessPolicy +import org.apache.nifi.registry.security.authorization.User as AuthUser +import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException +import org.apache.nifi.registry.security.authorization.resource.Authorizable +import org.apache.nifi.registry.security.authorization.resource.ResourceType +import spock.lang.Specification + +class AuthorizationServiceSpec extends Specification { + + def registryService = Mock(RegistryService) + def authorizableLookup = Mock(AuthorizableLookup) + def userGroupProvider = Mock(ConfigurableUserGroupProvider) + def accessPolicyProvider = Mock(ConfigurableAccessPolicyProvider) + + AuthorizationService authorizationService + + def setup() { + accessPolicyProvider.getUserGroupProvider() >> userGroupProvider + def standardAuthorizer = new StandardManagedAuthorizer(accessPolicyProvider, userGroupProvider) + authorizationService = new AuthorizationService(authorizableLookup, standardAuthorizer, registryService) + } + + // ----- User tests ------------------------------------------------------- + + def "create user"() { + + setup: + userGroupProvider.addUser(!null as AuthUser) >> { + AuthUser u -> new AuthUser.Builder().identifier(u.identifier).identity(u.identity).build() + } + userGroupProvider.getGroups() >> new HashSet() // needed for converting user to DTO + accessPolicyProvider.getAccessPolicies() >> new HashSet() // needed for converting user to DTO + + when: "new user is created successfully" + def user = new User("id", "username") + User createdUser = authorizationService.createUser(user) + + then: "created user has been assigned an identifier" + with(createdUser) { + identifier == "id" + identity == "username" + } + + } + + def "list users"() { + + setup: + userGroupProvider.getUsers() >> [ + new AuthUser.Builder().identifier("user1").identity("username1").build(), + new AuthUser.Builder().identifier("user2").identity("username2").build(), + new AuthUser.Builder().identifier("user3").identity("username3").build(), + ] + userGroupProvider.getGroups() >> new HashSet() + accessPolicyProvider.getAccessPolicies() >> new HashSet() + + when: "list of users is queried" + def users = authorizationService.getUsers() + + then: "users are successfully returned as list of DTO objects" + users != null + users.size() == 3 + with(users[0]) { + identifier == "user1" + identity == "username1" + } + with(users[1]) { + identifier == "user2" + identity == "username2" + } + with(users[2]) { + identifier == "user3" + identity == "username3" + } + + } + + def "get user"() { + + setup: + def user1 = new AuthUser.Builder().identifier("user-id-1").identity("user1").build() + def group1 = new Group.Builder().identifier("group-id-1").name("group1").addUser("user-id-1").build() + def apBuilder = new org.apache.nifi.registry.security.authorization.AccessPolicy.Builder().resource("/fake-resource").action(RequestAction.READ) + def ap1 = apBuilder.identifier("policy-1").addUser("user-id-1").build() + def ap2 = apBuilder.identifier("policy-2").clearUsers().addGroup("group-id-1").build() + def ap3 = apBuilder.identifier("policy-3").clearGroups().addGroup("does-not-exist").build() + userGroupProvider.getUser("does-not-exist") >> null + userGroupProvider.getUser("user-id-1") >> user1 + userGroupProvider.getGroup("group-id-1") >> group1 + userGroupProvider.getGroup("does-not-exist") >> null + userGroupProvider.getGroups() >> new HashSet([group1]) + accessPolicyProvider.getAccessPolicies() >> new HashSet<>([ap1, ap2, ap3]) + + + when: "get user for existing user identifier" + def userDto1 = authorizationService.getUser("user-id-1") + + then: "user is returned converted to DTO" + with(userDto1) { + identifier == "user-id-1" + identity == "user1" + userGroups.size() == 1 + userGroups[0].identifier == "group-id-1" + accessPolicies.size() == 2 + accessPolicies.stream().noneMatch({it.identifier == "policy-3"}) + } + + + when: "get user for non-existent tenant identifier" + def user2 = authorizationService.getUser("does-not-exist") + + then: "no user is returned" + thrown(ResourceNotFoundException.class) + + } + + def "update user"() { + + setup: + userGroupProvider.updateUser(!null as AuthUser) >> { + AuthUser u -> new AuthUser.Builder().identifier(u.identifier).identity(u.identity).build() + } + userGroupProvider.getGroups() >> new HashSet() + accessPolicyProvider.getAccessPolicies() >> new HashSet() + + + when: "user is updated" + def user = authorizationService.updateUser(new User("userId", "username")) + + then: "updated user is returned" + with(user) { + identifier == "userId" + identity == "username" + } + + } + + def "delete user"() { + + setup: + def user1 = new AuthUser.Builder().identifier("userId").identity("username").build() + userGroupProvider.getUser("userId") >> user1 + userGroupProvider.deleteUser(user1) >> user1 + userGroupProvider.getGroups() >> new HashSet() + accessPolicyProvider.getAccessPolicies() >> new HashSet() + + + when: "user is deleted" + def user = authorizationService.deleteUser("userId") + + then: "deleted user is returned converted to DTO" + with(user) { + identifier == "userId" + identity == "username" + } + + } + + // ----- User Group tests ------------------------------------------------- + + def "create user group"() { + + setup: + userGroupProvider.addGroup(!null as Group) >> { + Group g -> new Group.Builder().identifier(g.identifier).name(g.name).build() + } + accessPolicyProvider.getAccessPolicies() >> new HashSet() // needed for converting to DTO + + when: "new group is created successfully" + def group = new UserGroup("id", "groupName") + UserGroup createdGroup = authorizationService.createUserGroup(group) + + then: "created group has been assigned an identifier" + with(createdGroup) { + identifier == "id" + identity == "groupName" + } + + } + + def "list user groups"() { + + setup: + userGroupProvider.getGroups() >> [ + new Group.Builder().identifier("groupId1").name("groupName1").build(), + new Group.Builder().identifier("groupId2").name("groupName2").build(), + new Group.Builder().identifier("groupId3").name("groupName3").build(), + ] + accessPolicyProvider.getAccessPolicies() >> new HashSet() + + when: "list of groups is queried" + def groups = authorizationService.getUserGroups() + + then: "groups are successfully returned as list of DTO objects" + groups != null + groups.size() == 3 + with(groups[0]) { + identifier == "groupId1" + identity == "groupName1" + } + with(groups[1]) { + identifier == "groupId2" + identity == "groupName2" + } + with(groups[2]) { + identifier == "groupId3" + identity == "groupName3" + } + + } + + def "get user group"() { + + setup: + accessPolicyProvider.getAccessPolicies() >> new HashSet() + + + when: "get group for existing user identifier" + userGroupProvider.getGroup("groupId") >> new Group.Builder().identifier("groupId").name ("groupName").build() + def g1 = authorizationService.getUserGroup("groupId") + + then: "group is returned converted to DTO" + with(g1) { + identifier == "groupId" + identity == "groupName" + } + + + when: "get group for non-existent group identifier" + userGroupProvider.getUser("nonExistentId") >> null + userGroupProvider.getGroup("nonExistentId") >> null + def g2 = authorizationService.getUserGroup("nonExistentId") + + then: "no group is returned" + thrown(ResourceNotFoundException.class) + + } + + def "update user group"() { + + setup: + userGroupProvider.updateGroup(!null as Group) >> { + Group g -> new Group.Builder().identifier(g.identifier).name(g.name).build() + } + accessPolicyProvider.getAccessPolicies() >> new HashSet() + + + when: "group is updated" + def group = authorizationService.updateUserGroup(new UserGroup("id", "name")) + + then: "updated group is returned converted to DTO" + with(group) { + identifier == "id" + identity == "name" + } + + } + + def "delete user group"() { + + setup: + def group1 = new Group.Builder().identifier("id").name("name").build(); + userGroupProvider.getGroup("id") >> group1 + userGroupProvider.deleteGroup(group1) >> group1 + accessPolicyProvider.getAccessPolicies() >> new HashSet() + + + when: "group is deleted" + def group = authorizationService.deleteUserGroup("id") + + then: "deleted user is returned" + with(group) { + identifier == "id" + identity == "name" + } + + } + + // ----- Access Policy tests ---------------------------------------------- + + def "create access policy"() { + + setup: + accessPolicyProvider.addAccessPolicy(!null as AuthAccessPolicy) >> { + AuthAccessPolicy p -> new AuthAccessPolicy.Builder() + .identifier(p.identifier) + .resource(p.resource) + .action(p.action) + .addGroups(p.groups) + .addUsers(p.users) + .build() + } + accessPolicyProvider.isConfigurable(_ as AuthAccessPolicy) >> true + + + when: "new access policy is created successfully" + def accessPolicy = new AccessPolicy([resource: "/resource", action: "read"]) + accessPolicy.setIdentifier("id") + + def createdPolicy = authorizationService.createAccessPolicy(accessPolicy) + + then: "created policy has been assigned an identifier" + with(createdPolicy) { + identifier == "id" + resource == "/resource" + action == "read" + configurable == true + } + + } + + def "list access policies"() { + + setup: + accessPolicyProvider.getAccessPolicies() >> [ + new AuthAccessPolicy.Builder().identifier("ap1").resource("r1").action(RequestAction.READ).build(), + new AuthAccessPolicy.Builder().identifier("ap2").resource("r2").action(RequestAction.WRITE).build() + ] + + when: "list access polices is queried" + def policies = authorizationService.getAccessPolicies() + + then: "access policies are successfully returned as list of DTO objects" + policies != null + policies.size() == 2 + with(policies[0]) { + identifier == "ap1" + resource == "r1" + action == RequestAction.READ.toString() + } + with(policies[1]) { + identifier == "ap2" + resource == "r2" + action == RequestAction.WRITE.toString() + } + + } + + def "get access policy"() { + + when: "get policy for existing identifier" + accessPolicyProvider.getAccessPolicy("id") >> new AuthAccessPolicy.Builder() + .identifier("id") + .resource("/resource") + .action(RequestAction.READ) + .build() + def p1 = authorizationService.getAccessPolicy("id") + + then: "policy is returned converted to DTO" + with(p1) { + identifier == "id" + resource == "/resource" + action == RequestAction.READ.toString() + } + + + when: "get policy for non-existent identifier" + accessPolicyProvider.getAccessPolicy("nonExistentId") >> null + def p2 = authorizationService.getAccessPolicy("nonExistentId") + + then: "no policy is returned" + thrown(ResourceNotFoundException.class) + + } + + def "update access policy"() { + + setup: + def users = [ + "user1": "alice", + "user2": "bob", + "user3": "charlie" ] + def groups = [ + "group1": "users", + "group2": "devs", + "group3": "admins" ] + def policies = [ + "policy1": [ + "resource": "/resource1", + "action": "read", + "users": [ "user1" ], + "groups": [] + ] + ] + def mapDtoUser = { String id -> new User(id, users[id])} + def mapDtoGroup = { String id -> new UserGroup(id, groups[id])} + def mapAuthUser = { String id -> new AuthUser.Builder().identifier(id).identity(users[id]).build() } + def mapAuthGroup = { String id -> new Group.Builder().identifier(id).name(groups[id]).build() } + def mapAuthAccessPolicy = { + String id -> return new AuthAccessPolicy.Builder() + .identifier(id) + .resource(policies[id]["resource"] as String) + .action(RequestAction.valueOfValue(policies[id]["action"] as String)) + .addUsers(policies[id]["users"] as Set) + .addGroups(policies[id]["groups"] as Set) + .build() + } + userGroupProvider.getUser(!null as String) >> { String id -> users.containsKey(id) ? mapAuthUser(id) : null } + userGroupProvider.getGroup(!null as String) >> { String id -> groups.containsKey(id) ? mapAuthGroup(id) : null } + userGroupProvider.getUsers() >> { + def authUsers = [] + users.each{ k, v -> authUsers.add(new AuthUser.Builder().identifier(k).identity(v).build()) } + return authUsers + } + userGroupProvider.getGroups() >> { + def authGroups = [] + users.each{ k, v -> authGroups.add(new Group.Builder().identifier(k).name(v).build()) } + return authGroups + } + accessPolicyProvider.getAccessPolicy(!null as String) >> { String id -> policies.containsKey(id) ? mapAuthAccessPolicy(id) : null } + accessPolicyProvider.updateAccessPolicy(!null as AuthAccessPolicy) >> { + AuthAccessPolicy p -> new AuthAccessPolicy.Builder() + .identifier(p.identifier) + .resource(p.resource) + .action(p.action) + .addGroups(p.groups) + .addUsers(p.users) + .build() + } + accessPolicyProvider.isConfigurable(_ as AuthAccessPolicy) >> true + + + when: "policy is updated" + def policy = new AccessPolicy([identifier: "policy1", resource: "/resource1", action: "read"]) + policy.addUsers([mapDtoUser("user1"), mapDtoUser("user2")]) + policy.addUserGroups([mapDtoGroup("group1")]) + def p1 = authorizationService.updateAccessPolicy(policy) + + then: "updated group is returned converted to DTO" + p1 != null + p1.users.size() == 2 + def sortedUsers = p1.users.sort{it.identifier} + with(sortedUsers[0]) { + identifier == "user1" + identity == "alice" + } + with(sortedUsers[1]) { + identifier == "user2" + identity == "bob" + } + p1.userGroups.size() == 1 + with(p1.userGroups[0]) { + identifier == "group1" + identity == "users" + } + + + when: "attempt to change policy resource and action" + def p2 = authorizationService.updateAccessPolicy(new AccessPolicy([identifier: "policy1", resource: "/newResource", action: "write"])) + + then: "resource and action are unchanged" + with(p2) { + identifier == "policy1" + resource == "/resource1" + action == "read" + } + + } + + def "delete access policy"() { + + setup: + def policy1 = new AuthAccessPolicy.Builder() + .identifier("policy1") + .resource("/resource") + .action(RequestAction.READ) + .addGroups(new HashSet()) + .addUsers(new HashSet()) + .build() + + userGroupProvider.getGroups() >> new HashSet() + userGroupProvider.getUsers() >> new HashSet() + accessPolicyProvider.getAccessPolicy("id") >> policy1 + accessPolicyProvider.deleteAccessPolicy(!null as String) >> policy1 + + when: "access policy is deleted" + def policy = authorizationService.deleteAccessPolicy("id") + + then: "deleted policy is returned" + with(policy) { + identifier == "policy1" + resource == "/resource" + action == RequestAction.READ.toString() + } + + } + + // ----- Resource tests --------------------------------------------------- + + def "get resources"() { + + setup: + def buckets = [ + "b1": [ + "name": "Bucket #1", + "description": "An initial bucket for testing", + "createdTimestamp": 1 + ], + "b2": [ + "name": "Bucket #2", + "description": "A second bucket for testing", + "createdTimestamp": 2 + ], + ] + def mapBucket = { + String id -> new Bucket([ + identifier: id, + name: buckets[id]["name"] as String, + description: buckets[id]["description"] as String]) } + + registryService.getBuckets() >> {[ mapBucket("b1"), mapBucket("b2") ]} + + when: + def resources = authorizationService.getResources() + + then: + resources != null + resources.size() == 8 + def sortedResources = resources.sort{it.identifier} + sortedResources[0].identifier == "/actuator" + sortedResources[1].identifier == "/buckets" + sortedResources[2].identifier == "/buckets/b1" + sortedResources[3].identifier == "/buckets/b2" + sortedResources[4].identifier == "/policies" + sortedResources[5].identifier == "/proxy" + sortedResources[6].identifier == "/swagger" + sortedResources[7].identifier == "/tenants" + + } + + def "get authorized resources"() { + + setup: + def buckets = [ + "b1": [ + "name": "Bucket #1", + "description": "An initial bucket for testing", + "createdTimestamp": 1, + "allowPublicRead" : false + ], + "b2": [ + "name": "Bucket #2", + "description": "A second bucket for testing", + "createdTimestamp": 2, + "allowPublicRead" : true + ], + "b3": [ + "name": "Bucket #3", + "description": "A third bucket for testing", + "createdTimestamp": 3, + "allowPublicRead" : false + ] + ] + def mapBucket = { + String id -> new Bucket([ + identifier: id, + name: buckets[id]["name"] as String, + description: buckets[id]["description"] as String, + allowPublicRead: buckets[id]["allowPublicRead"] + ]) } + + registryService.getBuckets() >> {[ mapBucket("b1"), mapBucket("b2"), mapBucket("b3") ]} + + def authorized = Mock(Authorizable) + authorized.authorize(_, _, _) >> { return } + def denied = Mock(Authorizable) + denied.authorize(_, _, _) >> { throw new AccessDeniedException("") } + + authorizableLookup.getAuthorizableByResource("/actuator") >> denied + authorizableLookup.getAuthorizableByResource("/buckets") >> authorized + authorizableLookup.getAuthorizableByResource("/buckets/b1") >> authorized + authorizableLookup.getAuthorizableByResource("/buckets/b2") >> authorized + authorizableLookup.getAuthorizableByResource("/buckets/b3") >> denied + authorizableLookup.getAuthorizableByResource("/policies") >> authorized + authorizableLookup.getAuthorizableByResource("/proxy") >> denied + authorizableLookup.getAuthorizableByResource("/swagger") >> denied + authorizableLookup.getAuthorizableByResource("/tenants") >> authorized + + + when: + def resources = authorizationService.getAuthorizedResources(RequestAction.READ) + + then: + resources != null + resources.size() == 5 + def sortedResources = resources.sort{it.identifier} + sortedResources[0].identifier == "/buckets" + sortedResources[1].identifier == "/buckets/b1" + sortedResources[2].identifier == "/buckets/b2" + sortedResources[3].identifier == "/policies" + sortedResources[4].identifier == "/tenants" + + + when: + def filteredResources = authorizationService.getAuthorizedResources(RequestAction.READ, ResourceType.Bucket) + + then: + filteredResources != null + filteredResources.size() == 3 + def sortedFilteredResources = filteredResources.sort{it.identifier} + sortedFilteredResources[0].identifier == "/buckets" + sortedFilteredResources[1].identifier == "/buckets/b1" + sortedFilteredResources[2].identifier == "/buckets/b2" + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/GenerateExtensionManifestSchema.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/GenerateExtensionManifestSchema.java new file mode 100644 index 0000000000..be9b710cc0 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/GenerateExtensionManifestSchema.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry; + +import org.apache.nifi.registry.extension.component.manifest.ExtensionManifest; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.SchemaOutputResolver; +import javax.xml.transform.Result; +import javax.xml.transform.stream.StreamResult; +import java.io.File; +import java.io.IOException; + +/** + * This class can be used generate an XSD for the ExtensionManifest object model. + * + * Depending how you run this program the resulting schema will be written to the target directory of + * nifi-registry-framework, or the target directory of the root nifi-registry module, and will be named schema1.xsd. + */ +public class GenerateExtensionManifestSchema { + + public static void main(String[] args) throws IOException, JAXBException { + JAXBContext jaxbContext = JAXBContext.newInstance(ExtensionManifest.class); + SchemaOutputResolver sor = new MySchemaOutputResolver(); + jaxbContext.generateSchema(sor); + } + + public static class MySchemaOutputResolver extends SchemaOutputResolver { + + public Result createOutput(String namespaceURI, String suggestedFileName) throws IOException { + File file = new File("./target", suggestedFileName); + StreamResult result = new StreamResult(file); + result.setSystemId(file.toURI().toURL().toString()); + return result; + } + + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/DatabaseBaseTest.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/DatabaseBaseTest.java new file mode 100644 index 0000000000..02b04c0bf0 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/DatabaseBaseTest.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db; + +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +@RunWith(SpringRunner.class) +@SpringBootTest(classes = DatabaseTestApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, TransactionalTestExecutionListener.class}) +public abstract class DatabaseBaseTest { + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/DatabaseTestApplication.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/DatabaseTestApplication.java new file mode 100644 index 0000000000..dde331e97e --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/DatabaseTestApplication.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db; + +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.mockito.Mockito; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; + +/** + * Sets up the application context for database repository tests. + * + * The @SpringBootTest annotation on the repository tests will find this class by working up the package hierarchy. + * This class must be in the "db" package in order to find the entities in "db.entity" and repositories in "db.repository". + * + * The DataSourceFactory is excluded so that Spring Boot will load an in-memory H2 database. + */ +@SpringBootApplication +@ComponentScan( + excludeFilters = { + @ComponentScan.Filter( + type = FilterType.ASSIGNABLE_TYPE, + value = DataSourceFactory.class) + }) +public class DatabaseTestApplication { + + public static void main(String[] args) { + SpringApplication.run(DatabaseTestApplication.class, args); + } + + @Bean + public NiFiRegistryProperties createNiFiRegistryProperties() { + return Mockito.mock(NiFiRegistryProperties.class); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/TestDatabaseKeyService.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/TestDatabaseKeyService.java new file mode 100644 index 0000000000..d0ab56ae30 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/TestDatabaseKeyService.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db; + +import org.apache.nifi.registry.security.key.Key; +import org.apache.nifi.registry.security.key.KeyService; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +public class TestDatabaseKeyService extends DatabaseBaseTest { + + @Autowired + private KeyService keyService; + + @Test + public void testGetKeyByIdWhenExists() { + final Key existingKey = keyService.getKey("1"); + assertNotNull(existingKey); + assertEquals("1", existingKey.getId()); + assertEquals("unit_test_tenant_identity", existingKey.getIdentity()); + assertEquals("0123456789abcdef", existingKey.getKey()); + } + + @Test + public void testGetKeyByIdWhenDoesNotExist() { + final Key existingKey = keyService.getKey("2"); + assertNull(existingKey); + } + + @Test + public void testGetOrCreateKeyWhenExists() { + final Key existingKey = keyService.getOrCreateKey("unit_test_tenant_identity"); + assertNotNull(existingKey); + assertEquals("1", existingKey.getId()); + assertEquals("unit_test_tenant_identity", existingKey.getIdentity()); + assertEquals("0123456789abcdef", existingKey.getKey()); + } + + @Test + public void testGetOrCreateKeyWhenDoesNotExist() { + final Key createdKey = keyService.getOrCreateKey("does-not-exist"); + assertNotNull(createdKey); + assertNotNull(createdKey.getId()); + assertEquals("does-not-exist", createdKey.getIdentity()); + assertNotNull(createdKey.getKey()); + } + + @Test + public void testDeleteKeyWhenExists() { + final Key existingKey = keyService.getKey("1"); + assertNotNull(existingKey); + + keyService.deleteKey(existingKey.getIdentity()); + + final Key deletedKey = keyService.getKey("1"); + assertNull(deletedKey); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/TestDatabaseMetadataService.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/TestDatabaseMetadataService.java new file mode 100644 index 0000000000..3882b607dd --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/TestDatabaseMetadataService.java @@ -0,0 +1,1110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db; + +import org.apache.nifi.registry.db.entity.BucketEntity; +import org.apache.nifi.registry.db.entity.BucketItemEntity; +import org.apache.nifi.registry.db.entity.BucketItemEntityType; +import org.apache.nifi.registry.db.entity.BundleEntity; +import org.apache.nifi.registry.db.entity.BundleVersionDependencyEntity; +import org.apache.nifi.registry.db.entity.BundleVersionEntity; +import org.apache.nifi.registry.db.entity.ExtensionAdditionalDetailsEntity; +import org.apache.nifi.registry.db.entity.ExtensionEntity; +import org.apache.nifi.registry.db.entity.ExtensionProvidedServiceApiEntity; +import org.apache.nifi.registry.db.entity.ExtensionRestrictionEntity; +import org.apache.nifi.registry.db.entity.FlowEntity; +import org.apache.nifi.registry.db.entity.FlowSnapshotEntity; +import org.apache.nifi.registry.db.entity.TagCountEntity; +import org.apache.nifi.registry.extension.bundle.BundleFilterParams; +import org.apache.nifi.registry.extension.bundle.BundleType; +import org.apache.nifi.registry.extension.bundle.BundleVersionFilterParams; +import org.apache.nifi.registry.extension.component.ExtensionFilterParams; +import org.apache.nifi.registry.extension.component.manifest.ExtensionType; +import org.apache.nifi.registry.extension.component.manifest.ProvidedServiceAPI; +import org.apache.nifi.registry.service.MetadataService; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class TestDatabaseMetadataService extends DatabaseBaseTest { + + @Autowired + private MetadataService metadataService; + + //----------------- Buckets --------------------------------- + + @Test + public void testCreateAndGetBucket() { + final BucketEntity b = new BucketEntity(); + b.setId("testBucketId"); + b.setName("testBucketName"); + b.setDescription("testBucketDesc"); + b.setCreated(new Date()); + + metadataService.createBucket(b); + + final BucketEntity createdBucket = metadataService.getBucketById(b.getId()); + assertNotNull(createdBucket); + assertEquals(b.getId(), createdBucket.getId()); + assertEquals(b.getName(), createdBucket.getName()); + assertEquals(b.getDescription(), createdBucket.getDescription()); + assertEquals(b.getCreated().getTime(), createdBucket.getCreated().getTime()); + assertFalse(b.isAllowExtensionBundleRedeploy()); + } + + @Test + public void testGetBucketDoesNotExist() { + final BucketEntity bucket = metadataService.getBucketById("does-not-exist"); + assertNull(bucket); + } + + @Test + public void testGetBucketsByName() { + final List buckets = metadataService.getBucketsByName("Bucket 1"); + assertNotNull(buckets); + assertEquals(1, buckets.size()); + assertEquals("Bucket 1", buckets.get(0).getName()); + } + + @Test + public void testGetBucketsByNameNoneFound() { + final List buckets = metadataService.getBucketsByName("Bucket XYZ"); + assertNotNull(buckets); + assertEquals(0, buckets.size()); + } + + @Test + public void testUpdateBucket() { + final BucketEntity bucket = metadataService.getBucketById("1"); + assertNotNull(bucket); + assertFalse(bucket.isAllowExtensionBundleRedeploy()); + assertFalse(bucket.isAllowPublicRead()); + + final String updatedName = bucket.getName() + " UPDATED"; + final String updatedDesc = bucket.getDescription() + "DESC"; + + bucket.setName(updatedName); + bucket.setDescription(updatedDesc); + bucket.setAllowExtensionBundleRedeploy(true); + bucket.setAllowPublicRead(true); + + metadataService.updateBucket(bucket); + + final BucketEntity updatedBucket = metadataService.getBucketById(bucket.getId()); + assertNotNull(updatedName); + assertEquals(updatedName, updatedBucket.getName()); + assertEquals(updatedDesc, updatedBucket.getDescription()); + assertTrue(updatedBucket.isAllowExtensionBundleRedeploy()); + assertTrue(updatedBucket.isAllowPublicRead()); + } + + @Test + public void testDeleteBucketNoChildren() { + final BucketEntity bucket = metadataService.getBucketById("6"); + assertNotNull(bucket); + + metadataService.deleteBucket(bucket); + + final BucketEntity deletedBucket = metadataService.getBucketById("6"); + assertNull(deletedBucket); + } + + @Test + public void testDeleteBucketWithChildren() { + final BucketEntity bucket = metadataService.getBucketById("1"); + assertNotNull(bucket); + + metadataService.deleteBucket(bucket); + + final BucketEntity deletedBucket = metadataService.getBucketById("1"); + assertNull(deletedBucket); + } + + @Test + public void testGetBucketsForIds() { + final List buckets = metadataService.getBuckets(new HashSet<>(Arrays.asList("1", "2"))); + assertNotNull(buckets); + assertEquals(2, buckets.size()); + assertEquals("1", buckets.get(0).getId()); + assertEquals("2", buckets.get(1).getId()); + } + + @Test + public void testGetAllBuckets() { + final List buckets = metadataService.getAllBuckets(); + assertNotNull(buckets); + assertEquals(6, buckets.size()); + } + + //----------------- BucketItems --------------------------------- + + @Test + public void testGetBucketItemsForBucket() { + final BucketEntity bucket = metadataService.getBucketById("1"); + assertNotNull(bucket); + + final List items = metadataService.getBucketItems(bucket.getId()); + assertNotNull(items); + assertEquals(2, items.size()); + + items.stream().forEach(i -> assertNotNull(i.getBucketName())); + } + + @Test + public void testGetBucketItemsForBuckets() { + final List items = metadataService.getBucketItems(new HashSet<>(Arrays.asList("1", "2"))); + assertNotNull(items); + assertEquals(3, items.size()); + + items.stream().forEach(i -> assertNotNull(i.getBucketName())); + } + + @Test + public void testGetItemsWithCounts() { + final List items = metadataService.getBucketItems(new HashSet<>(Arrays.asList("1", "2"))); + assertNotNull(items); + + // 3 items across all buckets + assertEquals(3, items.size()); + + final BucketItemEntity item1 = items.stream().filter(i -> i.getId().equals("1")).findFirst().orElse(null); + assertNotNull(item1); + assertEquals(BucketItemEntityType.FLOW, item1.getType()); + + final FlowEntity flowEntity = (FlowEntity) item1; + assertEquals(3, flowEntity.getSnapshotCount()); + + items.stream().forEach(i -> assertNotNull(i.getBucketName())); + } + + @Test + public void testGetItemsWithCountsFilteredByBuckets() { + final List items = metadataService.getBucketItems(Collections.singleton("1")); + assertNotNull(items); + + // only 2 items in bucket 1 + assertEquals(2, items.size()); + + final BucketItemEntity item1 = items.stream().filter(i -> i.getId().equals("1")).findFirst().orElse(null); + assertNotNull(item1); + assertEquals(BucketItemEntityType.FLOW, item1.getType()); + + final FlowEntity flowEntity = (FlowEntity) item1; + assertEquals(3, flowEntity.getSnapshotCount()); + + items.stream().forEach(i -> assertNotNull(i.getBucketName())); + } + + //----------------- Flows --------------------------------- + + @Test + public void testGetFlowByIdWhenExists() { + final FlowEntity flow = metadataService.getFlowById("1"); + assertNotNull(flow); + assertEquals("1", flow.getId()); + assertEquals("1", flow.getBucketId()); + } + + @Test + public void testGetFlowByIdWhenDoesNotExist() { + final FlowEntity flow = metadataService.getFlowById("does-not-exist"); + assertNull(flow); + } + + @Test + public void testCreateFlow() { + final String bucketId = "1"; + + final FlowEntity flow = new FlowEntity(); + flow.setId(UUID.randomUUID().toString()); + flow.setBucketId(bucketId); + flow.setName("Test Flow 1"); + flow.setDescription("Description for Test Flow 1"); + flow.setCreated(new Date()); + flow.setModified(new Date()); + flow.setType(BucketItemEntityType.FLOW); + + metadataService.createFlow(flow); + + final FlowEntity createdFlow = metadataService.getFlowById(flow.getId()); + assertNotNull(flow); + assertEquals(flow.getId(), createdFlow.getId()); + assertEquals(flow.getBucketId(), createdFlow.getBucketId()); + assertEquals(flow.getName(), createdFlow.getName()); + assertEquals(flow.getDescription(), createdFlow.getDescription()); + assertEquals(flow.getCreated().getTime(), createdFlow.getCreated().getTime()); + assertEquals(flow.getModified(), createdFlow.getModified()); + assertEquals(flow.getType(), createdFlow.getType()); + } + + @Test + public void testGetFlowByIdWithSnapshotCount() { + final FlowEntity flowEntity = metadataService.getFlowByIdWithSnapshotCounts("1"); + assertNotNull(flowEntity); + assertEquals(3, flowEntity.getSnapshotCount()); + } + + @Test + public void testGetFlowsByBucket() { + final BucketEntity bucketEntity = metadataService.getBucketById("1"); + final List flows = metadataService.getFlowsByBucket(bucketEntity.getId()); + assertEquals(2, flows.size()); + + final FlowEntity flowEntity = flows.stream().filter(f -> f.getId().equals("1")).findFirst().orElse(null); + assertNotNull(flowEntity); + assertEquals(3, flowEntity.getSnapshotCount()); + } + + @Test + public void testGetFlowsByName() { + final List flows = metadataService.getFlowsByName("Flow 1"); + assertNotNull(flows); + assertEquals(2, flows.size()); + assertEquals("Flow 1", flows.get(0).getName()); + assertEquals("Flow 1", flows.get(1).getName()); + } + + @Test + public void testGetFlowsByNameByBucket() { + final List flows = metadataService.getFlowsByName("2","Flow 1"); + assertNotNull(flows); + assertEquals(1, flows.size()); + assertEquals("Flow 1", flows.get(0).getName()); + assertEquals("2", flows.get(0).getBucketId()); + } + + @Test + public void testUpdateFlow() { + final FlowEntity flow = metadataService.getFlowById("1"); + assertNotNull(flow); + + final Date originalModified = flow.getModified(); + + flow.setName(flow.getName() + " UPDATED"); + flow.setDescription(flow.getDescription() + " UPDATED"); + + metadataService.updateFlow(flow); + + final FlowEntity updatedFlow = metadataService.getFlowById( "1"); + assertNotNull(flow); + assertEquals(flow.getName(), updatedFlow.getName()); + assertEquals(flow.getDescription(), updatedFlow.getDescription()); + assertEquals(flow.getModified().getTime(), updatedFlow.getModified().getTime()); + assertTrue(updatedFlow.getModified().getTime() > originalModified.getTime()); + } + + @Test + public void testDeleteFlowWithSnapshots() { + final FlowEntity flow = metadataService.getFlowById( "1"); + assertNotNull(flow); + + metadataService.deleteFlow(flow); + + final FlowEntity deletedFlow = metadataService.getFlowById("1"); + assertNull(deletedFlow); + } + + //----------------- FlowSnapshots --------------------------------- + + @Test + public void testGetFlowSnapshot() { + final FlowSnapshotEntity entity = metadataService.getFlowSnapshot( "1", 1); + assertNotNull(entity); + assertEquals("1", entity.getFlowId()); + assertEquals(1, entity.getVersion().intValue()); + } + + @Test + public void testGetFlowSnapshotDoesNotExist() { + final FlowSnapshotEntity entity = metadataService.getFlowSnapshot( "DOES-NOT-EXIST", 1); + assertNull(entity); + } + + @Test + public void testCreateFlowSnapshot() { + final FlowSnapshotEntity flowSnapshot = new FlowSnapshotEntity(); + flowSnapshot.setFlowId("1"); + flowSnapshot.setVersion(4); + flowSnapshot.setCreated(new Date()); + flowSnapshot.setCreatedBy("test-user"); + flowSnapshot.setComments("Comments"); + + metadataService.createFlowSnapshot(flowSnapshot); + + final FlowSnapshotEntity createdFlowSnapshot = metadataService.getFlowSnapshot(flowSnapshot.getFlowId(), flowSnapshot.getVersion()); + assertNotNull(createdFlowSnapshot); + assertEquals(flowSnapshot.getFlowId(), createdFlowSnapshot.getFlowId()); + assertEquals(flowSnapshot.getVersion(), createdFlowSnapshot.getVersion()); + assertEquals(flowSnapshot.getComments(), createdFlowSnapshot.getComments()); + assertEquals(flowSnapshot.getCreated().getTime(), createdFlowSnapshot.getCreated().getTime()); + assertEquals(flowSnapshot.getCreatedBy(), createdFlowSnapshot.getCreatedBy()); + } + + @Test + public void testGetLatestSnapshot() { + final FlowSnapshotEntity latest = metadataService.getLatestSnapshot("1"); + assertNotNull(latest); + assertEquals("1", latest.getFlowId()); + assertEquals(3, latest.getVersion().intValue()); + } + + @Test + public void testGetLatestSnapshotDoesNotExist() { + final FlowSnapshotEntity latest = metadataService.getLatestSnapshot("DOES-NOT-EXIST"); + assertNull(latest); + } + + @Test + public void testGetFlowSnapshots() { + final List flowSnapshots = metadataService.getSnapshots( "1"); + assertNotNull(flowSnapshots); + assertEquals(3, flowSnapshots.size()); + } + + @Test + public void testGetFlowSnapshotsNoneFound() { + final List flowSnapshots = metadataService.getSnapshots( "2"); + assertNotNull(flowSnapshots); + assertEquals(0, flowSnapshots.size()); + } + + @Test + public void testDeleteFlowSnapshot() { + final FlowSnapshotEntity entity = metadataService.getFlowSnapshot( "1", 1); + assertNotNull(entity); + + metadataService.deleteFlowSnapshot(entity); + + final FlowSnapshotEntity deletedEntity = metadataService.getFlowSnapshot( "1", 1); + assertNull(deletedEntity); + } + + //----------------- Extension Bundles --------------------------------- + + @Test + public void testGetExtensionBundleById() { + final BundleEntity entity = metadataService.getBundle("eb1"); + assertNotNull(entity); + + assertEquals("eb1", entity.getId()); + assertEquals("nifi-example-processors-nar", entity.getName()); + assertEquals("Example processors bundle", entity.getDescription()); + assertNotNull(entity.getCreated()); + assertNotNull(entity.getModified()); + assertEquals(BucketItemEntityType.BUNDLE, entity.getType()); + assertEquals("3", entity.getBucketId()); + + assertEquals(BundleType.NIFI_NAR, entity.getBundleType()); + + assertEquals("org.apache.nifi", entity.getGroupId()); + assertEquals("nifi-example-processors-nar", entity.getArtifactId()); + } + + @Test + public void testGetExtensionBundleDoesNotExist() { + final BundleEntity entity = metadataService.getBundle("does-not-exist"); + assertNull(entity); + } + + @Test + public void testGetExtensionBundleByGroupArtifact() { + final String bucketId = "3"; + final String group = "org.apache.nifi"; + final String artifact = "nifi-example-service-api-nar"; + + final BundleEntity entity = metadataService.getBundle(bucketId, group, artifact); + assertNotNull(entity); + assertEquals(bucketId, entity.getBucketId()); + + assertEquals(group, entity.getGroupId()); + assertEquals(artifact, entity.getArtifactId()); + } + + @Test + public void testGetExtensionBundleByGroupArtifactDoesNotExist() { + final String bucketId = "3"; + final String group = "org.apache.nifi"; + final String artifact = "does-not-exist"; + + final BundleEntity entity = metadataService.getBundle(bucketId, group, artifact); + assertNull(entity); + } + + @Test + public void testGetExtensionBundlesWithEmptyFilterParams() { + final Set bucketIds = new HashSet<>(); + bucketIds.add("1"); + bucketIds.add("2"); + bucketIds.add("3"); + + final List bundles = metadataService.getBundles(bucketIds, BundleFilterParams.empty()); + assertNotNull(bundles); + assertEquals(3, bundles.size()); + + bundles.forEach(b -> { + assertTrue(b.getVersionCount() > 0); + assertNotNull(b.getBucketName()); + }); + } + + @Test + public void testGetExtensionBundlesWithFilterParams() { + final Set bucketIds = new HashSet<>(); + bucketIds.add("1"); + bucketIds.add("2"); + bucketIds.add("3"); + + final List bundles = metadataService.getBundles(bucketIds, + BundleFilterParams.empty()); + assertNotNull(bundles); + assertEquals(3, bundles.size()); + + final List bundles2 = metadataService.getBundles(bucketIds, + BundleFilterParams.of("org.apache.nifi", null)); + assertNotNull(bundles2); + assertEquals(2, bundles2.size()); + + final List bundles3 = metadataService.getBundles(bucketIds, + BundleFilterParams.of("org.apache.%", null)); + assertNotNull(bundles3); + assertEquals(2, bundles3.size()); + + final List bundles4 = metadataService.getBundles(bucketIds, + BundleFilterParams.of("org.apache.nifi", "nifi-example-processors-nar")); + assertNotNull(bundles4); + assertEquals(1, bundles4.size()); + + final List bundles5 = metadataService.getBundles(bucketIds, + BundleFilterParams.of("org.apache.nifi", "nifi-example-processors-%")); + assertNotNull(bundles5); + assertEquals(1, bundles5.size()); + + final List bundles6 = metadataService.getBundles(bucketIds, + BundleFilterParams.of(null, "nifi-example-processors-%")); + assertNotNull(bundles6); + assertEquals(1, bundles6.size()); + + final List bundles7 = metadataService.getBundles(bucketIds, + BundleFilterParams.of("Bucket %", null, "nifi-example-processors-%")); + assertNotNull(bundles7); + assertEquals(1, bundles7.size()); + } + + @Test + public void testGetExtensionBundlesByBucket() { + final List bundles = metadataService.getBundlesByBucket("3"); + assertNotNull(bundles); + assertEquals(3, bundles.size()); + + final List bundles2 = metadataService.getBundlesByBucket("6"); + assertNotNull(bundles2); + assertEquals(0, bundles2.size()); + } + + @Test + public void testGetExtensionBundlesByBucketAndGroup() { + final List bundles = metadataService.getBundlesByBucketAndGroup("3", "org.apache.nifi"); + assertNotNull(bundles); + assertEquals(2, bundles.size()); + + final List bundles2 = metadataService.getBundlesByBucketAndGroup("3", "does-not-exist"); + assertNotNull(bundles2); + assertEquals(0, bundles2.size()); + } + + @Test + public void testCreateExtensionBundle() { + final BundleEntity entity = new BundleEntity(); + entity.setId(UUID.randomUUID().toString()); + entity.setBucketId("3"); + entity.setName("nifi-foo-nar"); + entity.setDescription("This is foo nar"); + entity.setCreated(new Date()); + entity.setModified(new Date()); + entity.setGroupId("org.apache.nifi"); + entity.setArtifactId("nifi-foo-nar"); + entity.setBundleType(BundleType.NIFI_NAR); + + final BundleEntity createdEntity = metadataService.createBundle(entity); + assertNotNull(createdEntity); + + final List bundles = metadataService.getBundlesByBucket("3"); + assertNotNull(bundles); + assertEquals(4, bundles.size()); + } + + @Test + public void testDeleteExtensionBundle() { + final List bundles = metadataService.getBundlesByBucket("3"); + assertNotNull(bundles); + assertEquals(3, bundles.size()); + + final BundleEntity existingBundle = bundles.get(0); + metadataService.deleteBundle(existingBundle); + + final BundleEntity deletedBundle = metadataService.getBundle(existingBundle.getId()); + assertNull(deletedBundle); + + final List bundlesAfterDelete = metadataService.getBundlesByBucket("3"); + assertNotNull(bundlesAfterDelete); + assertEquals(2, bundlesAfterDelete.size()); + } + + @Test + public void testDeleteBucketWithExtensionBundles() { + final List bundles = metadataService.getBundlesByBucket("3"); + assertNotNull(bundles); + assertEquals(3, bundles.size()); + + final BucketEntity bucket = metadataService.getBucketById("3"); + assertNotNull(bucket); + metadataService.deleteBucket(bucket); + + final List bundlesAfterDelete = metadataService.getBundlesByBucket("3"); + assertNotNull(bundlesAfterDelete); + assertEquals(0, bundlesAfterDelete.size()); + } + + //----------------- Extension Bundle Versions --------------------------------- + + @Test + public void testCreateExtensionBundleVersion() { + final BundleVersionEntity bundleVersion = new BundleVersionEntity(); + bundleVersion.setId(UUID.randomUUID().toString()); + bundleVersion.setBundleId("eb1"); + bundleVersion.setVersion("1.1.0"); + bundleVersion.setCreated(new Date()); + bundleVersion.setCreatedBy("user2"); + bundleVersion.setDescription("This is v1.1.0"); + bundleVersion.setSha256Hex("123456789"); + bundleVersion.setSha256Supplied(false); + bundleVersion.setContentSize(2048); + bundleVersion.setSystemApiVersion("2.0.0"); + + bundleVersion.setBuildTool("JDK"); + bundleVersion.setBuildFlags("N/A"); + bundleVersion.setBuildBranch("master"); + bundleVersion.setBuildTag("HEAD"); + bundleVersion.setBuildRevision("123456"); + bundleVersion.setBuiltBy("jsmith"); + bundleVersion.setBuilt(new Date()); + + metadataService.createBundleVersion(bundleVersion); + + final BundleVersionEntity createdBundleVersion = metadataService.getBundleVersion("eb1", "1.1.0"); + assertNotNull(createdBundleVersion); + assertEquals(bundleVersion.getId(), createdBundleVersion.getId()); + assertFalse(bundleVersion.getSha256Supplied()); + + assertEquals(bundleVersion.getSystemApiVersion(), createdBundleVersion.getSystemApiVersion()); + assertEquals(bundleVersion.getBuildTool(), createdBundleVersion.getBuildTool()); + assertEquals(bundleVersion.getBuildFlags(), createdBundleVersion.getBuildFlags()); + assertEquals(bundleVersion.getBuildBranch(), createdBundleVersion.getBuildBranch()); + assertEquals(bundleVersion.getBuildTag(), createdBundleVersion.getBuildTag()); + assertEquals(bundleVersion.getBuildRevision(), createdBundleVersion.getBuildRevision()); + assertEquals(bundleVersion.getBuiltBy(), createdBundleVersion.getBuiltBy()); + assertEquals(bundleVersion.getBuilt().getTime(), createdBundleVersion.getBuilt().getTime()); + } + + @Test + public void testGetExtensionBundleVersionsWithEmptyBucketIdsAndEmptyFilterParams() { + final List versionEntities = metadataService.getBundleVersions( + Collections.emptySet(), BundleVersionFilterParams.empty()); + assertEquals(0, versionEntities.size()); + } + + @Test + public void testGetExtensionBundleVersionsWithEmptyFilterParams() { + final Set bucketIds = new HashSet<>(); + bucketIds.add("1"); + bucketIds.add("2"); + bucketIds.add("3"); + + final List versionEntities = metadataService.getBundleVersions( + bucketIds, BundleVersionFilterParams.empty()); + assertEquals(3, versionEntities.size()); + } + + @Test + public void testGetExtensionBundleVersionsWithFilterParams() { + final Set bucketIds = new HashSet<>(); + bucketIds.add("1"); + bucketIds.add("2"); + bucketIds.add("3"); + + final List versionEntities = metadataService.getBundleVersions( + bucketIds, BundleVersionFilterParams.of("org.apache.nifi", null, null)); + assertEquals(2, versionEntities.size()); + + versionEntities.forEach(bve -> { + assertNotNull(bve.getGroupId()); + assertNotNull(bve.getArtifactId()); + }); + + final List versionEntities2 = metadataService.getBundleVersions( + bucketIds, BundleVersionFilterParams.of("org.apache.%", null, null)); + assertEquals(2, versionEntities2.size()); + + final List versionEntities3 = metadataService.getBundleVersions( + bucketIds, BundleVersionFilterParams.of("org.apache.nifi", "nifi-example-processors-nar", null)); + assertEquals(1, versionEntities3.size()); + + final List versionEntities4 = metadataService.getBundleVersions( + bucketIds, BundleVersionFilterParams.of("org.apache.nifi", "nifi-example-processors-%", null)); + assertEquals(1, versionEntities4.size()); + + final List versionEntities5 = metadataService.getBundleVersions( + bucketIds, BundleVersionFilterParams.of("org.apache.nifi", "nifi-example-processors-nar", "1.0.0")); + assertEquals(1, versionEntities5.size()); + + final List versionEntities6 = metadataService.getBundleVersions( + bucketIds, BundleVersionFilterParams.of("org.apache.nifi", "nifi-example-processors-nar", "1.0.%")); + assertEquals(1, versionEntities6.size()); + + final List versionEntities7 = metadataService.getBundleVersions( + bucketIds, BundleVersionFilterParams.of("org.apache.nifi", "nifi-example-processors-nar", "NOT-FOUND")); + assertEquals(0, versionEntities7.size()); + } + + @Test + public void testGetExtensionBundleVersionByBundleIdAndVersion() { + final BundleVersionEntity bundleVersion = metadataService.getBundleVersion("eb1", "1.0.0"); + assertNotNull(bundleVersion); + assertEquals("eb1-v1", bundleVersion.getId()); + assertEquals("eb1", bundleVersion.getBundleId()); + assertEquals("1.0.0", bundleVersion.getVersion()); + assertNotNull(bundleVersion.getCreated()); + assertEquals("user1", bundleVersion.getCreatedBy()); + assertEquals("First version of eb1", bundleVersion.getDescription()); + assertTrue(bundleVersion.getSha256Supplied()); + assertEquals(1024, bundleVersion.getContentSize()); + } + + @Test + public void testGetExtensionBundleVersionByBundleIdAndVersionDoesNotExist() { + final BundleVersionEntity bundleVersion = metadataService.getBundleVersion("does-not-exist", "1.0.0"); + assertNull(bundleVersion); + } + + @Test + public void testGetExtensionBundleVersionByBucketGroupArtifactVersion() { + final String bucketId = "3"; + final String groupId = "org.apache.nifi"; + final String artifactId = "nifi-example-processors-nar"; + final String version = "1.0.0"; + + final BundleVersionEntity bundleVersion = metadataService.getBundleVersion(bucketId, groupId, artifactId, version); + assertNotNull(bundleVersion); + assertEquals("eb1-v1", bundleVersion.getId()); + assertTrue(bundleVersion.getSha256Supplied()); + } + + @Test + public void testGetExtensionBundleVersionByBucketGroupArtifactVersionWhenDoesNotExist() { + final String bucketId = "3"; + final String groupId = "org.apache.nifi"; + final String artifactId = "nifi-example-processors-nar"; + final String version = "FOO"; + + final BundleVersionEntity bundleVersion = metadataService.getBundleVersion(bucketId, groupId, artifactId, version); + assertNull(bundleVersion); + } + + @Test + public void testGetExtensionBundleVersionsByBundleId() { + final List bundleVersions = metadataService.getBundleVersions("eb1"); + assertNotNull(bundleVersions); + assertEquals(1, bundleVersions.size()); + + final BundleVersionEntity bundleVersion = bundleVersions.get(0); + assertEquals("eb1", bundleVersion.getBundleId()); + } + + @Test + public void testGetExtensionBundleVersionsByBundleIdWhenDoesNotExist() { + final List bundleVersions = metadataService.getBundleVersions("does-not-exist"); + assertNotNull(bundleVersions); + assertEquals(0, bundleVersions.size()); + } + + @Test + public void testGetExtensionBundleVersionsByBucketGroupArtifact() { + final String bucketId = "3"; + final String groupId = "org.apache.nifi"; + final String artifactId = "nifi-example-processors-nar"; + + final List bundleVersions = metadataService.getBundleVersions(bucketId, groupId, artifactId); + assertNotNull(bundleVersions); + assertEquals(1, bundleVersions.size()); + + final BundleVersionEntity bundleVersion = bundleVersions.get(0); + assertEquals("eb1-v1", bundleVersion.getId()); + } + + @Test + public void testGetExtensionBundleVersionsByBucketGroupArtifactWhenDoesNotExist() { + final String bucketId = "3"; + final String groupId = "org.apache.nifi"; + final String artifactId = "does-not-exist"; + + final List bundleVersions = metadataService.getBundleVersions(bucketId, groupId, artifactId); + assertNotNull(bundleVersions); + assertEquals(0, bundleVersions.size()); + } + + @Test + public void testGetExtensionBundleVersionsGlobal() { + final String groupId = "org.apache.nifi"; + final String artifactId = "nifi-example-processors-nar"; + final String version = "1.0.0"; + + final List bundleVersions = metadataService.getBundleVersionsGlobal(groupId, artifactId, version); + assertNotNull(bundleVersions); + assertEquals(1, bundleVersions.size()); + + final BundleVersionEntity bundleVersion = bundleVersions.get(0); + assertEquals("eb1-v1", bundleVersion.getId()); + } + + @Test + public void testDeleteExtensionBundleVersion() { + final BundleVersionEntity bundleVersion = metadataService.getBundleVersion("eb1", "1.0.0"); + assertNotNull(bundleVersion); + + metadataService.deleteBundleVersion(bundleVersion); + + final BundleVersionEntity deletedBundleVersion = metadataService.getBundleVersion("eb1", "1.0.0"); + assertNull(deletedBundleVersion); + } + + // ---------- Extension Bundle Version Dependencies ------------ + + @Test + public void testCreateExtensionBundleVersionDependency() { + final BundleVersionEntity versionEntity = metadataService.getBundleVersion("eb1", "1.0.0"); + assertNotNull(versionEntity); + + final List dependencies = metadataService.getDependenciesForBundleVersion(versionEntity.getId()); + assertNotNull(dependencies); + assertEquals(1, dependencies.size()); + + final BundleVersionDependencyEntity dependencyEntity = new BundleVersionDependencyEntity(); + dependencyEntity.setId(UUID.randomUUID().toString()); + dependencyEntity.setExtensionBundleVersionId(versionEntity.getId()); + dependencyEntity.setGroupId("com.foo"); + dependencyEntity.setArtifactId("foo-nar"); + dependencyEntity.setVersion("1.1.1"); + + metadataService.createDependency(dependencyEntity); + + final List dependencies2 = metadataService.getDependenciesForBundleVersion(versionEntity.getId()); + assertNotNull(dependencies2); + assertEquals(2, dependencies2.size()); + } + + @Test + public void testGetExtensionBundleVersionDependencies() { + final List dependencies = metadataService.getDependenciesForBundleVersion("eb1-v1"); + assertNotNull(dependencies); + assertEquals(1, dependencies.size()); + + final BundleVersionDependencyEntity dependency = dependencies.get(0); + assertEquals("eb1-v1-dep1", dependency.getId()); + assertEquals("eb1-v1", dependency.getExtensionBundleVersionId()); + assertEquals("org.apache.nifi", dependency.getGroupId()); + assertEquals("nifi-example-service-api-nar", dependency.getArtifactId()); + assertEquals("2.0.0", dependency.getVersion()); + } + + @Test + public void testGetExtensionBundleVersionDependenciesWhenNoneExist() { + final List dependencies = metadataService.getDependenciesForBundleVersion("DOES-NOT-EXIST"); + assertNotNull(dependencies); + assertEquals(0, dependencies.size()); + } + + //----------------- Extensions --------------------------------- + + @Test + public void testCreateExtension() { + final String extensionId = "4"; + + final ExtensionRestrictionEntity restrictionEntity = new ExtensionRestrictionEntity(); + restrictionEntity.setId(UUID.randomUUID().toString()); + restrictionEntity.setExtensionId(extensionId); + restrictionEntity.setRequiredPermission("read filesystem"); + restrictionEntity.setExplanation("Reads filesystem"); + + final ExtensionProvidedServiceApiEntity serviceApiEntity = new ExtensionProvidedServiceApiEntity(); + serviceApiEntity.setId(UUID.randomUUID().toString()); + serviceApiEntity.setExtensionId(extensionId); + serviceApiEntity.setClassName("com.foo.FooService"); + serviceApiEntity.setGroupId("com.foo"); + serviceApiEntity.setArtifactId("foo-nar"); + serviceApiEntity.setVersion("1.0.0"); + + final ExtensionEntity extension = new ExtensionEntity(); + extension.setId(extensionId); + extension.setBundleVersionId("eb1-v1"); + extension.setName("com.example.FooBarProcessor"); + extension.setDisplayName("FooBarProcessor"); + extension.setExtensionType(ExtensionType.PROCESSOR); + extension.setTags(new HashSet<>(Arrays.asList("tag1", "tag2"))); + extension.setProvidedServiceApis(Collections.singleton(serviceApiEntity)); + extension.setRestrictions(Collections.singleton(restrictionEntity)); + extension.setContent("{ \"name\" : \"org.apache.nifi.ExampleProcessor\", \"type\" : \"PROCESSOR\" }"); + extension.setAdditionalDetails("DETAILS"); + + metadataService.createExtension(extension); + + final ExtensionEntity retrievedExtension = metadataService.getExtensionById(extension.getId()); + assertEquals(extension.getId(), retrievedExtension.getId()); + assertEquals(extension.getBundleVersionId(), retrievedExtension.getBundleVersionId()); + assertEquals(extension.getName(), retrievedExtension.getName()); + assertEquals(extension.getDisplayName(), retrievedExtension.getDisplayName()); + assertEquals(extension.getExtensionType(), retrievedExtension.getExtensionType()); + assertEquals(extension.getContent(), retrievedExtension.getContent()); + } + + @Test + public void testGetExtensionById() { + final ExtensionEntity extension = metadataService.getExtensionById("e1"); + assertNotNull(extension); + assertEquals("e1", extension.getId()); + assertEquals("org.apache.nifi.ExampleProcessor", extension.getName()); + assertEquals("{ \"name\" : \"org.apache.nifi.ExampleProcessor\", \"type\" : \"PROCESSOR\" }", extension.getContent()); + assertFalse(extension.getHasAdditionalDetails()); + } + + @Test + public void testGetExtensionByIdWhenHasAdditionalDetails() { + final ExtensionEntity extension = metadataService.getExtensionById("e3"); + assertNotNull(extension); + assertEquals("e3", extension.getId()); + assertTrue(extension.getHasAdditionalDetails()); + } + + @Test + public void testGetExtensionByIdWithServiceApi() { + final ExtensionEntity extension = metadataService.getExtensionById("e3"); + assertNotNull(extension); + assertEquals("e3", extension.getId()); + assertEquals("org.apache.nifi.ExampleService", extension.getName()); + assertEquals("{ \"name\" : \"org.apache.nifi.ExampleService\", \"type\" : \"CONTROLLER_SERVICE\" }", extension.getContent()); + } + + @Test + public void testGetExtensionByIdWithRestriction() { + final ExtensionEntity extension = metadataService.getExtensionById("e2"); + assertNotNull(extension); + assertEquals("e2", extension.getId()); + assertEquals("org.apache.nifi.ExampleProcessorRestricted", extension.getName()); + assertEquals("{ \"name\" : \"org.apache.nifi.ExampleProcessorRestricted\", \"type\" : \"PROCESSOR\" }", extension.getContent()); + } + + @Test + public void testGetExtensionByIdDoesNotExist() { + final ExtensionEntity extension = metadataService.getExtensionById("does-not-exist"); + assertNull(extension); + } + + @Test + public void testGetExtensionByName() { + final ExtensionEntity entity = metadataService.getExtensionByName("eb1-v1", "org.apache.nifi.ExampleProcessor"); + assertNotNull(entity); + assertEquals("org.apache.nifi.ExampleProcessor", entity.getName()); + assertEquals("eb1-v1", entity.getBundleVersionId()); + } + + @Test + public void testGetExtensionByNameDoesNotExist() { + final ExtensionEntity entity = metadataService.getExtensionByName("eb1-v1", "org.apache.nifi.DOESNOTEXIST"); + assertNull(entity); + } + + @Test + public void testGetExtensionAdditionalDetailsWhenPresent() { + final ExtensionAdditionalDetailsEntity entity = metadataService.getExtensionAdditionalDetails("eb2-v1", "org.apache.nifi.ExampleService"); + assertNotNull(entity); + assertEquals("e3", entity.getExtensionId()); + assertTrue(entity.getAdditionalDetails().isPresent()); + } + + @Test + public void testGetExtensionAdditionalDetailsWhenNotPresent() { + final ExtensionAdditionalDetailsEntity entity = metadataService.getExtensionAdditionalDetails("eb1-v1", "org.apache.nifi.ExampleProcessor"); + assertNotNull(entity); + assertEquals("e1", entity.getExtensionId()); + assertFalse(entity.getAdditionalDetails().isPresent()); + } + + @Test + public void testGetExtensionAdditionalDetailsWhenExtensionDoesNotExist() { + final ExtensionAdditionalDetailsEntity entity = metadataService.getExtensionAdditionalDetails("eb1-v1", "org.apache.nifi.DOESNOTEXIST"); + assertNull(entity); + } + + @Test + public void testGetAllExtensions() { + final Set bucketIds = new HashSet<>(); + bucketIds.add("1"); + bucketIds.add("2"); + bucketIds.add("3"); + + final List extensions = metadataService.getExtensions(bucketIds, null); + assertNotNull(extensions); + assertEquals(3, extensions.size()); + } + + @Test + public void testGetAllExtensionsFilteredWithResult() { + final Set bucketIds = new HashSet<>(); + bucketIds.add("1"); + bucketIds.add("2"); + bucketIds.add("3"); + + final ExtensionFilterParams filterParams = new ExtensionFilterParams.Builder() + .extensionType(ExtensionType.CONTROLLER_SERVICE) + .bundleType(BundleType.NIFI_NAR) + .tag("SERVICE") + .tag("example") + .build(); + + final List extensions = metadataService.getExtensions(bucketIds, filterParams); + assertNotNull(extensions); + assertEquals(1, extensions.size()); + + final ExtensionEntity entity = extensions.get(0); + assertEquals(ExtensionType.CONTROLLER_SERVICE.toString(), entity.getExtensionType().toString()); + assertEquals(BundleType.NIFI_NAR, entity.getBundleType()); + } + + @Test + public void testGetAllExtensionsFilteredWithNoResult() { + final Set bucketIds = new HashSet<>(); + bucketIds.add("1"); + bucketIds.add("2"); + bucketIds.add("3"); + + final ExtensionFilterParams filterParams = new ExtensionFilterParams.Builder() + .tag("DOES NOT EXIST") + .build(); + + final List extensions = metadataService.getExtensions(bucketIds, filterParams); + assertNotNull(extensions); + assertEquals(0, extensions.size()); + } + + @Test + public void testGetAllExtensionsFilteredWithNoBuckets() { + final Set bucketIds = new HashSet<>(); + + final ExtensionFilterParams filterParams = new ExtensionFilterParams.Builder() + .extensionType(ExtensionType.CONTROLLER_SERVICE) + .bundleType(BundleType.NIFI_NAR) + .tag("SERVICE") + .tag("example") + .build(); + + final List extensions = metadataService.getExtensions(bucketIds, filterParams); + assertNotNull(extensions); + assertEquals(0, extensions.size()); + } + + @Test + public void testGetExtensionsByBundleVersionId() { + final List extensions = metadataService.getExtensionsByBundleVersionId("eb1-v1"); + assertNotNull(extensions); + assertEquals(2, extensions.size()); + } + + @Test + public void testGetExtensionsByBundleVersionIdDoesNotExist() { + final List extensions = metadataService.getExtensionsByBundleVersionId("does-not-exist"); + assertNotNull(extensions); + assertEquals(0, extensions.size()); + } + + @Test + public void testGetExtensionTags() { + final List tags = metadataService.getAllExtensionTags(); + assertNotNull(tags); + assertEquals(4, tags.size()); + + tags.forEach(t -> { + assertNotNull(t.getTag()); + assertTrue(t.getCount() > 0); + }); + } + + @Test + public void testDeleteExtension() { + final ExtensionEntity extension = metadataService.getExtensionById("e1"); + assertNotNull(extension); + + metadataService.deleteExtension(extension); + + final ExtensionEntity deletedExtension = metadataService.getExtensionById("e1"); + assertNull(deletedExtension); + } + + @Test + public void testGetExtensionsByProvidedServiceApi() { + final Set bucketIds = new HashSet<>(); + bucketIds.add("1"); + bucketIds.add("2"); + bucketIds.add("3"); + + final ProvidedServiceAPI serviceAPI = new ProvidedServiceAPI(); + serviceAPI.setClassName("org.apache.nifi.ExampleServiceAPI"); + serviceAPI.setGroupId("org.apache.nifi"); + serviceAPI.setArtifactId("nifi-example-service-api-nar"); + serviceAPI.setVersion("2.0.0"); + + final List extensions = metadataService.getExtensionsByProvidedServiceApi(bucketIds, serviceAPI); + assertEquals(1, extensions.size()); + assertEquals("e3", extensions.get(0).getId()); + } + + @Test + public void testGetExtensionsByProvidedServiceApiWithNoBuckets() { + final Set bucketIds = new HashSet<>(); + + final ProvidedServiceAPI serviceAPI = new ProvidedServiceAPI(); + serviceAPI.setClassName("org.apache.nifi.ExampleServiceAPI"); + serviceAPI.setGroupId("org.apache.nifi"); + serviceAPI.setArtifactId("nifi-example-service-api-nar"); + serviceAPI.setVersion("2.0.0"); + + final List extensions = metadataService.getExtensionsByProvidedServiceApi(bucketIds, serviceAPI); + assertEquals(0, extensions.size()); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/migration/TestLegacyDatabaseService.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/migration/TestLegacyDatabaseService.java new file mode 100644 index 0000000000..6940331913 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/migration/TestLegacyDatabaseService.java @@ -0,0 +1,143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.migration; + +import org.apache.nifi.registry.db.entity.BucketItemEntityType; +import org.flywaydb.core.Flyway; +import org.junit.Before; +import org.junit.Test; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.jdbc.core.JdbcTemplate; + +import javax.sql.DataSource; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * Test running the legacy Flyway migrations against an in-memory H2 and then using the LegacyDatabaseService to + * retrieve data. Purposely not using Spring test annotations here to avoid interfering with the normal DB context/flyway. + */ +public class TestLegacyDatabaseService { + + private DataSource dataSource; + private JdbcTemplate jdbcTemplate; + private Flyway flyway; + + private BucketEntityV1 bucketEntityV1; + private FlowEntityV1 flowEntityV1; + private FlowSnapshotEntityV1 flowSnapshotEntityV1; + + @Before + public void setup() { + dataSource = DataSourceBuilder.create() + .url("jdbc:h2:mem:legacydb") + .driverClassName("org.h2.Driver") + .build(); + + jdbcTemplate = new JdbcTemplate(dataSource); + + flyway = Flyway.configure() + .dataSource(dataSource) + .locations("db/migration/original") + .load(); + + flyway.migrate(); + + bucketEntityV1 = new BucketEntityV1(); + bucketEntityV1.setId("1"); + bucketEntityV1.setName("Bucket1"); + bucketEntityV1.setDescription("This is bucket 1"); + bucketEntityV1.setCreated(new Date()); + + jdbcTemplate.update("INSERT INTO bucket (ID, NAME, DESCRIPTION, CREATED) VALUES (?, ?, ?, ?)", + bucketEntityV1.getId(), + bucketEntityV1.getName(), + bucketEntityV1.getDescription(), + bucketEntityV1.getCreated()); + + flowEntityV1 = new FlowEntityV1(); + flowEntityV1.setId("1"); + flowEntityV1.setBucketId(bucketEntityV1.getId()); + flowEntityV1.setName("Flow1"); + flowEntityV1.setDescription("This is flow1"); + flowEntityV1.setCreated(new Date()); + flowEntityV1.setModified(new Date()); + + jdbcTemplate.update("INSERT INTO bucket_item (ID, NAME, DESCRIPTION, CREATED, MODIFIED, ITEM_TYPE, BUCKET_ID) VALUES (?, ?, ?, ?, ?, ?, ?)", + flowEntityV1.getId(), + flowEntityV1.getName(), + flowEntityV1.getDescription(), + flowEntityV1.getCreated(), + flowEntityV1.getModified(), + BucketItemEntityType.FLOW.toString(), + flowEntityV1.getBucketId()); + + jdbcTemplate.update("INSERT INTO flow (ID) VALUES (?)", flowEntityV1.getId()); + + flowSnapshotEntityV1 = new FlowSnapshotEntityV1(); + flowSnapshotEntityV1.setFlowId(flowEntityV1.getId()); + flowSnapshotEntityV1.setVersion(1); + flowSnapshotEntityV1.setComments("This is v1"); + flowSnapshotEntityV1.setCreated(new Date()); + flowSnapshotEntityV1.setCreatedBy("user1"); + + jdbcTemplate.update("INSERT INTO flow_snapshot (FLOW_ID, VERSION, CREATED, CREATED_BY, COMMENTS) VALUES (?, ?, ?, ?, ?)", + flowSnapshotEntityV1.getFlowId(), + flowSnapshotEntityV1.getVersion(), + flowSnapshotEntityV1.getCreated(), + flowSnapshotEntityV1.getCreatedBy(), + flowSnapshotEntityV1.getComments()); + } + + @Test + public void testGetLegacyData() { + final LegacyDatabaseService service = new LegacyDatabaseService(dataSource); + + final List buckets = service.getAllBuckets(); + assertEquals(1, buckets.size()); + + final BucketEntityV1 b = buckets.stream().findFirst().get(); + assertEquals(bucketEntityV1.getId(), b.getId()); + assertEquals(bucketEntityV1.getName(), b.getName()); + assertEquals(bucketEntityV1.getDescription(), b.getDescription()); + assertEquals(bucketEntityV1.getCreated(), b.getCreated()); + + final List flows = service.getAllFlows(); + assertEquals(1, flows.size()); + + final FlowEntityV1 f = flows.stream().findFirst().get(); + assertEquals(flowEntityV1.getId(), f.getId()); + assertEquals(flowEntityV1.getName(), f.getName()); + assertEquals(flowEntityV1.getDescription(), f.getDescription()); + assertEquals(flowEntityV1.getCreated(), f.getCreated()); + assertEquals(flowEntityV1.getModified(), f.getModified()); + assertEquals(flowEntityV1.getBucketId(), f.getBucketId()); + + final List flowSnapshots = service.getAllFlowSnapshots(); + assertEquals(1, flowSnapshots.size()); + + final FlowSnapshotEntityV1 fs = flowSnapshots.stream().findFirst().get(); + assertEquals(flowSnapshotEntityV1.getFlowId(), fs.getFlowId()); + assertEquals(flowSnapshotEntityV1.getVersion(), fs.getVersion()); + assertEquals(flowSnapshotEntityV1.getComments(), fs.getComments()); + assertEquals(flowSnapshotEntityV1.getCreatedBy(), fs.getCreatedBy()); + assertEquals(flowSnapshotEntityV1.getCreated(), fs.getCreated()); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/migration/TestLegacyEntityMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/migration/TestLegacyEntityMapper.java new file mode 100644 index 0000000000..3de297f6c2 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/migration/TestLegacyEntityMapper.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db.migration; + +import org.apache.nifi.registry.db.entity.BucketEntity; +import org.apache.nifi.registry.db.entity.FlowEntity; +import org.apache.nifi.registry.db.entity.FlowSnapshotEntity; +import org.junit.Test; + +import java.util.Date; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class TestLegacyEntityMapper { + + @Test + public void testMapLegacyEntities() { + final BucketEntityV1 bucketEntityV1 = new BucketEntityV1(); + bucketEntityV1.setId("1"); + bucketEntityV1.setName("Bucket1"); + bucketEntityV1.setDescription("This is bucket 1"); + bucketEntityV1.setCreated(new Date()); + + final BucketEntity bucketEntity = LegacyEntityMapper.createBucketEntity(bucketEntityV1); + assertNotNull(bucketEntity); + assertEquals(bucketEntityV1.getId(), bucketEntity.getId()); + assertEquals(bucketEntityV1.getName(), bucketEntity.getName()); + assertEquals(bucketEntityV1.getDescription(), bucketEntity.getDescription()); + assertEquals(bucketEntityV1.getCreated(), bucketEntity.getCreated()); + + final FlowEntityV1 flowEntityV1 = new FlowEntityV1(); + flowEntityV1.setId("1"); + flowEntityV1.setBucketId(bucketEntityV1.getId()); + flowEntityV1.setName("Flow1"); + flowEntityV1.setDescription("This is flow1"); + flowEntityV1.setCreated(new Date()); + flowEntityV1.setModified(new Date()); + + final FlowEntity flowEntity = LegacyEntityMapper.createFlowEntity(flowEntityV1); + assertNotNull(flowEntity); + assertEquals(flowEntityV1.getId(), flowEntity.getId()); + assertEquals(flowEntityV1.getBucketId(), flowEntity.getBucketId()); + assertEquals(flowEntityV1.getName(), flowEntity.getName()); + assertEquals(flowEntityV1.getDescription(), flowEntity.getDescription()); + assertEquals(flowEntityV1.getCreated(), flowEntity.getCreated()); + assertEquals(flowEntityV1.getModified(), flowEntity.getModified()); + + final FlowSnapshotEntityV1 flowSnapshotEntityV1 = new FlowSnapshotEntityV1(); + flowSnapshotEntityV1.setFlowId(flowEntityV1.getId()); + flowSnapshotEntityV1.setVersion(1); + flowSnapshotEntityV1.setComments("This is v1"); + flowSnapshotEntityV1.setCreated(new Date()); + flowSnapshotEntityV1.setCreatedBy("user1"); + + final FlowSnapshotEntity flowSnapshotEntity = LegacyEntityMapper.createFlowSnapshotEntity(flowSnapshotEntityV1); + assertNotNull(flowSnapshotEntity); + assertEquals(flowSnapshotEntityV1.getFlowId(), flowSnapshotEntity.getFlowId()); + assertEquals(flowSnapshotEntityV1.getVersion(), flowSnapshotEntity.getVersion()); + assertEquals(flowSnapshotEntityV1.getComments(), flowSnapshotEntity.getComments()); + assertEquals(flowSnapshotEntityV1.getCreatedBy(), flowSnapshotEntity.getCreatedBy()); + assertEquals(flowSnapshotEntityV1.getCreated(), flowSnapshotEntity.getCreated()); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/event/TestEventFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/event/TestEventFactory.java new file mode 100644 index 0000000000..f14f6b19c1 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/event/TestEventFactory.java @@ -0,0 +1,246 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.event; + +import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.extension.bundle.Bundle; +import org.apache.nifi.registry.extension.bundle.BundleType; +import org.apache.nifi.registry.extension.bundle.BundleVersion; +import org.apache.nifi.registry.extension.bundle.BundleVersionMetadata; +import org.apache.nifi.registry.flow.VersionedFlow; +import org.apache.nifi.registry.flow.VersionedFlowSnapshot; +import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata; +import org.apache.nifi.registry.flow.VersionedProcessGroup; +import org.apache.nifi.registry.hook.Event; +import org.apache.nifi.registry.hook.EventFieldName; +import org.apache.nifi.registry.hook.EventType; +import org.junit.Before; +import org.junit.Test; + +import java.util.UUID; + +import static org.junit.Assert.assertEquals; + +public class TestEventFactory { + + private Bucket bucket; + private VersionedFlow versionedFlow; + private VersionedFlowSnapshot versionedFlowSnapshot; + private Bundle bundle; + private BundleVersion bundleVersion; + + @Before + public void setup() { + bucket = new Bucket(); + bucket.setName("Bucket1"); + bucket.setIdentifier(UUID.randomUUID().toString()); + bucket.setCreatedTimestamp(System.currentTimeMillis()); + + versionedFlow = new VersionedFlow(); + versionedFlow.setIdentifier(UUID.randomUUID().toString()); + versionedFlow.setName("Flow 1"); + versionedFlow.setBucketIdentifier(bucket.getIdentifier()); + versionedFlow.setBucketName(bucket.getName()); + + VersionedFlowSnapshotMetadata metadata = new VersionedFlowSnapshotMetadata(); + metadata.setAuthor("user1"); + metadata.setComments("This is flow 1"); + metadata.setVersion(1); + metadata.setBucketIdentifier(bucket.getIdentifier()); + metadata.setFlowIdentifier(versionedFlow.getIdentifier()); + + versionedFlowSnapshot = new VersionedFlowSnapshot(); + versionedFlowSnapshot.setSnapshotMetadata(metadata); + versionedFlowSnapshot.setFlowContents(new VersionedProcessGroup()); + + bundle = new Bundle(); + bundle.setIdentifier(UUID.randomUUID().toString()); + bundle.setBucketIdentifier(bucket.getIdentifier()); + bundle.setBundleType(BundleType.NIFI_NAR); + bundle.setGroupId("org.apache.nifi"); + bundle.setArtifactId("nifi-foo-nar"); + + final BundleVersionMetadata bundleVersionMetadata = new BundleVersionMetadata(); + bundleVersionMetadata.setId(UUID.randomUUID().toString()); + bundleVersionMetadata.setVersion("1.0.0"); + bundleVersionMetadata.setBucketId(bucket.getIdentifier()); + bundleVersionMetadata.setBundleId(bundle.getIdentifier()); + + bundleVersion = new BundleVersion(); + bundleVersion.setVersionMetadata(bundleVersionMetadata); + bundleVersion.setBundle(bundle); + bundleVersion.setBucket(bucket); + } + + @Test + public void testBucketCreatedEvent() { + final Event event = EventFactory.bucketCreated(bucket); + event.validate(); + + assertEquals(EventType.CREATE_BUCKET, event.getEventType()); + assertEquals(2, event.getFields().size()); + + assertEquals(bucket.getIdentifier(), event.getField(EventFieldName.BUCKET_ID).getValue()); + assertEquals("unknown", event.getField(EventFieldName.USER).getValue()); + } + + @Test + public void testBucketUpdatedEvent() { + final Event event = EventFactory.bucketUpdated(bucket); + event.validate(); + + assertEquals(EventType.UPDATE_BUCKET, event.getEventType()); + assertEquals(2, event.getFields().size()); + + assertEquals(bucket.getIdentifier(), event.getField(EventFieldName.BUCKET_ID).getValue()); + assertEquals("unknown", event.getField(EventFieldName.USER).getValue()); + } + + @Test + public void testBucketDeletedEvent() { + final Event event = EventFactory.bucketDeleted(bucket); + event.validate(); + + assertEquals(EventType.DELETE_BUCKET, event.getEventType()); + assertEquals(2, event.getFields().size()); + + assertEquals(bucket.getIdentifier(), event.getField(EventFieldName.BUCKET_ID).getValue()); + assertEquals("unknown", event.getField(EventFieldName.USER).getValue()); + } + + @Test + public void testFlowCreated() { + final Event event = EventFactory.flowCreated(versionedFlow); + event.validate(); + + assertEquals(EventType.CREATE_FLOW, event.getEventType()); + assertEquals(3, event.getFields().size()); + + assertEquals(bucket.getIdentifier(), event.getField(EventFieldName.BUCKET_ID).getValue()); + assertEquals(versionedFlow.getIdentifier(), event.getField(EventFieldName.FLOW_ID).getValue()); + assertEquals("unknown", event.getField(EventFieldName.USER).getValue()); + } + + @Test + public void testFlowUpdated() { + final Event event = EventFactory.flowUpdated(versionedFlow); + event.validate(); + + assertEquals(EventType.UPDATE_FLOW, event.getEventType()); + assertEquals(3, event.getFields().size()); + + assertEquals(bucket.getIdentifier(), event.getField(EventFieldName.BUCKET_ID).getValue()); + assertEquals(versionedFlow.getIdentifier(), event.getField(EventFieldName.FLOW_ID).getValue()); + assertEquals("unknown", event.getField(EventFieldName.USER).getValue()); + } + + @Test + public void testFlowDeleted() { + final Event event = EventFactory.flowDeleted(versionedFlow); + event.validate(); + + assertEquals(EventType.DELETE_FLOW, event.getEventType()); + assertEquals(3, event.getFields().size()); + + assertEquals(bucket.getIdentifier(), event.getField(EventFieldName.BUCKET_ID).getValue()); + assertEquals(versionedFlow.getIdentifier(), event.getField(EventFieldName.FLOW_ID).getValue()); + assertEquals("unknown", event.getField(EventFieldName.USER).getValue()); + } + + @Test + public void testFlowVersionedCreated() { + final Event event = EventFactory.flowVersionCreated(versionedFlowSnapshot); + event.validate(); + + assertEquals(EventType.CREATE_FLOW_VERSION, event.getEventType()); + assertEquals(5, event.getFields().size()); + + assertEquals(bucket.getIdentifier(), event.getField(EventFieldName.BUCKET_ID).getValue()); + assertEquals(versionedFlow.getIdentifier(), event.getField(EventFieldName.FLOW_ID).getValue()); + + assertEquals(String.valueOf(versionedFlowSnapshot.getSnapshotMetadata().getVersion()), + event.getField(EventFieldName.VERSION).getValue()); + + assertEquals(versionedFlowSnapshot.getSnapshotMetadata().getAuthor(), + event.getField(EventFieldName.USER).getValue()); + + assertEquals(versionedFlowSnapshot.getSnapshotMetadata().getComments(), + event.getField(EventFieldName.COMMENT).getValue()); + } + + @Test + public void testFlowVersionedCreatedWhenCommentsMissing() { + versionedFlowSnapshot.getSnapshotMetadata().setComments(null); + final Event event = EventFactory.flowVersionCreated(versionedFlowSnapshot); + event.validate(); + assertEquals("", event.getField(EventFieldName.COMMENT).getValue()); + } + + @Test + public void testExtensionBundleCreated() { + final Event event = EventFactory.extensionBundleCreated(bundle); + event.validate(); + + assertEquals(EventType.CREATE_EXTENSION_BUNDLE, event.getEventType()); + assertEquals(3, event.getFields().size()); + + assertEquals(bucket.getIdentifier(), event.getField(EventFieldName.BUCKET_ID).getValue()); + assertEquals(bundle.getIdentifier(), event.getField(EventFieldName.EXTENSION_BUNDLE_ID).getValue()); + assertEquals("unknown", event.getField(EventFieldName.USER).getValue()); + } + + @Test + public void testExtensionBundleDeleted() { + final Event event = EventFactory.extensionBundleDeleted(bundle); + event.validate(); + + assertEquals(EventType.DELETE_EXTENSION_BUNDLE, event.getEventType()); + assertEquals(3, event.getFields().size()); + + assertEquals(bucket.getIdentifier(), event.getField(EventFieldName.BUCKET_ID).getValue()); + assertEquals(bundle.getIdentifier(), event.getField(EventFieldName.EXTENSION_BUNDLE_ID).getValue()); + assertEquals("unknown", event.getField(EventFieldName.USER).getValue()); + } + + @Test + public void testExtensionBundleVersionCreated() { + final Event event = EventFactory.extensionBundleVersionCreated(bundleVersion); + event.validate(); + + assertEquals(EventType.CREATE_EXTENSION_BUNDLE_VERSION, event.getEventType()); + assertEquals(4, event.getFields().size()); + + assertEquals(bucket.getIdentifier(), event.getField(EventFieldName.BUCKET_ID).getValue()); + assertEquals(bundle.getIdentifier(), event.getField(EventFieldName.EXTENSION_BUNDLE_ID).getValue()); + assertEquals(bundleVersion.getVersionMetadata().getVersion(), event.getField(EventFieldName.VERSION).getValue()); + assertEquals("unknown", event.getField(EventFieldName.USER).getValue()); + } + + @Test + public void testExtensionBundleVersionDeleted() { + final Event event = EventFactory.extensionBundleVersionDeleted(bundleVersion); + event.validate(); + + assertEquals(EventType.DELETE_EXTENSION_BUNDLE_VERSION, event.getEventType()); + assertEquals(4, event.getFields().size()); + + assertEquals(bucket.getIdentifier(), event.getField(EventFieldName.BUCKET_ID).getValue()); + assertEquals(bundle.getIdentifier(), event.getField(EventFieldName.EXTENSION_BUNDLE_ID).getValue()); + assertEquals(bundleVersion.getVersionMetadata().getVersion(), event.getField(EventFieldName.VERSION).getValue()); + assertEquals("unknown", event.getField(EventFieldName.USER).getValue()); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/event/TestEventService.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/event/TestEventService.java new file mode 100644 index 0000000000..0270dd8ec6 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/event/TestEventService.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.event; + +import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.hook.Event; +import org.apache.nifi.registry.hook.EventHookException; +import org.apache.nifi.registry.hook.EventHookProvider; +import org.apache.nifi.registry.provider.ProviderConfigurationContext; +import org.apache.nifi.registry.provider.ProviderCreationException; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +public class TestEventService { + + private CapturingEventHook eventHook; + private EventService eventService; + + @Before + public void setup() { + eventHook = new CapturingEventHook(); + eventService = new EventService(Collections.singletonList(eventHook)); + eventService.postConstruct(); + } + + @After + public void teardown() throws Exception { + eventService.destroy(); + } + + @Test + public void testPublishConsume() throws InterruptedException { + final Bucket bucket = new Bucket(); + bucket.setIdentifier(UUID.randomUUID().toString()); + + final Event bucketCreatedEvent = EventFactory.bucketCreated(bucket); + eventService.publish(bucketCreatedEvent); + + final Event bucketDeletedEvent = EventFactory.bucketDeleted(bucket); + eventService.publish(bucketDeletedEvent); + + Thread.sleep(1000); + + final List events = eventHook.getEvents(); + Assert.assertEquals(2, events.size()); + + final Event firstEvent = events.get(0); + Assert.assertEquals(bucketCreatedEvent.getEventType(), firstEvent.getEventType()); + + final Event secondEvent = events.get(1); + Assert.assertEquals(bucketDeletedEvent.getEventType(), secondEvent.getEventType()); + } + + /** + * Simple implementation of EventHookProvider that captures event for later verification. + */ + private class CapturingEventHook implements EventHookProvider { + + private List events = new ArrayList<>(); + + @Override + public void onConfigured(ProviderConfigurationContext configurationContext) throws ProviderCreationException { + + } + + @Override + public void handle(Event event) throws EventHookException { + events.add(event); + } + + public List getEvents() { + return events; + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/event/TestStandardEvent.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/event/TestStandardEvent.java new file mode 100644 index 0000000000..8e9f73f7ca --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/event/TestStandardEvent.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.event; + +import org.apache.nifi.registry.hook.Event; +import org.apache.nifi.registry.hook.EventField; +import org.apache.nifi.registry.hook.EventFieldName; +import org.apache.nifi.registry.hook.EventType; +import org.junit.Assert; +import org.junit.Test; + +public class TestStandardEvent { + + @Test(expected = IllegalStateException.class) + public void testInvalidEvent() { + final Event event = new StandardEvent.Builder() + .eventType(EventType.CREATE_BUCKET) + .build(); + + event.validate(); + } + + @Test + public void testGetFieldWhenDoesNotExist() { + final Event event = new StandardEvent.Builder() + .eventType(EventType.CREATE_BUCKET) + .build(); + + final EventField field = event.getField(EventFieldName.BUCKET_ID); + Assert.assertNull(field); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/MockBundlePersistenceProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/MockBundlePersistenceProvider.java new file mode 100644 index 0000000000..ab1d0e4ec7 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/MockBundlePersistenceProvider.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider; + +import org.apache.nifi.registry.extension.BundleCoordinate; +import org.apache.nifi.registry.extension.BundlePersistenceContext; +import org.apache.nifi.registry.extension.BundlePersistenceException; +import org.apache.nifi.registry.extension.BundlePersistenceProvider; +import org.apache.nifi.registry.extension.BundleVersionCoordinate; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Map; + +public class MockBundlePersistenceProvider implements BundlePersistenceProvider { + + private Map properties; + + @Override + public void createBundleVersion(BundlePersistenceContext context, InputStream contentStream) throws BundlePersistenceException { + + } + + @Override + public void updateBundleVersion(BundlePersistenceContext context, InputStream contentStream) throws BundlePersistenceException { + + } + + @Override + public void getBundleVersionContent(BundleVersionCoordinate versionCoordinate, OutputStream outputStream) throws BundlePersistenceException { + + } + + @Override + public void deleteBundleVersion(BundleVersionCoordinate versionCoordinate) throws BundlePersistenceException { + + } + + @Override + public void deleteAllBundleVersions(BundleCoordinate bundleCoordinate) throws BundlePersistenceException { + + } + + @Override + public void onConfigured(ProviderConfigurationContext configurationContext) + throws ProviderCreationException { + properties = configurationContext.getProperties(); + } + + public Map getProperties() { + return properties; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/MockFlowPersistenceProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/MockFlowPersistenceProvider.java new file mode 100644 index 0000000000..430f3a3bf6 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/MockFlowPersistenceProvider.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider; + +import org.apache.nifi.registry.flow.FlowPersistenceProvider; +import org.apache.nifi.registry.flow.FlowSnapshotContext; +import org.apache.nifi.registry.flow.FlowPersistenceException; + +import java.util.Map; + +public class MockFlowPersistenceProvider implements FlowPersistenceProvider { + + private Map properties; + + @Override + public void onConfigured(ProviderConfigurationContext configurationContext) throws ProviderCreationException { + properties = configurationContext.getProperties(); + } + + @Override + public void saveFlowContent(FlowSnapshotContext context, byte[] content) throws FlowPersistenceException { + + } + + @Override + public byte[] getFlowContent(String bucketId, String flowId, int version) throws FlowPersistenceException { + return new byte[0]; + } + + @Override + public void deleteAllFlowContent(String bucketId, String flowId) throws FlowPersistenceException { + + } + + @Override + public void deleteFlowContent(String bucketId, String flowId, int version) throws FlowPersistenceException { + + } + + public Map getProperties() { + return properties; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/TestStandardProviderFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/TestStandardProviderFactory.java new file mode 100644 index 0000000000..7fb8deeca3 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/TestStandardProviderFactory.java @@ -0,0 +1,115 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider; + +import org.apache.nifi.registry.extension.BundlePersistenceProvider; +import org.apache.nifi.registry.extension.ExtensionClassLoader; +import org.apache.nifi.registry.extension.ExtensionManager; +import org.apache.nifi.registry.flow.FlowPersistenceProvider; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.junit.Test; +import org.mockito.Mockito; + +import javax.sql.DataSource; +import java.net.URL; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.when; + +public class TestStandardProviderFactory { + + @Test + public void testGetProvidersSuccess() { + final NiFiRegistryProperties props = new NiFiRegistryProperties(); + props.setProperty(NiFiRegistryProperties.PROVIDERS_CONFIGURATION_FILE, "src/test/resources/provider/providers-good.xml"); + + final ExtensionManager extensionManager = Mockito.mock(ExtensionManager.class); + when(extensionManager.getExtensionClassLoader(any(String.class))) + .thenReturn(new ExtensionClassLoader("/tmp", new URL[0],this.getClass().getClassLoader())); + + final DataSource dataSource = Mockito.mock(DataSource.class); + + final ProviderFactory providerFactory = new StandardProviderFactory(props, extensionManager, dataSource); + providerFactory.initialize(); + + final FlowPersistenceProvider flowPersistenceProvider = providerFactory.getFlowPersistenceProvider(); + assertNotNull(flowPersistenceProvider); + + final MockFlowPersistenceProvider mockFlowProvider = (MockFlowPersistenceProvider) flowPersistenceProvider; + assertNotNull(mockFlowProvider.getProperties()); + assertEquals("flow foo", mockFlowProvider.getProperties().get("Flow Property 1")); + assertEquals("flow bar", mockFlowProvider.getProperties().get("Flow Property 2")); + + final BundlePersistenceProvider bundlePersistenceProvider = providerFactory.getBundlePersistenceProvider(); + assertNotNull(bundlePersistenceProvider); + + final MockBundlePersistenceProvider mockBundlePersistenceProvider = (MockBundlePersistenceProvider) bundlePersistenceProvider; + assertNotNull(mockBundlePersistenceProvider.getProperties()); + assertEquals("extension foo", mockBundlePersistenceProvider.getProperties().get("Extension Property 1")); + assertEquals("extension bar", mockBundlePersistenceProvider.getProperties().get("Extension Property 2")); + } + + @Test(expected = ProviderFactoryException.class) + public void testGetFlowProviderBeforeInitializingShouldThrowException() { + final NiFiRegistryProperties props = new NiFiRegistryProperties(); + props.setProperty(NiFiRegistryProperties.PROVIDERS_CONFIGURATION_FILE, "src/test/resources/provider/providers-good.xml"); + + final ExtensionManager extensionManager = Mockito.mock(ExtensionManager.class); + when(extensionManager.getExtensionClassLoader(any(String.class))) + .thenReturn(new ExtensionClassLoader("/tmp", new URL[0],this.getClass().getClassLoader())); + + final DataSource dataSource = Mockito.mock(DataSource.class); + + final ProviderFactory providerFactory = new StandardProviderFactory(props, extensionManager, dataSource); + providerFactory.getFlowPersistenceProvider(); + } + + @Test(expected = ProviderFactoryException.class) + public void testProvidersConfigDoesNotExist() { + final NiFiRegistryProperties props = new NiFiRegistryProperties(); + props.setProperty(NiFiRegistryProperties.PROVIDERS_CONFIGURATION_FILE, "src/test/resources/provider/providers-does-not-exist.xml"); + + final ExtensionManager extensionManager = Mockito.mock(ExtensionManager.class); + when(extensionManager.getExtensionClassLoader(any(String.class))) + .thenReturn(new ExtensionClassLoader("/tmp", new URL[0],this.getClass().getClassLoader())); + + final DataSource dataSource = Mockito.mock(DataSource.class); + + final ProviderFactory providerFactory = new StandardProviderFactory(props, extensionManager, dataSource); + providerFactory.initialize(); + } + + @Test(expected = ProviderFactoryException.class) + public void testFlowProviderClassNotFound() { + final NiFiRegistryProperties props = new NiFiRegistryProperties(); + props.setProperty(NiFiRegistryProperties.PROVIDERS_CONFIGURATION_FILE, "src/test/resources/provider/providers-class-not-found.xml"); + + final ExtensionManager extensionManager = Mockito.mock(ExtensionManager.class); + when(extensionManager.getExtensionClassLoader(any(String.class))) + .thenReturn(new ExtensionClassLoader("/tmp", new URL[0],this.getClass().getClassLoader())); + + final DataSource dataSource = Mockito.mock(DataSource.class); + + final ProviderFactory providerFactory = new StandardProviderFactory(props, extensionManager, dataSource); + providerFactory.initialize(); + + providerFactory.getFlowPersistenceProvider(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/extension/TestFileSystemBundlePersistenceProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/extension/TestFileSystemBundlePersistenceProvider.java new file mode 100644 index 0000000000..75c3cacf7a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/extension/TestFileSystemBundlePersistenceProvider.java @@ -0,0 +1,326 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider.extension; + +import org.apache.commons.io.IOUtils; +import org.apache.nifi.registry.extension.BundleCoordinate; +import org.apache.nifi.registry.extension.BundlePersistenceContext; +import org.apache.nifi.registry.extension.BundlePersistenceException; +import org.apache.nifi.registry.extension.BundlePersistenceProvider; +import org.apache.nifi.registry.extension.BundleVersionCoordinate; +import org.apache.nifi.registry.extension.BundleVersionType; +import org.apache.nifi.registry.provider.ProviderConfigurationContext; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +import static org.mockito.Mockito.when; + +public class TestFileSystemBundlePersistenceProvider { + + static final String EXTENSION_STORAGE_DIR = "target/extension_storage"; + + static final ProviderConfigurationContext CONFIGURATION_CONTEXT = new ProviderConfigurationContext() { + @Override + public Map getProperties() { + final Map props = new HashMap<>(); + props.put(FileSystemBundlePersistenceProvider.BUNDLE_STORAGE_DIR_PROP, EXTENSION_STORAGE_DIR); + return props; + } + }; + + private File bundleStorageDir; + private BundlePersistenceProvider fileSystemBundleProvider; + + @Before + public void setup() throws IOException { + bundleStorageDir = new File(EXTENSION_STORAGE_DIR); + if (bundleStorageDir.exists()) { + org.apache.commons.io.FileUtils.cleanDirectory(bundleStorageDir); + bundleStorageDir.delete(); + } + + Assert.assertFalse(bundleStorageDir.exists()); + + fileSystemBundleProvider = new FileSystemBundlePersistenceProvider(); + fileSystemBundleProvider.onConfigured(CONFIGURATION_CONTEXT); + Assert.assertTrue(bundleStorageDir.exists()); + } + + @Test + public void testCreateSuccessfully() throws IOException { + final BundleVersionType type = BundleVersionType.NIFI_NAR; + + // first version in b1 + final String content1 = "g1-a1-1.0.0"; + final BundleVersionCoordinate versionCoordinate1 = getVersionCoordinate("b1", "g1", "a1", "1.0.0", type); + createBundleVersion(fileSystemBundleProvider, versionCoordinate1 , content1); + verifyBundleVersion(bundleStorageDir, versionCoordinate1, content1); + + // second version in b1 + final String content2 = "g1-a1-1.1.0"; + final BundleVersionCoordinate versionCoordinate2 = getVersionCoordinate("b1", "g1", "a1", "1.1.0", type); + createBundleVersion(fileSystemBundleProvider, versionCoordinate2, content2); + verifyBundleVersion(bundleStorageDir, versionCoordinate2, content2); + + // same bundle but in b2 + final String content3 = "g1-a1-1.1.0"; + final BundleVersionCoordinate versionCoordinate3 = getVersionCoordinate("b2", "g1", "a1", "1.1.0", type); + createBundleVersion(fileSystemBundleProvider, versionCoordinate3, content3); + verifyBundleVersion(bundleStorageDir, versionCoordinate3, content2); + } + + @Test + public void testCreateWhenBundleVersionAlreadyExists() throws IOException { + final BundleVersionType type = BundleVersionType.NIFI_NAR; + + final String content1 = "g1-a1-1.0.0"; + final BundleVersionCoordinate versionCoordinate = getVersionCoordinate("b1", "g1", "a1", "1.0.0", type); + createBundleVersion(fileSystemBundleProvider, versionCoordinate, content1); + verifyBundleVersion(bundleStorageDir, versionCoordinate, content1); + + // try to save same bundle version that already exists + try { + final String newContent = "new content"; + createBundleVersion(fileSystemBundleProvider, versionCoordinate, newContent); + Assert.fail("Should have thrown exception"); + } catch (BundlePersistenceException e) { + // expected + } + + // verify existing content wasn't modified + verifyBundleVersion(bundleStorageDir, versionCoordinate, content1); + } + + @Test + public void testUpdateWhenBundleVersionAlreadyExists() throws IOException { + final BundleVersionType type = BundleVersionType.NIFI_NAR; + + final String content1 = "g1-a1-1.0.0"; + final BundleVersionCoordinate versionCoordinate = getVersionCoordinate("b1", "g1", "a1", "1.0.0", type); + createBundleVersion(fileSystemBundleProvider, versionCoordinate, content1); + verifyBundleVersion(bundleStorageDir, versionCoordinate, content1); + + // try to save same bundle version that already exists with new content + final String newContent = "new content"; + updateBundleVersion(fileSystemBundleProvider, versionCoordinate, newContent); + verifyBundleVersion(bundleStorageDir, versionCoordinate, newContent); + + // retrieved content should be updated + try (final OutputStream out = new ByteArrayOutputStream()) { + fileSystemBundleProvider.getBundleVersionContent(versionCoordinate, out); + final String retrievedContent = new String(((ByteArrayOutputStream) out).toByteArray(), StandardCharsets.UTF_8); + Assert.assertEquals(newContent, retrievedContent); + } + } + + @Test + public void testCreateAndGet() throws IOException { + final String bucketId = "b1"; + final String groupId = "g1"; + final String artifactId = "a1"; + final BundleVersionType type = BundleVersionType.NIFI_NAR; + + final String content1 = groupId + "-" + artifactId + "-" + "1.0.0"; + final BundleVersionCoordinate versionCoordinate1 = getVersionCoordinate(bucketId, groupId, artifactId, "1.0.0", type); + createBundleVersion(fileSystemBundleProvider,versionCoordinate1, content1); + + final String content2 = groupId + "-" + artifactId + "-" + "1.1.0"; + final BundleVersionCoordinate versionCoordinate2 = getVersionCoordinate(bucketId, groupId, artifactId, "1.1.0", type); + createBundleVersion(fileSystemBundleProvider, versionCoordinate2, content2); + + try (final OutputStream out = new ByteArrayOutputStream()) { + fileSystemBundleProvider.getBundleVersionContent(versionCoordinate1, out); + + final String retrievedContent1 = new String(((ByteArrayOutputStream) out).toByteArray(), StandardCharsets.UTF_8); + Assert.assertEquals(content1, retrievedContent1); + } + + try (final OutputStream out = new ByteArrayOutputStream()) { + fileSystemBundleProvider.getBundleVersionContent(versionCoordinate2, out); + + final String retrievedContent2 = new String(((ByteArrayOutputStream) out).toByteArray(), StandardCharsets.UTF_8); + Assert.assertEquals(content2, retrievedContent2); + } + } + + @Test(expected = BundlePersistenceException.class) + public void testGetWhenDoesNotExist() throws IOException { + final String bucketId = "b1"; + final String groupId = "g1"; + final String artifactId = "a1"; + final String version = "1.0.0"; + final BundleVersionType type = BundleVersionType.NIFI_NAR; + + try (final OutputStream out = new ByteArrayOutputStream()) { + final BundleVersionCoordinate versionCoordinate = getVersionCoordinate(bucketId, groupId, artifactId, version, type); + fileSystemBundleProvider.getBundleVersionContent(versionCoordinate, out); + Assert.fail("Should have thrown exception"); + } + } + + @Test + public void testDeleteExtensionBundleVersion() throws IOException { + final String bucketId = "b1"; + final String groupId = "g1"; + final String artifactId = "a1"; + final String version = "1.0.0"; + final BundleVersionType bundleType = BundleVersionType.NIFI_NAR; + + final BundleVersionCoordinate versionCoordinate = getVersionCoordinate(bucketId, groupId, artifactId, version, bundleType); + + // create and verify the bundle version + final String content1 = groupId + "-" + artifactId + "-" + version; + createBundleVersion(fileSystemBundleProvider, versionCoordinate, content1); + verifyBundleVersion(bundleStorageDir, versionCoordinate, content1); + + // delete the bundle version + fileSystemBundleProvider.deleteBundleVersion(versionCoordinate); + + // verify it was deleted + final File bundleVersionDir = FileSystemBundlePersistenceProvider.getBundleVersionDirectory(bundleStorageDir, versionCoordinate); + final File bundleFile = FileSystemBundlePersistenceProvider.getBundleFile(bundleVersionDir, versionCoordinate); + Assert.assertFalse(bundleFile.exists()); + } + + @Test + public void testDeleteExtensionBundleVersionWhenDoesNotExist() throws IOException { + final String bucketId = "b1"; + final String groupId = "g1"; + final String artifactId = "a1"; + final String version = "1.0.0"; + final BundleVersionType bundleType = BundleVersionType.NIFI_NAR; + + final BundleVersionCoordinate versionCoordinate = getVersionCoordinate(bucketId, groupId, artifactId, version, bundleType); + + // verify the bundle version does not already exist + final File bundleVersionDir = FileSystemBundlePersistenceProvider.getBundleVersionDirectory(bundleStorageDir, versionCoordinate); + final File bundleFile = FileSystemBundlePersistenceProvider.getBundleFile(bundleVersionDir, versionCoordinate); + Assert.assertFalse(bundleFile.exists()); + + // delete the bundle version + fileSystemBundleProvider.deleteBundleVersion(versionCoordinate); + } + + @Test + public void testDeleteAllBundleVersions() throws IOException { + final String bucketId = "b1"; + final String groupId = "g1"; + final String artifactId = "a1"; + final String version1 = "1.0.0"; + final String version2 = "2.0.0"; + final BundleVersionType bundleType = BundleVersionType.NIFI_NAR; + + // create and verify the bundle version 1 + final String content1 = groupId + "-" + artifactId + "-" + version1; + final BundleVersionCoordinate versionCoordinate1 = getVersionCoordinate(bucketId, groupId, artifactId, version1, bundleType); + createBundleVersion(fileSystemBundleProvider, versionCoordinate1, content1); + verifyBundleVersion(bundleStorageDir, versionCoordinate1, content1); + + // create and verify the bundle version 2 + final String content2 = groupId + "-" + artifactId + "-" + version2; + final BundleVersionCoordinate versionCoordinate2 = getVersionCoordinate(bucketId, groupId, artifactId, version2, bundleType); + createBundleVersion(fileSystemBundleProvider, versionCoordinate2, content2); + verifyBundleVersion(bundleStorageDir, versionCoordinate2, content2); + + Assert.assertEquals(1, bundleStorageDir.listFiles().length); + final BundleCoordinate bundleCoordinate = getBundleCoordinate(bucketId, groupId, artifactId); + fileSystemBundleProvider.deleteAllBundleVersions(bundleCoordinate); + Assert.assertEquals(0, bundleStorageDir.listFiles().length); + } + + @Test + public void testDeleteAllBundleVersionsWhenDoesNotExist() throws IOException { + final String bucketId = "b1"; + final String groupId = "g1"; + final String artifactId = "a1"; + + Assert.assertEquals(0, bundleStorageDir.listFiles().length); + final BundleCoordinate bundleCoordinate = getBundleCoordinate(bucketId, groupId, artifactId); + fileSystemBundleProvider.deleteAllBundleVersions(bundleCoordinate); + Assert.assertEquals(0, bundleStorageDir.listFiles().length); + } + + private void createBundleVersion(final BundlePersistenceProvider persistenceProvider, + final BundleVersionCoordinate versionCoordinate, + final String content) throws IOException { + final BundlePersistenceContext context = getPersistenceContext(versionCoordinate); + try (final InputStream in = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))) { + persistenceProvider.createBundleVersion(context, in); + } + } + + private void updateBundleVersion(final BundlePersistenceProvider persistenceProvider, + final BundleVersionCoordinate versionCoordinate, + final String content) throws IOException { + final BundlePersistenceContext context = getPersistenceContext(versionCoordinate); + try (final InputStream in = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))) { + persistenceProvider.updateBundleVersion(context, in); + } + } + + private static BundlePersistenceContext getPersistenceContext(final BundleVersionCoordinate versionCoordinate) { + final BundlePersistenceContext context = Mockito.mock(BundlePersistenceContext.class); + when(context.getCoordinate()).thenReturn(versionCoordinate); + return context; + } + + private static BundleVersionCoordinate getVersionCoordinate(final String bucketId, final String groupId, final String artifactId, + final String version, final BundleVersionType bundleType) { + + final BundleVersionCoordinate coordinate = Mockito.mock(BundleVersionCoordinate.class); + when(coordinate.getBucketId()).thenReturn(bucketId); + when(coordinate.getGroupId()).thenReturn(groupId); + when(coordinate.getArtifactId()).thenReturn(artifactId); + when(coordinate.getVersion()).thenReturn(version); + when(coordinate.getType()).thenReturn(bundleType); + return coordinate; + } + + private static BundleCoordinate getBundleCoordinate(final String bucketId, final String groupId, final String artifactId) { + final BundleCoordinate coordinate = Mockito.mock(BundleCoordinate.class); + when(coordinate.getBucketId()).thenReturn(bucketId); + when(coordinate.getGroupId()).thenReturn(groupId); + when(coordinate.getArtifactId()).thenReturn(artifactId); + return coordinate; + } + + private static void verifyBundleVersion(final File storageDir, final BundleVersionCoordinate versionCoordinate, + final String contentString) throws IOException { + + final File bundleVersionDir = FileSystemBundlePersistenceProvider.getBundleVersionDirectory(storageDir, versionCoordinate); + final File bundleFile = FileSystemBundlePersistenceProvider.getBundleFile(bundleVersionDir, versionCoordinate); + Assert.assertTrue(bundleFile.exists()); + + try (InputStream in = new FileInputStream(bundleFile)) { + Assert.assertEquals(contentString, IOUtils.toString(in, StandardCharsets.UTF_8)); + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/TestDatabaseFlowPersistenceProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/TestDatabaseFlowPersistenceProvider.java new file mode 100644 index 0000000000..5851b21805 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/TestDatabaseFlowPersistenceProvider.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider.flow; + +import org.apache.nifi.registry.db.DatabaseBaseTest; +import org.apache.nifi.registry.flow.FlowPersistenceProvider; +import org.apache.nifi.registry.flow.FlowSnapshotContext; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.sql.DataSource; +import java.nio.charset.StandardCharsets; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.when; + +public class TestDatabaseFlowPersistenceProvider extends DatabaseBaseTest { + + @Autowired + private DataSource dataSource; + + private FlowPersistenceProvider persistenceProvider; + + @Before + public void setup() { + persistenceProvider = new DatabaseFlowPersistenceProvider(); + ((DatabaseFlowPersistenceProvider)persistenceProvider).setDataSource(dataSource); + } + + @Test + public void testAll() { + // Save two versions of a flow... + final FlowSnapshotContext context1 = getFlowSnapshotContext("b1", "f1", 1); + final byte[] content1 = "f1v1".getBytes(StandardCharsets.UTF_8); + persistenceProvider.saveFlowContent(context1, content1); + + final FlowSnapshotContext context2 = getFlowSnapshotContext("b1", "f1", 2); + final byte[] content2 = "f1v2".getBytes(StandardCharsets.UTF_8); + persistenceProvider.saveFlowContent(context2, content2); + + // Verify we can retrieve both versions and that the content is correct + final byte[] retrievedContent1 = persistenceProvider.getFlowContent(context1.getBucketId(), context1.getFlowId(), context1.getVersion()); + assertNotNull(retrievedContent1); + assertEquals("f1v1", new String(retrievedContent1, StandardCharsets.UTF_8)); + + final byte[] retrievedContent2 = persistenceProvider.getFlowContent(context2.getBucketId(), context2.getFlowId(), context2.getVersion()); + assertNotNull(retrievedContent2); + assertEquals("f1v2", new String(retrievedContent2, StandardCharsets.UTF_8)); + + // Delete a specific version and verify we can longer retrieve it + persistenceProvider.deleteFlowContent(context1.getBucketId(), context1.getFlowId(), context1.getVersion()); + + final byte[] deletedContent1 = persistenceProvider.getFlowContent(context1.getBucketId(), context1.getFlowId(), context1.getVersion()); + assertNull(deletedContent1); + + // Delete all content for a flow + persistenceProvider.deleteAllFlowContent(context1.getBucketId(), context1.getFlowId()); + + final byte[] deletedContent2 = persistenceProvider.getFlowContent(context2.getBucketId(), context2.getFlowId(), context2.getVersion()); + assertNull(deletedContent2); + } + + private FlowSnapshotContext getFlowSnapshotContext(final String bucketId, final String flowId, final int version) { + final FlowSnapshotContext context = Mockito.mock(FlowSnapshotContext.class); + when(context.getBucketId()).thenReturn(bucketId); + when(context.getFlowId()).thenReturn(flowId); + when(context.getVersion()).thenReturn(version); + return context; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/TestFileSystemFlowPersistenceProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/TestFileSystemFlowPersistenceProvider.java new file mode 100644 index 0000000000..14a4861380 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/TestFileSystemFlowPersistenceProvider.java @@ -0,0 +1,204 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider.flow; + +import org.apache.commons.io.IOUtils; +import org.apache.nifi.registry.flow.FlowPersistenceProvider; +import org.apache.nifi.registry.flow.FlowSnapshotContext; +import org.apache.nifi.registry.provider.ProviderConfigurationContext; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +import static org.mockito.Mockito.when; + +public class TestFileSystemFlowPersistenceProvider { + + static final String FLOW_STORAGE_DIR = "target/flow_storage"; + + static final ProviderConfigurationContext CONFIGURATION_CONTEXT = new ProviderConfigurationContext() { + @Override + public Map getProperties() { + final Map props = new HashMap<>(); + props.put(FileSystemFlowPersistenceProvider.FLOW_STORAGE_DIR_PROP, FLOW_STORAGE_DIR); + return props; + } + }; + + private File flowStorageDir; + private FileSystemFlowPersistenceProvider fileSystemFlowProvider; + + @Before + public void setup() throws IOException { + flowStorageDir = new File(FLOW_STORAGE_DIR); + if (flowStorageDir.exists()) { + org.apache.commons.io.FileUtils.cleanDirectory(flowStorageDir); + flowStorageDir.delete(); + } + + Assert.assertFalse(flowStorageDir.exists()); + + fileSystemFlowProvider = new FileSystemFlowPersistenceProvider(); + fileSystemFlowProvider.onConfigured(CONFIGURATION_CONTEXT); + Assert.assertTrue(flowStorageDir.exists()); + } + + @Test + public void testSaveSuccessfully() throws IOException { + createAndSaveSnapshot(fileSystemFlowProvider,"bucket1", "flow1", 1, "flow1v1"); + verifySnapshot(flowStorageDir, "bucket1", "flow1", 1, "flow1v1"); + + createAndSaveSnapshot(fileSystemFlowProvider,"bucket1", "flow1", 2, "flow1v2"); + verifySnapshot(flowStorageDir, "bucket1", "flow1", 2, "flow1v2"); + + createAndSaveSnapshot(fileSystemFlowProvider,"bucket1", "flow2", 1, "flow2v1"); + verifySnapshot(flowStorageDir, "bucket1", "flow2", 1, "flow2v1"); + + createAndSaveSnapshot(fileSystemFlowProvider,"bucket2", "flow3", 1, "flow3v1"); + verifySnapshot(flowStorageDir, "bucket2", "flow3", 1, "flow3v1"); + } + + @Test + public void testSaveWithExistingVersion() throws IOException { + final FlowSnapshotContext context = Mockito.mock(FlowSnapshotContext.class); + when(context.getBucketId()).thenReturn("bucket1"); + when(context.getFlowId()).thenReturn("flow1"); + when(context.getVersion()).thenReturn(1); + + final byte[] content = "flow1v1".getBytes(StandardCharsets.UTF_8); + fileSystemFlowProvider.saveFlowContent(context, content); + + // save new content for an existing version + final byte[] content2 = "XXX".getBytes(StandardCharsets.UTF_8); + try { + fileSystemFlowProvider.saveFlowContent(context, content2); + Assert.fail("Should have thrown exception"); + } catch (Exception e) { + + } + + // verify the new content wasn't written + final File flowSnapshotFile = new File(flowStorageDir, "bucket1/flow1/1/1" + FileSystemFlowPersistenceProvider.SNAPSHOT_EXTENSION); + try (InputStream in = new FileInputStream(flowSnapshotFile)) { + Assert.assertEquals("flow1v1", IOUtils.toString(in, StandardCharsets.UTF_8)); + } + } + + @Test + public void testSaveAndGet() throws IOException { + createAndSaveSnapshot(fileSystemFlowProvider,"bucket1", "flow1", 1, "flow1v1"); + createAndSaveSnapshot(fileSystemFlowProvider,"bucket1", "flow1", 2, "flow1v2"); + + final byte[] flow1v1 = fileSystemFlowProvider.getFlowContent("bucket1", "flow1", 1); + Assert.assertEquals("flow1v1", new String(flow1v1, StandardCharsets.UTF_8)); + + final byte[] flow1v2 = fileSystemFlowProvider.getFlowContent("bucket1", "flow1", 2); + Assert.assertEquals("flow1v2", new String(flow1v2, StandardCharsets.UTF_8)); + } + + @Test + public void testGetWhenDoesNotExist() { + final byte[] flow1v1 = fileSystemFlowProvider.getFlowContent("bucket1", "flow1", 1); + Assert.assertNull(flow1v1); + } + + @Test + public void testDeleteSnapshots() throws IOException { + final String bucketId = "bucket1"; + final String flowId = "flow1"; + + createAndSaveSnapshot(fileSystemFlowProvider, bucketId, flowId, 1, "flow1v1"); + createAndSaveSnapshot(fileSystemFlowProvider, bucketId, flowId, 2, "flow1v2"); + + Assert.assertNotNull(fileSystemFlowProvider.getFlowContent(bucketId, flowId, 1)); + Assert.assertNotNull(fileSystemFlowProvider.getFlowContent(bucketId, flowId, 2)); + + fileSystemFlowProvider.deleteAllFlowContent(bucketId, flowId); + + Assert.assertNull(fileSystemFlowProvider.getFlowContent(bucketId, flowId, 1)); + Assert.assertNull(fileSystemFlowProvider.getFlowContent(bucketId, flowId, 2)); + + // delete a flow that doesn't exist + fileSystemFlowProvider.deleteAllFlowContent(bucketId, "some-other-flow"); + + // delete a bucket that doesn't exist + fileSystemFlowProvider.deleteAllFlowContent("some-other-bucket", flowId); + } + + @Test + public void testDeleteSnapshot() throws IOException { + final String bucketId = "bucket1"; + final String flowId = "flow1"; + + createAndSaveSnapshot(fileSystemFlowProvider, bucketId, flowId, 1, "flow1v1"); + createAndSaveSnapshot(fileSystemFlowProvider, bucketId, flowId, 2, "flow1v2"); + + Assert.assertNotNull(fileSystemFlowProvider.getFlowContent(bucketId, flowId, 1)); + Assert.assertNotNull(fileSystemFlowProvider.getFlowContent(bucketId, flowId, 2)); + + fileSystemFlowProvider.deleteFlowContent(bucketId, flowId, 1); + + Assert.assertNull(fileSystemFlowProvider.getFlowContent(bucketId, flowId, 1)); + Assert.assertNotNull(fileSystemFlowProvider.getFlowContent(bucketId, flowId, 2)); + + fileSystemFlowProvider.deleteFlowContent(bucketId, flowId, 2); + + Assert.assertNull(fileSystemFlowProvider.getFlowContent(bucketId, flowId, 1)); + Assert.assertNull(fileSystemFlowProvider.getFlowContent(bucketId, flowId, 2)); + + // delete a version that doesn't exist + fileSystemFlowProvider.deleteFlowContent(bucketId, flowId, 3); + + // delete a flow that doesn't exist + fileSystemFlowProvider.deleteFlowContent(bucketId, "some-other-flow", 1); + + // delete a bucket that doesn't exist + fileSystemFlowProvider.deleteFlowContent("some-other-bucket", flowId, 1); + } + + private void createAndSaveSnapshot(final FlowPersistenceProvider flowPersistenceProvider, final String bucketId, final String flowId, final int version, + final String contentString) throws IOException { + final FlowSnapshotContext context = Mockito.mock(FlowSnapshotContext.class); + when(context.getBucketId()).thenReturn(bucketId); + when(context.getFlowId()).thenReturn(flowId); + when(context.getVersion()).thenReturn(version); + + final byte[] content = contentString.getBytes(StandardCharsets.UTF_8); + flowPersistenceProvider.saveFlowContent(context, content); + } + + private void verifySnapshot(final File flowStorageDir, final String bucketId, final String flowId, final int version, + final String contentString) throws IOException { + // verify the correct snapshot file was created + final File flowSnapshotFile = new File(flowStorageDir, + bucketId + "/" + flowId + "/" + version + "/" + version + FileSystemFlowPersistenceProvider.SNAPSHOT_EXTENSION); + Assert.assertTrue(flowSnapshotFile.exists()); + + try (InputStream in = new FileInputStream(flowSnapshotFile)) { + Assert.assertEquals(contentString, IOUtils.toString(in, StandardCharsets.UTF_8)); + } + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/TestFlowMetadataSynchronizer.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/TestFlowMetadataSynchronizer.java new file mode 100644 index 0000000000..6ee5e62e7a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/TestFlowMetadataSynchronizer.java @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider.flow; + +import org.apache.nifi.registry.db.entity.BucketEntity; +import org.apache.nifi.registry.db.entity.FlowEntity; +import org.apache.nifi.registry.db.entity.FlowSnapshotEntity; +import org.apache.nifi.registry.flow.FlowPersistenceProvider; +import org.apache.nifi.registry.flow.MetadataAwareFlowPersistenceProvider; +import org.apache.nifi.registry.metadata.BucketMetadata; +import org.apache.nifi.registry.metadata.FlowMetadata; +import org.apache.nifi.registry.metadata.FlowSnapshotMetadata; +import org.apache.nifi.registry.service.MetadataService; +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class TestFlowMetadataSynchronizer { + + private MetadataService metadataService; + private MetadataAwareFlowPersistenceProvider metadataAwareflowPersistenceProvider; + private FlowPersistenceProvider standardFlowPersistenceProvider; + private List metadata; + private FlowMetadataSynchronizer synchronizer; + + @Before + public void setup() { + metadataService = mock(MetadataService.class); + metadataAwareflowPersistenceProvider = mock(MetadataAwareFlowPersistenceProvider.class); + standardFlowPersistenceProvider = mock(FlowPersistenceProvider.class); + synchronizer = new FlowMetadataSynchronizer(metadataService, metadataAwareflowPersistenceProvider); + + final FlowSnapshotMetadata snapshotMetadata1 = new FlowSnapshotMetadata(); + snapshotMetadata1.setVersion(1); + snapshotMetadata1.setAuthor("user1"); + snapshotMetadata1.setComments("This is v1"); + snapshotMetadata1.setCreated(System.currentTimeMillis()); + + final FlowSnapshotMetadata snapshotMetadata2 = new FlowSnapshotMetadata(); + snapshotMetadata2.setVersion(2); + snapshotMetadata2.setAuthor("user1"); + snapshotMetadata2.setComments("This is v2"); + snapshotMetadata2.setCreated(System.currentTimeMillis()); + + final List snapshotMetadata = Arrays.asList(snapshotMetadata1, snapshotMetadata2); + + final FlowMetadata flowMetadata1 = new FlowMetadata(); + flowMetadata1.setIdentifier("1"); + flowMetadata1.setName("Flow 1"); + flowMetadata1.setDescription("This is flow 1"); + flowMetadata1.setFlowSnapshotMetadata(snapshotMetadata); + + final List flowMetadata = Arrays.asList(flowMetadata1); + + final BucketMetadata bucketMetadata = new BucketMetadata(); + bucketMetadata.setIdentifier("1"); + bucketMetadata.setName("Bucket 1"); + bucketMetadata.setDescription("This is bucket 1"); + bucketMetadata.setFlowMetadata(flowMetadata); + + metadata = Arrays.asList(bucketMetadata); + when(metadataAwareflowPersistenceProvider.getMetadata()).thenReturn(metadata); + } + + @Test + public void testWhenMetadataAwareAndHasDataShouldSynchronize() { + when(metadataService.getAllBuckets()).thenReturn(Collections.emptyList()); + + synchronizer.synchronize(); + verify(metadataService, times(1)).createBucket(any(BucketEntity.class)); + verify(metadataService, times(1)).createFlow(any(FlowEntity.class)); + verify(metadataService, times(2)).createFlowSnapshot(any(FlowSnapshotEntity.class)); + } + + @Test + public void testWhenMetadataAwareAndDatabaseNotEmptyShouldNotSynchronize() { + final BucketEntity bucketEntity = new BucketEntity(); + bucketEntity.setId("1"); + when(metadataService.getAllBuckets()).thenReturn(Collections.singletonList(bucketEntity)); + + synchronizer.synchronize(); + verify(metadataService, times(0)).createBucket(any(BucketEntity.class)); + verify(metadataService, times(0)).createFlow(any(FlowEntity.class)); + verify(metadataService, times(0)).createFlowSnapshot(any(FlowSnapshotEntity.class)); + } + + @Test + public void testWhenNotMetadataAwareShouldNotSynchronize() { + when(metadataService.getAllBuckets()).thenReturn(Collections.emptyList()); + synchronizer = new FlowMetadataSynchronizer(metadataService, standardFlowPersistenceProvider); + synchronizer.synchronize(); + + verify(metadataService, times(0)).createBucket(any(BucketEntity.class)); + verify(metadataService, times(0)).createFlow(any(FlowEntity.class)); + verify(metadataService, times(0)).createFlowSnapshot(any(FlowSnapshotEntity.class)); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/TestStandardFlowSnapshotContext.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/TestStandardFlowSnapshotContext.java new file mode 100644 index 0000000000..bff2ef9153 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/TestStandardFlowSnapshotContext.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider.flow; + +import org.apache.nifi.registry.flow.FlowSnapshotContext; +import org.junit.Assert; +import org.junit.Test; + +public class TestStandardFlowSnapshotContext { + + @Test + public void testBuilder() { + final String bucketId = "1234-1234-1234-1234"; + final String bucketName = "Some Bucket"; + final String flowId = "2345-2345-2345-2345"; + final String flowName = "Some Flow"; + final int version = 2; + final String comments = "Some Comments"; + final String author = "anonymous"; + final long timestamp = System.currentTimeMillis(); + + final FlowSnapshotContext context = new StandardFlowSnapshotContext.Builder() + .bucketId(bucketId) + .bucketName(bucketName) + .flowId(flowId) + .flowName(flowName) + .version(version) + .comments(comments) + .author(author) + .snapshotTimestamp(timestamp) + .build(); + + Assert.assertEquals(bucketId, context.getBucketId()); + Assert.assertEquals(bucketName, context.getBucketName()); + Assert.assertEquals(flowId, context.getFlowId()); + Assert.assertEquals(flowName, context.getFlowName()); + Assert.assertEquals(version, context.getVersion()); + Assert.assertEquals(comments, context.getComments()); + Assert.assertEquals(author, context.getAuthor()); + Assert.assertEquals(timestamp, context.getSnapshotTimestamp()); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/git/TestGitFlowPersistenceProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/git/TestGitFlowPersistenceProvider.java new file mode 100644 index 0000000000..45351abc14 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/git/TestGitFlowPersistenceProvider.java @@ -0,0 +1,290 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider.flow.git; + +import org.apache.nifi.registry.flow.FlowPersistenceException; +import org.apache.nifi.registry.provider.ProviderConfigurationContext; +import org.apache.nifi.registry.provider.ProviderCreationException; +import org.apache.nifi.registry.provider.StandardProviderConfigurationContext; +import org.apache.nifi.registry.provider.flow.StandardFlowSnapshotContext; +import org.apache.nifi.registry.util.FileUtils; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.StoredConfig; +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public class TestGitFlowPersistenceProvider { + + private static final Logger logger = LoggerFactory.getLogger(TestGitFlowPersistenceProvider.class); + + private void assertCreationFailure(final Map properties, final Consumer assertion) { + final GitFlowPersistenceProvider persistenceProvider = new GitFlowPersistenceProvider(); + + try { + final ProviderConfigurationContext configurationContext = new StandardProviderConfigurationContext(properties); + persistenceProvider.onConfigured(configurationContext); + fail("Should fail"); + } catch (ProviderCreationException e) { + assertion.accept(e); + } + } + + @Test + public void testNoFlowStorageDirSpecified() { + final Map properties = new HashMap<>(); + assertCreationFailure(properties, + e -> assertEquals("The property Flow Storage Directory must be provided", e.getMessage())); + } + + @Test + public void testLoadNonExistingDir() { + final Map properties = new HashMap<>(); + properties.put(GitFlowPersistenceProvider.FLOW_STORAGE_DIR_PROP, "target/non-existing"); + assertCreationFailure(properties, + e -> assertEquals("'target/non-existing' is not a directory or does not exist.", e.getCause().getMessage())); + } + + @Test + public void testLoadNonGitDir() { + final Map properties = new HashMap<>(); + properties.put(GitFlowPersistenceProvider.FLOW_STORAGE_DIR_PROP, "target"); + assertCreationFailure(properties, + e -> assertEquals("Directory 'target' does not contain a .git directory." + + " Please init and configure the directory with 'git init' command before using it from NiFi Registry.", + e.getCause().getMessage())); + } + + @FunctionalInterface + private interface GitConsumer { + void accept(Git git) throws GitAPIException; + } + + private void assertProvider(final Map properties, final GitConsumer gitConsumer, final Consumer assertion, boolean deleteDir) + throws IOException, GitAPIException { + + final File gitDir = new File(properties.get(GitFlowPersistenceProvider.FLOW_STORAGE_DIR_PROP)); + try { + FileUtils.ensureDirectoryExistAndCanReadAndWrite(gitDir); + + try (final Git git = Git.init().setDirectory(gitDir).call()) { + logger.debug("Initiated a git repository {}", git); + final StoredConfig config = git.getRepository().getConfig(); + config.setString("user", null, "name", "git-user"); + config.setString("user", null, "email", "git-user@example.com"); + config.save(); + gitConsumer.accept(git); + } + + final GitFlowPersistenceProvider persistenceProvider = new GitFlowPersistenceProvider(); + + final ProviderConfigurationContext configurationContext = new StandardProviderConfigurationContext(properties); + persistenceProvider.onConfigured(configurationContext); + assertion.accept(persistenceProvider); + + } finally { + if (deleteDir) { + FileUtils.deleteFile(gitDir, true); + } + } + } + + @Test + public void testLoadEmptyGitDir() throws GitAPIException, IOException { + final Map properties = new HashMap<>(); + properties.put(GitFlowPersistenceProvider.FLOW_STORAGE_DIR_PROP, "target/empty-git"); + + assertProvider(properties, g -> {}, p -> { + try { + p.getFlowContent("bucket-id-A", "flow-id-1", 1); + } catch (FlowPersistenceException e) { + assertEquals("Bucket ID bucket-id-A was not found.", e.getMessage()); + } + }, true); + } + + @Test + public void testLoadCommitHistories() throws GitAPIException, IOException { + final Map properties = new HashMap<>(); + properties.put(GitFlowPersistenceProvider.FLOW_STORAGE_DIR_PROP, "target/repo-with-histories"); + + assertProvider(properties, g -> {}, p -> { + // Create some Flows and keep the directory. + final StandardFlowSnapshotContext.Builder contextBuilder = new StandardFlowSnapshotContext.Builder() + .bucketId("bucket-id-A") + .bucketName("C'est/Bucket A/です。") + .flowId("flow-id-1") + .flowName("テスト_用/フロー#1\\[contains invalid chars]") + .author("unit-test-user") + .comments("Initial commit.") + .snapshotTimestamp(new Date().getTime()) + .version(1); + + final byte[] flow1Ver1 = "Flow1 ver.1".getBytes(StandardCharsets.UTF_8); + p.saveFlowContent(contextBuilder.build(), flow1Ver1); + + contextBuilder.comments("2nd commit.").version(2); + final byte[] flow1Ver2 = "Flow1 ver.2".getBytes(StandardCharsets.UTF_8); + p.saveFlowContent(contextBuilder.build(), flow1Ver2); + + // Rename flow. + contextBuilder.flowName("FlowOne").comments("3rd commit.").version(3); + final byte[] flow1Ver3 = "FlowOne ver.3".getBytes(StandardCharsets.UTF_8); + p.saveFlowContent(contextBuilder.build(), flow1Ver3); + + // Adding another flow. + contextBuilder.flowId("flow-id-2").flowName("FlowTwo").comments("4th commit.").version(1); + final byte[] flow2Ver1 = "FlowTwo ver.1".getBytes(StandardCharsets.UTF_8); + p.saveFlowContent(contextBuilder.build(), flow2Ver1); + + // Rename bucket. + contextBuilder.bucketName("New name for Bucket A").comments("5th commit.").version(2); + final byte[] flow2Ver2 = "FlowTwo ver.2".getBytes(StandardCharsets.UTF_8); + p.saveFlowContent(contextBuilder.build(), flow2Ver2); + + + }, false); + + assertProvider(properties, g -> { + // Assert commit. + final AtomicInteger commitCount = new AtomicInteger(0); + final String[] commitMessages = { + "5th commit.\n\nBy NiFi Registry user: unit-test-user", + "4th commit.\n\nBy NiFi Registry user: unit-test-user", + "3rd commit.\n\nBy NiFi Registry user: unit-test-user", + "2nd commit.\n\nBy NiFi Registry user: unit-test-user", + "Initial commit.\n\nBy NiFi Registry user: unit-test-user" + }; + for (RevCommit commit : g.log().call()) { + assertEquals("git-user", commit.getAuthorIdent().getName()); + final int commitIndex = commitCount.getAndIncrement(); + assertEquals(commitMessages[commitIndex], commit.getFullMessage()); + } + assertEquals(commitMessages.length, commitCount.get()); + }, p -> { + // Should be able to load flow from commit histories. + final byte[] flow1Ver1 = p.getFlowContent("bucket-id-A", "flow-id-1", 1); + assertEquals("Flow1 ver.1", new String(flow1Ver1, StandardCharsets.UTF_8)); + + final byte[] flow1Ver2 = p.getFlowContent("bucket-id-A", "flow-id-1", 2); + assertEquals("Flow1 ver.2", new String(flow1Ver2, StandardCharsets.UTF_8)); + + // Even if the name of flow has been changed, it can be retrieved by the same flow id. + final byte[] flow1Ver3 = p.getFlowContent("bucket-id-A", "flow-id-1", 3); + assertEquals("FlowOne ver.3", new String(flow1Ver3, StandardCharsets.UTF_8)); + + final byte[] flow2Ver1 = p.getFlowContent("bucket-id-A", "flow-id-2", 1); + assertEquals("FlowTwo ver.1", new String(flow2Ver1, StandardCharsets.UTF_8)); + + // Even if the name of bucket has been changed, it can be retrieved by the same flow id. + final byte[] flow2Ver2 = p.getFlowContent("bucket-id-A", "flow-id-2", 2); + assertEquals("FlowTwo ver.2", new String(flow2Ver2, StandardCharsets.UTF_8)); + + // Delete the 2nd flow. + p.deleteAllFlowContent("bucket-id-A", "flow-id-2"); + + }, false); + + assertProvider(properties, g -> { + // Assert commit. + final AtomicInteger commitCount = new AtomicInteger(0); + final String[] commitMessages = { + "Deleted flow FlowTwo.snapshot:flow-id-2 in bucket New_name_for_Bucket_A:bucket-id-A.", + "5th commit.", + "4th commit.", + "3rd commit.", + "2nd commit.", + "Initial commit." + }; + for (RevCommit commit : g.log().call()) { + assertEquals("git-user", commit.getAuthorIdent().getName()); + final int commitIndex = commitCount.getAndIncrement(); + assertEquals(commitMessages[commitIndex], commit.getShortMessage()); + } + assertEquals(commitMessages.length, commitCount.get()); + }, p -> { + // Should be able to load flow from commit histories. + final byte[] flow1Ver1 = p.getFlowContent("bucket-id-A", "flow-id-1", 1); + assertEquals("Flow1 ver.1", new String(flow1Ver1, StandardCharsets.UTF_8)); + + final byte[] flow1Ver2 = p.getFlowContent("bucket-id-A", "flow-id-1", 2); + assertEquals("Flow1 ver.2", new String(flow1Ver2, StandardCharsets.UTF_8)); + + // Even if the name of flow has been changed, it can be retrieved by the same flow id. + final byte[] flow1Ver3 = p.getFlowContent("bucket-id-A", "flow-id-1", 3); + assertEquals("FlowOne ver.3", new String(flow1Ver3, StandardCharsets.UTF_8)); + + // The 2nd flow has been deleted, and should not exist. + try { + p.getFlowContent("bucket-id-A", "flow-id-2", 1); + } catch (FlowPersistenceException e) { + assertEquals("Flow ID flow-id-2 was not found in bucket New_name_for_Bucket_A:bucket-id-A.", e.getMessage()); + } + + try { + p.getFlowContent("bucket-id-A", "flow-id-2", 2); + } catch (FlowPersistenceException e) { + assertEquals("Flow ID flow-id-2 was not found in bucket New_name_for_Bucket_A:bucket-id-A.", e.getMessage()); + } + + // Delete the 1st flow, too. + p.deleteAllFlowContent("bucket-id-A", "flow-id-1"); + + }, false); + + assertProvider(properties, g -> { + // Assert commit. + final AtomicInteger commitCount = new AtomicInteger(0); + final String[] commitMessages = { + "Deleted flow FlowOne.snapshot:flow-id-1 in bucket New_name_for_Bucket_A:bucket-id-A.", + "Deleted flow FlowTwo.snapshot:flow-id-2 in bucket New_name_for_Bucket_A:bucket-id-A.", + "5th commit.", + "4th commit.", + "3rd commit.", + "2nd commit.", + "Initial commit." + }; + for (RevCommit commit : g.log().call()) { + assertEquals("git-user", commit.getAuthorIdent().getName()); + final int commitIndex = commitCount.getAndIncrement(); + assertEquals(commitMessages[commitIndex], commit.getShortMessage()); + } + assertEquals(commitMessages.length, commitCount.get()); + }, p -> { + // The 1st flow has been deleted, and should not exist. Moreover, the bucket A has been deleted since there's no flow. + try { + p.getFlowContent("bucket-id-A", "flow-id-1", 1); + } catch (FlowPersistenceException e) { + assertEquals("Bucket ID bucket-id-A was not found.", e.getMessage()); + } + }, true); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/hook/TestScriptEventHookProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/hook/TestScriptEventHookProvider.java new file mode 100644 index 0000000000..39d45ae39f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/hook/TestScriptEventHookProvider.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider.hook; + +import org.apache.nifi.registry.extension.ExtensionClassLoader; +import org.apache.nifi.registry.extension.ExtensionManager; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.apache.nifi.registry.provider.ProviderCreationException; +import org.apache.nifi.registry.provider.ProviderFactory; +import org.apache.nifi.registry.provider.StandardProviderFactory; +import org.junit.Test; +import org.mockito.Mockito; + +import javax.sql.DataSource; + +import java.net.URL; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +public class TestScriptEventHookProvider { + + @Test(expected = ProviderCreationException.class) + public void testBadScriptProvider() { + final NiFiRegistryProperties props = new NiFiRegistryProperties(); + props.setProperty(NiFiRegistryProperties.PROVIDERS_CONFIGURATION_FILE, "src/test/resources/provider/hook/bad-script-provider.xml"); + + final ExtensionManager extensionManager = Mockito.mock(ExtensionManager.class); + when(extensionManager.getExtensionClassLoader(any(String.class))) + .thenReturn(new ExtensionClassLoader("/tmp", new URL[0],this.getClass().getClassLoader())); + + final DataSource dataSource = Mockito.mock(DataSource.class); + + final ProviderFactory providerFactory = new StandardProviderFactory(props, extensionManager, dataSource); + providerFactory.initialize(); + providerFactory.getEventHookProviders(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/authorization/TestStandardAuthorizableLookup.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/authorization/TestStandardAuthorizableLookup.java new file mode 100644 index 0000000000..2804ac730e --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/authorization/TestStandardAuthorizableLookup.java @@ -0,0 +1,404 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException; +import org.apache.nifi.registry.security.authorization.resource.Authorizable; +import org.apache.nifi.registry.security.authorization.resource.ResourceFactory; +import org.apache.nifi.registry.security.authorization.user.NiFiUser; +import org.apache.nifi.registry.security.authorization.user.StandardNiFiUser; +import org.apache.nifi.registry.service.RegistryService; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentMatcher; + +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class TestStandardAuthorizableLookup { + + private static final NiFiUser USER_NO_PROXY_CHAIN = new StandardNiFiUser.Builder() + .identity("user1") + .build(); + + private static final NiFiUser USER_WITH_PROXY_CHAIN = new StandardNiFiUser.Builder() + .identity("user1") + .chain(new StandardNiFiUser.Builder().identity("CN=localhost, OU=NIFI").build()) + .build(); + + private Authorizer authorizer; + private RegistryService registryService; + private AuthorizableLookup authorizableLookup; + + private Bucket bucketPublic; + private Bucket bucketNotPublic; + + @Before + public void setup() { + authorizer = mock(Authorizer.class); + registryService = mock(RegistryService.class); + authorizableLookup = new StandardAuthorizableLookup(registryService); + + bucketPublic = new Bucket(); + bucketPublic.setIdentifier(UUID.randomUUID().toString()); + bucketPublic.setName("Public Bucket"); + bucketPublic.setAllowPublicRead(true); + + bucketNotPublic = new Bucket(); + bucketNotPublic.setIdentifier(UUID.randomUUID().toString()); + bucketNotPublic.setName("Non Public Bucket"); + bucketNotPublic.setAllowPublicRead(false); + + when(registryService.getBucket(bucketPublic.getIdentifier())).thenReturn(bucketPublic); + when(registryService.getBucket(bucketNotPublic.getIdentifier())).thenReturn(bucketNotPublic); + } + + // Test check method for Bucket Authorizable + + @Test + public void testCheckReadPublicBucketWithNoProxyChain() { + final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier()); + final AuthorizationResult result = bucketAuthorizable.checkAuthorization(authorizer, RequestAction.READ, USER_NO_PROXY_CHAIN); + assertNotNull(result); + assertEquals(AuthorizationResult.Result.Approved, result.getResult()); + + // Should never call authorizer because resource is public + verify(authorizer, times(0)).authorize(any(AuthorizationRequest.class)); + } + + @Test + public void testCheckReadPublicBucketWithProxyChain() { + final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier()); + final AuthorizationResult result = bucketAuthorizable.checkAuthorization(authorizer, RequestAction.READ, USER_WITH_PROXY_CHAIN); + assertNotNull(result); + assertEquals(AuthorizationResult.Result.Approved, result.getResult()); + + // Should never call authorizer because resource is public + verify(authorizer, times(0)).authorize(any(AuthorizationRequest.class)); + } + + @Test + public void testCheckWritePublicBucketWithUnauthorizedUserAndNoProxyChain() { + final RequestAction action = RequestAction.WRITE; + + // first request will be to the specific bucket + final AuthorizationRequest expectedBucketAuthorizationRequest = getBucketAuthorizationRequest( + bucketPublic.getIdentifier(), action, USER_NO_PROXY_CHAIN); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedBucketAuthorizationRequest)))) + .thenReturn(AuthorizationResult.denied()); + + // second request will go to parent of /buckets + final AuthorizationRequest expectedBucketsAuthorizationRequest = getBucketsAuthorizationRequest(action, USER_NO_PROXY_CHAIN); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedBucketsAuthorizationRequest)))) + .thenReturn(AuthorizationResult.denied()); + + // should reach authorizer and return denied + final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier()); + final AuthorizationResult result = bucketAuthorizable.checkAuthorization(authorizer, action, USER_NO_PROXY_CHAIN); + assertNotNull(result); + assertEquals(AuthorizationResult.Result.Denied, result.getResult()); + + // Should call authorizer twice for specific bucket and top-level /buckets + verify(authorizer, times(2)).authorize(any(AuthorizationRequest.class)); + } + + @Test + public void testCheckWritePublicBucketWithUnauthorizedProxyChain() { + final RequestAction action = RequestAction.WRITE; + + // first request will be to authorize the proxy + final AuthorizationRequest expectedProxyAuthorizationRequest = getProxyAuthorizationRequest(action, USER_WITH_PROXY_CHAIN.getChain()); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedProxyAuthorizationRequest)))) + .thenReturn(AuthorizationResult.denied()); + + // the authorization of the proxy chain should return denied + final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier()); + final AuthorizationResult result = bucketAuthorizable.checkAuthorization(authorizer, action, USER_WITH_PROXY_CHAIN); + assertNotNull(result); + assertEquals(AuthorizationResult.Result.Denied, result.getResult()); + + // Should never call authorizer once for /proxy and then return denied + verify(authorizer, times(1)).authorize(any(AuthorizationRequest.class)); + } + + @Test + public void testCheckWritePublicBucketWithUnauthorizedUserAndAuthorizedProxyChain() { + final NiFiUser user = USER_WITH_PROXY_CHAIN; + final RequestAction action = RequestAction.WRITE; + + // first request will be to authorize the proxy + final AuthorizationRequest expectedProxyAuthorizationRequest = getProxyAuthorizationRequest(action, user.getChain()); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedProxyAuthorizationRequest)))) + .thenReturn(AuthorizationResult.approved()); + + // second request will be to the specific bucket + final AuthorizationRequest expectedBucketAuthorizationRequest = getBucketAuthorizationRequest( + bucketPublic.getIdentifier(), action, user); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedBucketAuthorizationRequest)))) + .thenReturn(AuthorizationResult.denied()); + + // third request will go to parent of /buckets + final AuthorizationRequest expectedBucketsAuthorizationRequest = getBucketsAuthorizationRequest(action, user); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedBucketsAuthorizationRequest)))) + .thenReturn(AuthorizationResult.denied()); + + // the authorization of the proxy chain should return denied + final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier()); + final AuthorizationResult result = bucketAuthorizable.checkAuthorization(authorizer, action, user); + assertNotNull(result); + assertEquals(AuthorizationResult.Result.Denied, result.getResult()); + + // Should call authorizer three time for /proxy, /bucket/{id}, and /buckets + verify(authorizer, times(3)).authorize(any(AuthorizationRequest.class)); + } + + @Test + public void testCheckWritePublicBucketWithAuthorizedUserAndAuthorizedProxyChain() { + final NiFiUser user = USER_WITH_PROXY_CHAIN; + final RequestAction action = RequestAction.WRITE; + + // first request will be to authorize the proxy + final AuthorizationRequest expectedProxyAuthorizationRequest = getProxyAuthorizationRequest(action, user.getChain()); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedProxyAuthorizationRequest)))) + .thenReturn(AuthorizationResult.approved()); + + // second request will be to the specific bucket + final AuthorizationRequest expectedBucketAuthorizationRequest = getBucketAuthorizationRequest( + bucketPublic.getIdentifier(), action, user); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedBucketAuthorizationRequest)))) + .thenReturn(AuthorizationResult.approved()); + + // the authorization should all return approved + final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier()); + final AuthorizationResult result = bucketAuthorizable.checkAuthorization(authorizer, action, user); + assertNotNull(result); + assertEquals(AuthorizationResult.Result.Approved, result.getResult()); + + // Should call authorizer two times for /proxy and /bucket/{id} + verify(authorizer, times(2)).authorize(any(AuthorizationRequest.class)); + } + + // Test authorize method for Bucket Authorizable + + @Test + public void testAuthorizeReadPublicBucketWithNoProxyChain() { + final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier()); + bucketAuthorizable.authorize(authorizer, RequestAction.READ, USER_NO_PROXY_CHAIN); + + // Should never call authorizer because resource is public + verify(authorizer, times(0)).authorize(any(AuthorizationRequest.class)); + } + + @Test + public void testAuthorizeReadPublicBucketWithProxyChain() { + final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier()); + bucketAuthorizable.authorize(authorizer, RequestAction.READ, USER_WITH_PROXY_CHAIN); + + // Should never call authorizer because resource is public + verify(authorizer, times(0)).authorize(any(AuthorizationRequest.class)); + } + + @Test + public void testAuthorizeWritePublicBucketWithUnauthorizedUserAndNoProxyChain() { + final RequestAction action = RequestAction.WRITE; + + // first request will be to the specific bucket + final AuthorizationRequest expectedBucketAuthorizationRequest = getBucketAuthorizationRequest( + bucketPublic.getIdentifier(), action, USER_NO_PROXY_CHAIN); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedBucketAuthorizationRequest)))) + .thenReturn(AuthorizationResult.denied()); + + // second request will go to parent of /buckets + final AuthorizationRequest expectedBucketsAuthorizationRequest = getBucketsAuthorizationRequest(action, USER_NO_PROXY_CHAIN); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedBucketsAuthorizationRequest)))) + .thenReturn(AuthorizationResult.denied()); + + // should reach authorizer and throw access denied + final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier()); + try { + bucketAuthorizable.authorize(authorizer, action, USER_NO_PROXY_CHAIN); + Assert.fail("Should have thrown exception"); + } catch (AccessDeniedException e) { + // Should never call authorizer twice for specific bucket and top-level /buckets + verify(authorizer, times(2)).authorize(any(AuthorizationRequest.class)); + } + } + + @Test + public void testAuthorizeWritePublicBucketWithUnauthorizedProxyChain() { + final RequestAction action = RequestAction.WRITE; + + // first request will be to authorize the proxy + final AuthorizationRequest expectedProxyAuthorizationRequest = getProxyAuthorizationRequest(action, USER_WITH_PROXY_CHAIN.getChain()); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedProxyAuthorizationRequest)))) + .thenReturn(AuthorizationResult.denied()); + + // the authorization of the proxy chain should throw UntrustedProxyException + final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier()); + try { + bucketAuthorizable.authorize(authorizer, action, USER_WITH_PROXY_CHAIN); + Assert.fail("Should have thrown exception"); + } catch (UntrustedProxyException e) { + // Should call authorizer once for /proxy and then throw exception + verify(authorizer, times(1)).authorize(any(AuthorizationRequest.class)); + } + } + + @Test + public void testAuthorizeWritePublicBucketWithUnauthorizedUserAndAuthorizedProxyChain() { + final NiFiUser user = USER_WITH_PROXY_CHAIN; + final RequestAction action = RequestAction.WRITE; + + // first request will be to authorize the proxy + final AuthorizationRequest expectedProxyAuthorizationRequest = getProxyAuthorizationRequest(action, user.getChain()); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedProxyAuthorizationRequest)))) + .thenReturn(AuthorizationResult.approved()); + + // second request will be to the specific bucket + final AuthorizationRequest expectedBucketAuthorizationRequest = getBucketAuthorizationRequest( + bucketPublic.getIdentifier(), action, user); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedBucketAuthorizationRequest)))) + .thenReturn(AuthorizationResult.denied()); + + // third request will go to parent of /buckets + final AuthorizationRequest expectedBucketsAuthorizationRequest = getBucketsAuthorizationRequest(action, user); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedBucketsAuthorizationRequest)))) + .thenReturn(AuthorizationResult.denied()); + + // the authorization of the proxy chain should throw UntrustedProxyException + final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier()); + try { + bucketAuthorizable.authorize(authorizer, action, user); + Assert.fail("Should have thrown exception"); + } catch (AccessDeniedException e) { + // Should call authorizer three times for /proxy, /bucket/{id}, and /buckets + verify(authorizer, times(3)).authorize(any(AuthorizationRequest.class)); + } + } + + @Test + public void testAuthorizeWritePublicBucketWithAuthorizedUserAndAuthorizedProxyChain() { + final NiFiUser user = USER_WITH_PROXY_CHAIN; + final RequestAction action = RequestAction.WRITE; + + // first request will be to authorize the proxy + final AuthorizationRequest expectedProxyAuthorizationRequest = getProxyAuthorizationRequest(action, user.getChain()); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedProxyAuthorizationRequest)))) + .thenReturn(AuthorizationResult.approved()); + + // second request will be to the specific bucket + final AuthorizationRequest expectedBucketAuthorizationRequest = getBucketAuthorizationRequest( + bucketPublic.getIdentifier(), action, user); + + when(authorizer.authorize(argThat(new AuthorizationRequestMatcher(expectedBucketAuthorizationRequest)))) + .thenReturn(AuthorizationResult.approved()); + + // the authorization should all return approved so no exception + final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketPublic.getIdentifier()); + bucketAuthorizable.authorize(authorizer, action, user); + + // Should call authorizer two times for /proxy and /bucket/{id} + verify(authorizer, times(2)).authorize(any(AuthorizationRequest.class)); + } + + private AuthorizationRequest getBucketAuthorizationRequest(final String bucketIdentifier, final RequestAction action, final NiFiUser user) { + return new AuthorizationRequest.Builder() + .resource(ResourceFactory.getBucketResource(bucketIdentifier, bucketIdentifier)) + .action(action) + .identity(user.getIdentity()) + .accessAttempt(true) + .anonymous(false) + .build(); + } + + private AuthorizationRequest getBucketsAuthorizationRequest(final RequestAction action, final NiFiUser user) { + return new AuthorizationRequest.Builder() + .resource(ResourceFactory.getBucketsResource()) + .action(action) + .identity(user.getIdentity()) + .accessAttempt(true) + .anonymous(false) + .build(); + } + + private AuthorizationRequest getProxyAuthorizationRequest(final RequestAction action, final NiFiUser user) { + return new AuthorizationRequest.Builder() + .resource(ResourceFactory.getProxyResource()) + .action(action) + .identity(user.getIdentity()) + .accessAttempt(true) + .anonymous(false) + .build(); + } + + /** + * ArugmentMatcher for AuthorizationRequest. + */ + private static class AuthorizationRequestMatcher implements ArgumentMatcher { + + private final AuthorizationRequest expectedAuthorizationRequest; + + public AuthorizationRequestMatcher(final AuthorizationRequest expectedAuthorizationRequest) { + this.expectedAuthorizationRequest = expectedAuthorizationRequest; + } + + @Override + public boolean matches(final AuthorizationRequest authorizationRequest) { + if (authorizationRequest == null) { + return false; + } + + final String requestResourceId = authorizationRequest.getResource().getIdentifier(); + final String expectedResourceId = expectedAuthorizationRequest.getResource().getIdentifier(); + + final String requestAction = authorizationRequest.getAction().toString(); + final String expectedAction = expectedAuthorizationRequest.getAction().toString(); + + final String requestUserIdentity = authorizationRequest.getIdentity(); + final String expectedUserIdentity = authorizationRequest.getIdentity(); + + return requestResourceId.equals(expectedResourceId) + && requestAction.equals(expectedAction) + && requestUserIdentity.equals(expectedUserIdentity); + } + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/authorization/database/TestDatabaseAccessPolicyProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/authorization/database/TestDatabaseAccessPolicyProvider.java new file mode 100644 index 0000000000..7f522ff0f3 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/authorization/database/TestDatabaseAccessPolicyProvider.java @@ -0,0 +1,496 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.database; + +import org.apache.nifi.registry.db.DatabaseBaseTest; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.apache.nifi.registry.security.authorization.AbstractConfigurableAccessPolicyProvider; +import org.apache.nifi.registry.security.authorization.AccessPolicy; +import org.apache.nifi.registry.security.authorization.AuthorizerConfigurationContext; +import org.apache.nifi.registry.security.authorization.AuthorizerInitializationContext; +import org.apache.nifi.registry.security.authorization.ConfigurableAccessPolicyProvider; +import org.apache.nifi.registry.security.authorization.Group; +import org.apache.nifi.registry.security.authorization.RequestAction; +import org.apache.nifi.registry.security.authorization.User; +import org.apache.nifi.registry.security.authorization.UserGroupProvider; +import org.apache.nifi.registry.security.authorization.UserGroupProviderLookup; +import org.apache.nifi.registry.security.authorization.resource.ResourceFactory; +import org.apache.nifi.registry.security.authorization.util.AccessPolicyProviderUtils; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.identity.DefaultIdentityMapper; +import org.apache.nifi.registry.security.identity.IdentityMapper; +import org.apache.nifi.registry.util.StandardPropertyValue; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; + +import javax.sql.DataSource; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class TestDatabaseAccessPolicyProvider extends DatabaseBaseTest { + + private static final String UGP_IDENTIFIER = "mock-user-group-provider"; + + private static final User ADMIN_USER = new User.Builder().identifierGenerateRandom().identity("admin").build(); + private static final User ADMIN_USER2 = new User.Builder().identifierGenerateRandom().identity("admin2").build(); + + private static final User NIFI1_USER = new User.Builder().identifierGenerateRandom().identity("nifi1").build(); + private static final User NIFI2_USER = new User.Builder().identifierGenerateRandom().identity("nifi2").build(); + private static final User NIFI3_USER = new User.Builder().identifierGenerateRandom().identity("nifi3").build(); + + private static final Set NIFI_IDENTITIES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + NIFI1_USER.getIdentity(), NIFI2_USER.getIdentity(), NIFI3_USER.getIdentity()))); + + private static final Group NIFI_GROUP = new Group.Builder().identifierGenerateRandom().name("nifi-nodes").build(); + private static final Group OTHERS_GROUP = new Group.Builder().identifierGenerateRandom().name("other-members").build(); + + @Autowired + private DataSource dataSource; + private JdbcTemplate jdbcTemplate; + private NiFiRegistryProperties properties; + private IdentityMapper identityMapper; + + private UserGroupProviderLookup userGroupProviderLookup; + private UserGroupProvider userGroupProvider; + + // Class under test + private ConfigurableAccessPolicyProvider policyProvider; + + @Before + public void setup() { + properties = new NiFiRegistryProperties(); + identityMapper = new DefaultIdentityMapper(properties); + jdbcTemplate = new JdbcTemplate(dataSource); + + userGroupProvider = mock(UserGroupProvider.class); + when(userGroupProvider.getUserByIdentity(ADMIN_USER.getIdentity())).thenReturn(ADMIN_USER); + when(userGroupProvider.getUserByIdentity(ADMIN_USER2.getIdentity())).thenReturn(ADMIN_USER2); + when(userGroupProvider.getUserByIdentity(NIFI1_USER.getIdentity())).thenReturn(NIFI1_USER); + when(userGroupProvider.getUserByIdentity(NIFI2_USER.getIdentity())).thenReturn(NIFI2_USER); + when(userGroupProvider.getUserByIdentity(NIFI3_USER.getIdentity())).thenReturn(NIFI3_USER); + when(userGroupProvider.getGroups()).thenReturn(Collections.unmodifiableSet( + new HashSet<>(Arrays.asList(OTHERS_GROUP, NIFI_GROUP)))); + + userGroupProviderLookup = mock(UserGroupProviderLookup.class); + when(userGroupProviderLookup.getUserGroupProvider(UGP_IDENTIFIER)).thenReturn(userGroupProvider); + + final AuthorizerInitializationContext initializationContext = mock(AuthorizerInitializationContext.class); + when(initializationContext.getUserGroupProviderLookup()).thenReturn(userGroupProviderLookup); + + final DatabaseAccessPolicyProvider databaseProvider = new DatabaseAccessPolicyProvider(); + databaseProvider.setDataSource(dataSource); + databaseProvider.setIdentityMapper(identityMapper); + databaseProvider.initialize(initializationContext); + + policyProvider = databaseProvider; + } + + private void configure(final String initialAdmin, final Set nifiIdentifies) { + configure(initialAdmin, nifiIdentifies, null); + } + + /** + * Helper method to call onConfigured with a configuration context. + * + * @param initialAdmin the initial admin identity to put in the context, or null + * @param nifiIdentifies the nifi identities to put in the context, or null + * @param nifiGroupName the name of the nifi group + */ + private void configure(final String initialAdmin, final Set nifiIdentifies, final String nifiGroupName) { + final Map properties = new HashMap<>(); + properties.put(AbstractConfigurableAccessPolicyProvider.PROP_USER_GROUP_PROVIDER, UGP_IDENTIFIER); + + if (initialAdmin != null) { + properties.put(AccessPolicyProviderUtils.PROP_INITIAL_ADMIN_IDENTITY, initialAdmin); + } + + if (nifiIdentifies != null) { + int i = 1; + for (final String nifiIdentity : nifiIdentifies) { + properties.put(AccessPolicyProviderUtils.PROP_NIFI_IDENTITY_PREFIX + i++, nifiIdentity); + } + } + + if (nifiGroupName != null) { + properties.put(AccessPolicyProviderUtils.PROP_NIFI_GROUP_NAME, nifiGroupName); + } + + final AuthorizerConfigurationContext configurationContext = mock(AuthorizerConfigurationContext.class); + when(configurationContext.getProperties()).thenReturn(properties); + when(configurationContext.getProperty(AbstractConfigurableAccessPolicyProvider.PROP_USER_GROUP_PROVIDER)) + .thenReturn(new StandardPropertyValue(UGP_IDENTIFIER)); + when(configurationContext.getProperty(AccessPolicyProviderUtils.PROP_INITIAL_ADMIN_IDENTITY)) + .thenReturn(new StandardPropertyValue(initialAdmin)); + when(configurationContext.getProperty(AccessPolicyProviderUtils.PROP_NIFI_GROUP_NAME)) + .thenReturn(new StandardPropertyValue(nifiGroupName)); + policyProvider.onConfigured(configurationContext); + } + + private void configure() { + configure(null, null, null); + } + + // -- Helper methods for accessing the DB outside of the provider + + private int getPolicyCount() { + return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM APP_POLICY", Integer.class); + } + + private void createPolicy(final String identifier, final String resource, final RequestAction action) { + final String policySql = "INSERT INTO APP_POLICY(IDENTIFIER, RESOURCE, ACTION) VALUES (?, ?, ?)"; + final int rowsUpdated = jdbcTemplate.update(policySql, identifier, resource, action.toString()); + assertEquals(1, rowsUpdated); + } + + private void addUserToPolicy(final String policyIdentifier, final String userIdentifier) { + final String policyUserSql = "INSERT INTO APP_POLICY_USER(POLICY_IDENTIFIER, USER_IDENTIFIER) VALUES (?, ?)"; + final int rowsUpdated = jdbcTemplate.update(policyUserSql, policyIdentifier, userIdentifier); + assertEquals(1, rowsUpdated); + } + + private void addGroupToPolicy(final String policyIdentifier, final String groupIdentifier) { + final String policyGroupSql = "INSERT INTO APP_POLICY_GROUP(POLICY_IDENTIFIER, GROUP_IDENTIFIER) VALUES (?, ?)"; + final int rowsUpdated = jdbcTemplate.update(policyGroupSql, policyIdentifier, groupIdentifier); + assertEquals(1, rowsUpdated); + } + + // -- Test onConfigured + + @Test + public void testOnConfiguredCreatesInitialPolicies() { + // verify no policies in DB + assertEquals(0, getPolicyCount()); + + configure(ADMIN_USER.getIdentity(), NIFI_IDENTITIES, NIFI_GROUP.getName()); + + // verify policies got created for admin and NiFi identities + final Set policies = policyProvider.getAccessPolicies(); + assertNotNull(policies); + assertEquals(18, policies.size()); + } + + @Test + public void testOnConfiguredWhenOnlyInitialAdmin() { + // verify no policies in DB + assertEquals(0, getPolicyCount()); + + configure(ADMIN_USER.getIdentity(), null); + + // verify policies got created for admin and NiFi identities + final Set policies = policyProvider.getAccessPolicies(); + assertNotNull(policies); + assertEquals(18, policies.size()); + + // verify each policy only has the initial admin + policies.forEach(p -> { + assertNotNull(p.getUsers()); + assertEquals(1, p.getUsers().size()); + assertTrue(p.getUsers().contains(ADMIN_USER.getIdentifier())); + }); + } + + @Test + public void testOnConfiguredWhenOnlyInitialNiFiIdentities() { + // verify no policies in DB + assertEquals(0, getPolicyCount()); + + configure(null, NIFI_IDENTITIES); + + // verify policies got created for admin and NiFi identities + final Set policies = policyProvider.getAccessPolicies(); + assertNotNull(policies); + assertEquals(4, policies.size()); + + // verify each policy only has the initial admin + policies.forEach(p -> { + assertNotNull(p.getUsers()); + assertEquals(3, p.getUsers().size()); + assertFalse(p.getUsers().contains(ADMIN_USER.getIdentifier())); + }); + } + + @Test + public void testOnConfiguredWhenOnlyNiFiGroupName() { + // verify no policies in DB + assertEquals(0, getPolicyCount()); + + configure(null, null, NIFI_GROUP.getName()); + + // verify policies got created for admin and NiFi identities + final Set policies = policyProvider.getAccessPolicies(); + assertNotNull(policies); + assertEquals(4, policies.size()); + + // verify each policy only has the initial admin + policies.forEach(p -> { + assertNotNull(p.getGroups()); + assertEquals(1, p.getGroups().size()); + assertTrue(p.getGroups().contains(NIFI_GROUP.getIdentifier())); + }); + } + + @Test + public void testOnConfiguredStillCreatesInitialPoliciesWhenPoliciesAlreadyExist() { + // create one policy + createPolicy("policy1", "/foo", RequestAction.READ); + assertEquals(1, getPolicyCount()); + + configure(ADMIN_USER.getIdentity(), NIFI_IDENTITIES); + assertTrue(getPolicyCount() > 1); + } + + @Test + public void testOnConfiguredWhenChangingInitialAdmin() { + configure(ADMIN_USER.getIdentity(), NIFI_IDENTITIES); + configure(ADMIN_USER.getIdentity(), NIFI_IDENTITIES); + configure("admin2", NIFI_IDENTITIES); + + final AccessPolicy readBuckets = policyProvider.getAccessPolicy( + ResourceFactory.getBucketsResource().getIdentifier(), RequestAction.READ); + assertNotNull(readBuckets); + assertEquals(5, readBuckets.getUsers().size()); + assertTrue(readBuckets.getUsers().contains(ADMIN_USER.getIdentifier())); + assertTrue(readBuckets.getUsers().contains(ADMIN_USER2.getIdentifier())); + assertTrue(readBuckets.getUsers().contains(NIFI1_USER.getIdentifier())); + assertTrue(readBuckets.getUsers().contains(NIFI2_USER.getIdentifier())); + assertTrue(readBuckets.getUsers().contains(NIFI3_USER.getIdentifier())); + } + + @Test + public void testOnConfiguredAppliesIdentityMappings() { + // Set up an identity mapping for kerberos principals + properties.setProperty("nifi.registry.security.identity.mapping.pattern.kerb", "^(.*?)@(.*?)$"); + properties.setProperty("nifi.registry.security.identity.mapping.value.kerb", "$1"); + + identityMapper = new DefaultIdentityMapper(properties); + ((DatabaseAccessPolicyProvider)policyProvider).setIdentityMapper(identityMapper); + + // Call configure with full admin identity, should get mapped to just 'admin' before looking up user + configure("admin@HDF.COM", null); + + // verify policies got created for admin and NiFi identities + final Set policies = policyProvider.getAccessPolicies(); + assertNotNull(policies); + assertEquals(18, policies.size()); + + // verify each policy only has the initial admin + policies.forEach(p -> { + assertNotNull(p.getUsers()); + assertEquals(1, p.getUsers().size()); + assertTrue(p.getUsers().contains(ADMIN_USER.getIdentifier())); + }); + } + + @Test + public void testOnConfiguredAppliesGroupMappings() { + // Set up an identity mapping for kerberos principals + properties.setProperty("nifi.registry.security.group.mapping.pattern.anyGroup", "^(.*)$"); + properties.setProperty("nifi.registry.security.group.mapping.value.anyGroup", "$1"); + properties.setProperty("nifi.registry.security.group.mapping.transform.anyGroup", "LOWER"); + + identityMapper = new DefaultIdentityMapper(properties); + ((DatabaseAccessPolicyProvider)policyProvider).setIdentityMapper(identityMapper); + + // Call configure with NiFi Group in all uppercase, should get mapped to lower case + configure(null, null, NIFI_GROUP.getName().toUpperCase()); + + // verify policies got created for admin and NiFi identities + final Set policies = policyProvider.getAccessPolicies(); + assertNotNull(policies); + assertEquals(4, policies.size()); + + // verify each policy only has the initial admin + policies.forEach(p -> { + assertNotNull(p.getGroups()); + assertEquals(1, p.getGroups().size()); + assertTrue(p.getGroups().contains(NIFI_GROUP.getIdentifier())); + }); + } + + @Test(expected = SecurityProviderCreationException.class) + public void testOnConfiguredWhenInitialAdminNotFound() { + configure("does-not-exist", null); + } + + @Test(expected = SecurityProviderCreationException.class) + public void testOnConfiguredWhenNiFiIdentityNotFound() { + configure(null, Collections.singleton("does-not-exist")); + } + + // -- Test AccessPolicy methods + + @Test + public void testAddAccessPolicy() { + configure(); + assertEquals(0, getPolicyCount()); + + final AccessPolicy policy = new AccessPolicy.Builder() + .identifierGenerateRandom() + .resource("/buckets") + .action(RequestAction.READ) + .addUser("user1") + .addGroup("group1") + .build(); + + final AccessPolicy createdPolicy = policyProvider.addAccessPolicy(policy); + assertNotNull(createdPolicy); + + final AccessPolicy retrievedPolicy = policyProvider.getAccessPolicy(policy.getIdentifier()); + verifyPoliciesEqual(policy, retrievedPolicy); + } + + @Test + public void testGetPolicyByIdentifierWhenExists() { + configure(ADMIN_USER.getIdentity(), NIFI_IDENTITIES); + + final Set policies = policyProvider.getAccessPolicies(); + assertNotNull(policies); + assertTrue(policies.size() > 0); + + final AccessPolicy existingPolicy = policies.stream().findFirst().get(); + final AccessPolicy retrievedPolicy = policyProvider.getAccessPolicy(existingPolicy.getIdentifier()); + verifyPoliciesEqual(existingPolicy, retrievedPolicy); + } + + @Test + public void testGetPolicyByIdentifierWhenDoesNotExist() { + configure(ADMIN_USER.getIdentity(), NIFI_IDENTITIES); + + final AccessPolicy retrievedPolicy = policyProvider.getAccessPolicy("does-not-exist"); + assertNull(retrievedPolicy); + } + + @Test + public void testGetPolicyByResourceAndActionWhenExists() { + configure(ADMIN_USER.getIdentity(), NIFI_IDENTITIES); + + final Set policies = policyProvider.getAccessPolicies(); + assertNotNull(policies); + assertTrue(policies.size() > 0); + + final AccessPolicy existingPolicy = policies.stream().findFirst().get(); + final AccessPolicy retrievedPolicy = policyProvider.getAccessPolicy(existingPolicy.getResource(), existingPolicy.getAction()); + verifyPoliciesEqual(existingPolicy, retrievedPolicy); + } + + @Test + public void testGetPolicyByResourceAndActionWhenDoesNotExist() { + configure(ADMIN_USER.getIdentity(), NIFI_IDENTITIES); + + final AccessPolicy retrievedPolicy = policyProvider.getAccessPolicy("does-not-exist", RequestAction.READ); + assertNull(retrievedPolicy); + } + + @Test + public void testUpdatePolicyWhenExists() { + configure(ADMIN_USER.getIdentity(), NIFI_IDENTITIES); + + final AccessPolicy readBucketsPolicy = policyProvider.getAccessPolicy( + ResourceFactory.getBucketsResource().getIdentifier(), RequestAction.READ); + assertNotNull(readBucketsPolicy); + assertEquals(4, readBucketsPolicy.getUsers().size()); + assertEquals(0, readBucketsPolicy.getGroups().size()); + + final AccessPolicy updatedPolicy = new AccessPolicy.Builder(readBucketsPolicy) + .addUser("user1") + .addGroup("group1") + .build(); + + final AccessPolicy returnedPolicy = policyProvider.updateAccessPolicy(updatedPolicy); + assertNotNull(returnedPolicy); + + final AccessPolicy retrievedPolicy = policyProvider.getAccessPolicy(readBucketsPolicy.getIdentifier()); + verifyPoliciesEqual(updatedPolicy, retrievedPolicy); + } + + @Test + public void testUpdatePolicyWhenDoesNotExist() { + configure(ADMIN_USER.getIdentity(), NIFI_IDENTITIES); + + final AccessPolicy policy = new AccessPolicy.Builder() + .identifierGenerateRandom() + .resource("/foo") + .action(RequestAction.READ) + .addUser("user1") + .addGroup("group1") + .build(); + + final AccessPolicy returnedPolicy = policyProvider.updateAccessPolicy(policy); + assertNull(returnedPolicy); + + final AccessPolicy retrievedPolicy = policyProvider.getAccessPolicy(policy.getIdentifier()); + assertNull(retrievedPolicy); + } + + @Test + public void testDeletePolicyWhenExists() { + configure(ADMIN_USER.getIdentity(), NIFI_IDENTITIES); + + final AccessPolicy readBucketsPolicy = policyProvider.getAccessPolicy( + ResourceFactory.getBucketsResource().getIdentifier(), RequestAction.READ); + assertNotNull(readBucketsPolicy); + assertEquals(4, readBucketsPolicy.getUsers().size()); + assertEquals(0, readBucketsPolicy.getGroups().size()); + + final AccessPolicy deletedPolicy = policyProvider.deleteAccessPolicy(readBucketsPolicy); + assertNotNull(deletedPolicy); + + final AccessPolicy retrievedPolicy = policyProvider.getAccessPolicy(readBucketsPolicy.getIdentifier()); + assertNull(retrievedPolicy); + } + + @Test + public void testDeletePolicyWhenDoesNotExist() { + configure(ADMIN_USER.getIdentity(), NIFI_IDENTITIES); + + final AccessPolicy policy = new AccessPolicy.Builder() + .identifierGenerateRandom() + .resource("/foo") + .action(RequestAction.READ) + .addUser("user1") + .addGroup("group1") + .build(); + + final AccessPolicy deletedPolicy = policyProvider.deleteAccessPolicy(policy); + assertNull(deletedPolicy); + } + + private void verifyPoliciesEqual(final AccessPolicy policy1, final AccessPolicy policy2) { + assertNotNull(policy1); + assertNotNull(policy2); + assertEquals(policy1.getIdentifier(), policy2.getIdentifier()); + assertEquals(policy1.getResource(), policy2.getResource()); + assertEquals(policy1.getAction(), policy2.getAction()); + assertEquals(policy1.getUsers().size(), policy2.getUsers().size()); + assertEquals(policy1.getGroups().size(), policy2.getGroups().size()); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/authorization/database/TestDatabaseUserGroupProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/authorization/database/TestDatabaseUserGroupProvider.java new file mode 100644 index 0000000000..0f8d4322ea --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/authorization/database/TestDatabaseUserGroupProvider.java @@ -0,0 +1,595 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.database; + +import org.apache.nifi.registry.db.DatabaseBaseTest; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.apache.nifi.registry.security.authorization.AuthorizerConfigurationContext; +import org.apache.nifi.registry.security.authorization.ConfigurableUserGroupProvider; +import org.apache.nifi.registry.security.authorization.Group; +import org.apache.nifi.registry.security.authorization.User; +import org.apache.nifi.registry.security.authorization.UserAndGroups; +import org.apache.nifi.registry.security.authorization.UserGroupProviderInitializationContext; +import org.apache.nifi.registry.security.authorization.util.UserGroupProviderUtils; +import org.apache.nifi.registry.security.identity.DefaultIdentityMapper; +import org.apache.nifi.registry.security.identity.IdentityMapper; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; + +import javax.sql.DataSource; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class TestDatabaseUserGroupProvider extends DatabaseBaseTest { + + @Autowired + private DataSource dataSource; + private NiFiRegistryProperties properties; + private IdentityMapper identityMapper; + + private ConfigurableUserGroupProvider userGroupProvider; + + @Before + public void setup() { + properties = new NiFiRegistryProperties(); + identityMapper = new DefaultIdentityMapper(properties); + + final DatabaseUserGroupProvider databaseUserGroupProvider = new DatabaseUserGroupProvider(); + databaseUserGroupProvider.setDataSource(dataSource); + databaseUserGroupProvider.setIdentityMapper(identityMapper); + + final UserGroupProviderInitializationContext initializationContext = mock(UserGroupProviderInitializationContext.class); + databaseUserGroupProvider.initialize(initializationContext); + + userGroupProvider = databaseUserGroupProvider; + } + + /** + * Helper method to call onConfigured with a configuration context that has initial users. + * + * @param initialUserIdentities the initial user identities to place in the configuration context + */ + private void configureWithInitialUsers(final String ... initialUserIdentities) { + final Map configProperties = new HashMap<>(); + + for (int i=0; i < initialUserIdentities.length; i++) { + final String initialUserIdentity = initialUserIdentities[i]; + configProperties.put(UserGroupProviderUtils.PROP_INITIAL_USER_IDENTITY_PREFIX + (i+1), initialUserIdentity); + } + + final AuthorizerConfigurationContext configurationContext = mock(AuthorizerConfigurationContext.class); + when(configurationContext.getProperties()).thenReturn(configProperties); + + userGroupProvider.onConfigured(configurationContext); + } + + /** + * Helper method to create a user outside of the provider. + * + * @param userIdentifier the user identifier + * @param userIdentity the user identity + */ + private void createUser(final String userIdentifier, final String userIdentity) { + final JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); + final String sql = "INSERT INTO UGP_USER(IDENTIFIER, IDENTITY) VALUES (?, ?)"; + final int updatedRows1 = jdbcTemplate.update(sql, new Object[] {userIdentifier, userIdentity}); + assertEquals(1, updatedRows1); + } + + /** + * Helper method to create a group outside of the provider. + * + * @param groupIdentifier the group identifier + * @param groupIdentity the group identity + */ + private void createGroup(final String groupIdentifier, final String groupIdentity) { + final JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); + final String sql = "INSERT INTO UGP_GROUP(IDENTIFIER, IDENTITY) VALUES (?, ?)"; + final int updatedRows1 = jdbcTemplate.update(sql, new Object[] {groupIdentifier, groupIdentity}); + assertEquals(1, updatedRows1); + } + + /** + * Helper method to add a user to a group outside of the provider + * + * @param userIdentifier the user identifier + * @param groupIdentifier the group identifier + */ + private void addUserToGroup(final String userIdentifier, final String groupIdentifier) { + final JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); + final String sql = "INSERT INTO UGP_USER_GROUP(USER_IDENTIFIER, GROUP_IDENTIFIER) VALUES (?, ?)"; + final int updatedRows1 = jdbcTemplate.update(sql, new Object[] {userIdentifier, groupIdentifier}); + assertEquals(1, updatedRows1); + } + + // -- Test onConfigured + + @Test + public void testOnConfiguredCreatesInitialUsersWhenNoUsersAndGroups() { + final String userIdentity1 = "user1"; + final String userIdentity2 = "user2"; + configureWithInitialUsers(userIdentity1, userIdentity2); + + final Set users = userGroupProvider.getUsers(); + assertEquals(2, users.size()); + assertNotNull(users.stream().filter(u -> u.getIdentity().equals(userIdentity1)).findFirst().orElse(null)); + assertNotNull(users.stream().filter(u -> u.getIdentity().equals(userIdentity2)).findFirst().orElse(null)); + } + + @Test + public void testOnConfiguredStillCreatesInitialUsersWhenExistingUsersAndGroups() { + // Create a user in the DB before we call onConfigured + final String existingUserIdentity= "existingUser"; + final String existingUserIdentifier = UUID.randomUUID().toString(); + createUser(existingUserIdentifier, existingUserIdentity); + + // Call onConfigured with initial users + final String userIdentity1 = "user1"; + final String userIdentity2 = "user2"; + configureWithInitialUsers(userIdentity1, userIdentity2); + + // Verify the initial users were not created + final Set users = userGroupProvider.getUsers(); + assertEquals(3, users.size()); + assertNotNull(users.stream().filter(u -> u.getIdentity().equals(existingUserIdentity)).findFirst().orElse(null)); + assertNotNull(users.stream().filter(u -> u.getIdentity().equals(userIdentity1)).findFirst().orElse(null)); + assertNotNull(users.stream().filter(u -> u.getIdentity().equals(userIdentity2)).findFirst().orElse(null)); + } + + @Test + public void testOnConfiguredWithSameUsers() { + // Create a user in the DB before we call onConfigured + final String existingUserIdentity= "existingUser"; + final String existingUserIdentifier = UUID.randomUUID().toString(); + createUser(existingUserIdentifier, existingUserIdentity); + + // Call onConfigured with same identity that already exists + configureWithInitialUsers(existingUserIdentity); + + // Verify there is only one user + final Set users = userGroupProvider.getUsers(); + assertEquals(1, users.size()); + assertNotNull(users.stream().filter(u -> u.getIdentity().equals(existingUserIdentity)).findFirst().orElse(null)); + } + + @Test + public void testOnConfiguredAppliesIdentityMappingsToInitialUsers() { + // Set up an identity mapping for kerberos principals + properties.setProperty("nifi.registry.security.identity.mapping.pattern.kerb", "^(.*?)@(.*?)$"); + properties.setProperty("nifi.registry.security.identity.mapping.value.kerb", "$1"); + + identityMapper = new DefaultIdentityMapper(properties); + ((DatabaseUserGroupProvider)userGroupProvider).setIdentityMapper(identityMapper); + + // Call onConfigured with two initial users - one kerberos principal, one DN + final String userIdentity1 = "user1@NIFI.COM"; + final String userIdentity2 = "CN=user2, OU=NIFI"; + configureWithInitialUsers(userIdentity1, userIdentity2); + + // Verify the kerberos principal had the mapping applied + final Set users = userGroupProvider.getUsers(); + assertEquals(2, users.size()); + assertNotNull(users.stream().filter(u -> u.getIdentity().equals("user1")).findFirst().orElse(null)); + assertNotNull(users.stream().filter(u -> u.getIdentity().equals(userIdentity2)).findFirst().orElse(null)); + } + + // -- Test User Methods + + @Test + public void testAddUser() { + configureWithInitialUsers(); + + final Set users = userGroupProvider.getUsers(); + assertEquals(0, users.size()); + + final User user1 = new User.Builder() + .identifier(UUID.randomUUID().toString()) + .identity("user1") + .build(); + + final User createdUser1 = userGroupProvider.addUser(user1); + assertNotNull(createdUser1); + + final Set usersAfterCreate = userGroupProvider.getUsers(); + assertEquals(1, usersAfterCreate.size()); + + final User retrievedUser1 = usersAfterCreate.stream().findFirst().get(); + assertEquals(user1.getIdentifier(), retrievedUser1.getIdentifier()); + assertEquals(user1.getIdentity(), retrievedUser1.getIdentity()); + } + + @Test + public void testGetUserByIdentifierWhenExists() { + configureWithInitialUsers(); + + final String userIdentifier = UUID.randomUUID().toString(); + final String userIdentity = "user1"; + createUser(userIdentifier, userIdentity); + + final User retrievedUser1 = userGroupProvider.getUser(userIdentifier); + assertNotNull(retrievedUser1); + assertEquals(userIdentifier, retrievedUser1.getIdentifier()); + assertEquals(userIdentity, retrievedUser1.getIdentity()); + } + + @Test + public void testGetUserByIdentifierWhenDoesNotExist() { + configureWithInitialUsers(); + + final User retrievedUser1 = userGroupProvider.getUser("does-not-exist"); + assertNull(retrievedUser1); + } + + @Test + public void testGetUserByIdentityWhenExists() { + configureWithInitialUsers(); + + final String userIdentifier = UUID.randomUUID().toString(); + final String userIdentity = "user1"; + createUser(userIdentifier, userIdentity); + + final User retrievedUser1 = userGroupProvider.getUserByIdentity(userIdentity); + assertNotNull(retrievedUser1); + assertEquals(userIdentifier, retrievedUser1.getIdentifier()); + assertEquals(userIdentity, retrievedUser1.getIdentity()); + } + + @Test + public void testGetUserByIdentityWhenDoesNotExist() { + configureWithInitialUsers(); + + final User retrievedUser1 = userGroupProvider.getUserByIdentity("does-not-exist"); + assertNull(retrievedUser1); + } + + @Test + public void testGetUserAndGroupsWhenExists() { + configureWithInitialUsers(); + + // Create some users... + final User user1 = new User.Builder().identifier(UUID.randomUUID().toString()).identity("user1").build(); + createUser(user1.getIdentifier(), user1.getIdentity()); + + final User user2 = new User.Builder().identifier(UUID.randomUUID().toString()).identity("user2").build(); + createUser(user2.getIdentifier(), user2.getIdentity()); + + // Create some groups... + final Group group1 = new Group.Builder().identifier(UUID.randomUUID().toString()).name("group1").build(); + createGroup(group1.getIdentifier(), group1.getName()); + + final Group group2 = new Group.Builder().identifier(UUID.randomUUID().toString()).name("group2").build(); + createGroup(group2.getIdentifier(), group2.getName()); + + // Add users to groups... + addUserToGroup(user1.getIdentifier(), group1.getIdentifier()); + addUserToGroup(user2.getIdentifier(), group2.getIdentifier()); + + // Retrieve UserAndGroups... + final UserAndGroups user1AndGroups = userGroupProvider.getUserAndGroups(user1.getIdentity()); + assertNotNull(user1AndGroups); + + // Verify retrieved user.. + final User retrievedUser1 = user1AndGroups.getUser(); + assertNotNull(retrievedUser1); + assertEquals(user1.getIdentifier(), retrievedUser1.getIdentifier()); + assertEquals(user1.getIdentity(), retrievedUser1.getIdentity()); + + // Verify retrieved groups.. + final Set user1Groups = user1AndGroups.getGroups(); + assertNotNull(user1Groups); + assertEquals(1, user1Groups.size()); + + final Group user1Group = user1Groups.stream().findFirst().get(); + assertEquals(group1.getIdentifier(), user1Group.getIdentifier()); + assertEquals(group1.getName(), user1Group.getName()); + + assertNotNull(user1Group.getUsers()); + assertEquals(1, user1Group.getUsers().size()); + assertTrue(user1Group.getUsers().contains(user1.getIdentifier())); + } + + @Test + public void testGetUserAndGroupsWhenDoesNotExist() { + configureWithInitialUsers(); + + final UserAndGroups userAndGroups = userGroupProvider.getUserAndGroups("does-not-exist"); + assertNotNull(userAndGroups); + assertNull(userAndGroups.getUser()); + assertNull(userAndGroups.getGroups()); + } + + @Test + public void testUpdateUserWhenExists() { + configureWithInitialUsers(); + + final User user1 = new User.Builder().identifier(UUID.randomUUID().toString()).identity("user1").build(); + createUser(user1.getIdentifier(), user1.getIdentity()); + + final User retrievedUser1 = userGroupProvider.getUser(user1.getIdentifier()); + assertNotNull(retrievedUser1); + + final User modifiedUser1 = new User.Builder(retrievedUser1).identity("user1 updated").build(); + + final User updatedUser1 = userGroupProvider.updateUser(modifiedUser1); + assertNotNull(updatedUser1); + assertEquals(modifiedUser1.getIdentity(), updatedUser1.getIdentity()); + } + + @Test + public void testUpdateUserWhenDoesNotExist() { + configureWithInitialUsers(); + + final User user1 = new User.Builder().identifier(UUID.randomUUID().toString()).identity("user1").build(); + final User updatedUser1 = userGroupProvider.updateUser(user1); + assertNull(updatedUser1); + } + + @Test + public void testDeleteUserWhenExists() { + final String user1Identity = "user1"; + configureWithInitialUsers(user1Identity); + + // verify user1 exists + final User retrievedUser1 = userGroupProvider.getUserByIdentity(user1Identity); + assertNotNull(retrievedUser1); + assertEquals(user1Identity, retrievedUser1.getIdentity()); + + // add user1 to a group to test deleting group association when deleting a user + final Group group1 = new Group.Builder() + .identifier(UUID.randomUUID().toString()) + .name("group1") + .addUser(retrievedUser1.getIdentifier()) + .build(); + + final Group createdGroup1 = userGroupProvider.addGroup(group1); + assertNotNull(createdGroup1); + + // delete user1 + final User deletedUser1 = userGroupProvider.deleteUser(retrievedUser1); + assertNotNull(deletedUser1); + + // verify user1 no longer exists + final User retrievedUser1AfterDelete = userGroupProvider.getUserByIdentity(user1Identity); + assertNull(retrievedUser1AfterDelete); + + // verify user1 no longer a member of group1 + final Group retrievedGroup1 = userGroupProvider.getGroup(group1.getIdentifier()); + assertNotNull(retrievedGroup1); + assertEquals(0, retrievedGroup1.getUsers().size()); + } + + @Test + public void testDeleteUserWhenDoesNotExist() { + configureWithInitialUsers(); + + final User user1 = new User.Builder().identifier(UUID.randomUUID().toString()).identity("user1").build(); + final User updatedUser1 = userGroupProvider.deleteUser(user1); + assertNull(updatedUser1); + } + + // -- Test Group Methods + + @Test + public void testAddGroupWithoutUsers() { + configureWithInitialUsers(); + + final Set groupsBefore = userGroupProvider.getGroups(); + assertEquals(0, groupsBefore.size()); + + final Group group1 = new Group.Builder().identifier(UUID.randomUUID().toString()).name("group1").build(); + + final Group createdGroup1 = userGroupProvider.addGroup(group1); + assertNotNull(createdGroup1); + + final Set groupsAfter = userGroupProvider.getGroups(); + assertEquals(1, groupsAfter.size()); + + final Group retrievedGroup1 = groupsAfter.stream().findFirst().get(); + assertEquals(group1.getIdentifier(), retrievedGroup1.getIdentifier()); + assertEquals(group1.getName(), retrievedGroup1.getName()); + assertNotNull(retrievedGroup1.getUsers()); + assertEquals(0, retrievedGroup1.getUsers().size()); + } + + @Test + public void testAddGroupWithUsers() { + configureWithInitialUsers(); + + final String user1Identifier = UUID.randomUUID().toString(); + final String user1Identity = "user1"; + createUser(user1Identifier, user1Identity); + + final Set groupsBefore = userGroupProvider.getGroups(); + assertEquals(0, groupsBefore.size()); + + final Group group1 = new Group.Builder() + .identifier(UUID.randomUUID().toString()) + .name("group1") + .addUser(user1Identifier) + .build(); + + final Group createdGroup1 = userGroupProvider.addGroup(group1); + assertNotNull(createdGroup1); + + final Set groupsAfter = userGroupProvider.getGroups(); + assertEquals(1, groupsAfter.size()); + + final Group retrievedGroup1 = groupsAfter.stream().findFirst().get(); + assertEquals(group1.getIdentifier(), retrievedGroup1.getIdentifier()); + assertEquals(group1.getName(), retrievedGroup1.getName()); + assertNotNull(retrievedGroup1.getUsers()); + assertEquals(1, retrievedGroup1.getUsers().size()); + assertTrue(retrievedGroup1.getUsers().contains(user1Identifier)); + } + + @Test + public void testGetGroupWhenExists() { + configureWithInitialUsers(); + + final String group1Identifier = UUID.randomUUID().toString(); + final String group1Identity = "group1"; + createGroup(group1Identifier, group1Identity); + + final Group group1 = userGroupProvider.getGroup(group1Identifier); + assertNotNull(group1); + assertEquals(group1Identifier, group1.getIdentifier()); + assertEquals(group1Identity, group1.getName()); + + assertNotNull(group1.getUsers()); + assertEquals(0, group1.getUsers().size()); + } + + @Test + public void testGetGroupWhenDoesNotExist() { + configureWithInitialUsers(); + + final Group group1 = userGroupProvider.getGroup("does-not-exist"); + assertNull(group1); + } + + @Test + public void testUpdateGroupWhenExists() { + configureWithInitialUsers(); + + // Create some users... + final User user1 = new User.Builder().identifier(UUID.randomUUID().toString()).identity("user1").build(); + createUser(user1.getIdentifier(), user1.getIdentity()); + + final User user2 = new User.Builder().identifier(UUID.randomUUID().toString()).identity("user2").build(); + createUser(user2.getIdentifier(), user2.getIdentity()); + + // Create a group and add user1 to it... + final Group group1 = new Group.Builder().identifier(UUID.randomUUID().toString()).name("group1").build(); + createGroup(group1.getIdentifier(), group1.getName()); + addUserToGroup(user1.getIdentifier(), group1.getIdentifier()); + + // Retrieve the created group... + final Group retrievedGroup1 = userGroupProvider.getGroup(group1.getIdentifier()); + assertNotNull(retrievedGroup1); + assertEquals(group1.getName(), retrievedGroup1.getName()); + assertEquals(1, retrievedGroup1.getUsers().size()); + + // Modify the name and add a user... + final Group modifiedGroup1 = new Group.Builder(retrievedGroup1) + .name(retrievedGroup1.getName() + " updated") + .addUser(user2.getIdentifier()) + .build(); + + // Perform the update... + final Group updatedGroup1 = userGroupProvider.updateGroup(modifiedGroup1); + assertNotNull(updatedGroup1); + + // Re-retrieve and verify the updates were made... + final Group retrievedGroup1AfterUpdate = userGroupProvider.getGroup(group1.getIdentifier()); + assertNotNull(retrievedGroup1AfterUpdate); + assertEquals(modifiedGroup1.getName(), retrievedGroup1AfterUpdate.getName()); + assertEquals(2, retrievedGroup1AfterUpdate.getUsers().size()); + } + + @Test + public void testUpdateGroupWhenDoesNotExist() { + configureWithInitialUsers(); + final Group group1 = new Group.Builder().identifier(UUID.randomUUID().toString()).name("group1").build(); + final Group updatedGroup1 = userGroupProvider.updateGroup(group1); + assertNull(updatedGroup1); + } + + @Test + public void testUpdateGroupRemoveAllUsers() { + configureWithInitialUsers(); + + // Create some users... + final User user1 = new User.Builder().identifier(UUID.randomUUID().toString()).identity("user1").build(); + createUser(user1.getIdentifier(), user1.getIdentity()); + + final User user2 = new User.Builder().identifier(UUID.randomUUID().toString()).identity("user2").build(); + createUser(user2.getIdentifier(), user2.getIdentity()); + + // Create a group and add user1 to it... + final Group group1 = new Group.Builder().identifier(UUID.randomUUID().toString()).name("group1").build(); + createGroup(group1.getIdentifier(), group1.getName()); + addUserToGroup(user1.getIdentifier(), group1.getIdentifier()); + + // Retrieve the created group... + final Group retrievedGroup1 = userGroupProvider.getGroup(group1.getIdentifier()); + assertNotNull(retrievedGroup1); + assertEquals(group1.getName(), retrievedGroup1.getName()); + assertEquals(1, retrievedGroup1.getUsers().size()); + + // Modify the name and add a user... + final Group modifiedGroup1 = new Group.Builder(retrievedGroup1) + .name(retrievedGroup1.getName() + " updated") + .clearUsers() + .build(); + + // Perform the update... + final Group updatedGroup1 = userGroupProvider.updateGroup(modifiedGroup1); + assertNotNull(updatedGroup1); + + // Re-retrieve and verify the updates were made... + final Group retrievedGroup1AfterUpdate = userGroupProvider.getGroup(group1.getIdentifier()); + assertNotNull(retrievedGroup1AfterUpdate); + assertEquals(modifiedGroup1.getName(), retrievedGroup1AfterUpdate.getName()); + assertEquals(0, retrievedGroup1AfterUpdate.getUsers().size()); + } + + @Test + public void testDeleteGroupWhenExists() { + configureWithInitialUsers(); + + // Create some users... + final User user1 = new User.Builder().identifier(UUID.randomUUID().toString()).identity("user1").build(); + createUser(user1.getIdentifier(), user1.getIdentity()); + + final User user2 = new User.Builder().identifier(UUID.randomUUID().toString()).identity("user2").build(); + createUser(user2.getIdentifier(), user2.getIdentity()); + + // Create a group and add user1 to it... + final Group group1 = new Group.Builder().identifier(UUID.randomUUID().toString()).name("group1").build(); + createGroup(group1.getIdentifier(), group1.getName()); + addUserToGroup(user1.getIdentifier(), group1.getIdentifier()); + + // Retrieve the created group... + final Group retrievedGroup1 = userGroupProvider.getGroup(group1.getIdentifier()); + assertNotNull(retrievedGroup1); + + // Delete the group... + final Group deletedGroup1 = userGroupProvider.deleteGroup(retrievedGroup1); + assertNotNull(deletedGroup1); + + assertNull(userGroupProvider.getGroup(group1.getIdentifier())); + } + + @Test + public void testDeleteGroupWhenDoesNotExist() { + configureWithInitialUsers(); + final Group group1 = new Group.Builder().identifier(UUID.randomUUID().toString()).name("group1").build(); + final Group updatedGroup1 = userGroupProvider.deleteGroup(group1); + assertNull(updatedGroup1); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/ldap/tenants/LdapUserGroupProviderTest.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/ldap/tenants/LdapUserGroupProviderTest.java new file mode 100644 index 0000000000..4026121865 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/ldap/tenants/LdapUserGroupProviderTest.java @@ -0,0 +1,777 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.ldap.tenants; + +import org.apache.directory.server.annotations.CreateLdapServer; +import org.apache.directory.server.annotations.CreateTransport; +import org.apache.directory.server.core.annotations.ApplyLdifFiles; +import org.apache.directory.server.core.annotations.CreateDS; +import org.apache.directory.server.core.annotations.CreatePartition; +import org.apache.directory.server.core.integ.AbstractLdapTestUnit; +import org.apache.directory.server.core.integ.FrameworkRunner; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.apache.nifi.registry.security.authorization.AuthorizerConfigurationContext; +import org.apache.nifi.registry.security.authorization.Group; +import org.apache.nifi.registry.security.authorization.UserAndGroups; +import org.apache.nifi.registry.security.authorization.UserGroupProviderInitializationContext; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.identity.DefaultIdentityMapper; +import org.apache.nifi.registry.security.identity.IdentityMapper; +import org.apache.nifi.registry.security.ldap.LdapAuthenticationStrategy; +import org.apache.nifi.registry.security.ldap.ReferralStrategy; +import org.apache.nifi.registry.util.StandardPropertyValue; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; + +import java.util.Properties; +import java.util.Set; + +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_AUTHENTICATION_STRATEGY; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_CONNECT_TIMEOUT; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_GROUP_MEMBERSHIP_ENFORCE_CASE_SENSITIVITY; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_GROUP_MEMBER_ATTRIBUTE; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_GROUP_MEMBER_REFERENCED_USER_ATTRIBUTE; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_GROUP_NAME_ATTRIBUTE; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_GROUP_OBJECT_CLASS; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_GROUP_SEARCH_BASE; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_GROUP_SEARCH_FILTER; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_GROUP_SEARCH_SCOPE; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_MANAGER_DN; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_MANAGER_PASSWORD; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_PAGE_SIZE; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_READ_TIMEOUT; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_REFERRAL_STRATEGY; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_SYNC_INTERVAL; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_URL; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_USER_GROUP_ATTRIBUTE; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_USER_GROUP_REFERENCED_GROUP_ATTRIBUTE; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_USER_IDENTITY_ATTRIBUTE; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_USER_OBJECT_CLASS; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_USER_SEARCH_BASE; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_USER_SEARCH_FILTER; +import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_USER_SEARCH_SCOPE; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(FrameworkRunner.class) +@CreateLdapServer(transports = {@CreateTransport(protocol = "LDAP")}) +@CreateDS(name = "nifi-example", partitions = {@CreatePartition(name = "example", suffix = "o=nifi")}) +@ApplyLdifFiles("nifi-example.ldif") +public class LdapUserGroupProviderTest extends AbstractLdapTestUnit { + + private static final String USER_SEARCH_BASE = "ou=users,o=nifi"; + private static final String GROUP_SEARCH_BASE = "ou=groups,o=nifi"; + + private LdapUserGroupProvider ldapUserGroupProvider; + private IdentityMapper identityMapper; + + @Before + public void setup() { + final UserGroupProviderInitializationContext initializationContext = mock(UserGroupProviderInitializationContext.class); + when(initializationContext.getIdentifier()).thenReturn("identifier"); + + identityMapper = new DefaultIdentityMapper(getNiFiProperties(new Properties())); + + ldapUserGroupProvider = new LdapUserGroupProvider(); + ldapUserGroupProvider.setIdentityMapper(identityMapper); + ldapUserGroupProvider.initialize(initializationContext); + } + + @Test(expected = SecurityProviderCreationException.class) + public void testNoSearchBasesSpecified() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, null); + ldapUserGroupProvider.onConfigured(configurationContext); + } + + @Test(expected = SecurityProviderCreationException.class) + public void testUserSearchBaseSpecifiedButNoUserObjectClass() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null); + when(configurationContext.getProperty(PROP_USER_OBJECT_CLASS)).thenReturn(new StandardPropertyValue(null)); + ldapUserGroupProvider.onConfigured(configurationContext); + } + + @Test(expected = SecurityProviderCreationException.class) + public void testUserSearchBaseSpecifiedButNoUserSearchScope() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null); + when(configurationContext.getProperty(PROP_USER_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue(null)); + ldapUserGroupProvider.onConfigured(configurationContext); + } + + @Test(expected = SecurityProviderCreationException.class) + public void testInvalidUserSearchScope() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null); + when(configurationContext.getProperty(PROP_USER_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue("not-valid")); + ldapUserGroupProvider.onConfigured(configurationContext); + } + + @Test + public void testSearchUsersWithNoIdentityAttribute() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null); + ldapUserGroupProvider.onConfigured(configurationContext); + + assertEquals(8, ldapUserGroupProvider.getUsers().size()); + assertNotNull(ldapUserGroupProvider.getUserByIdentity("cn=User 1,ou=users,o=nifi")); + assertTrue(ldapUserGroupProvider.getGroups().isEmpty()); + } + + @Test + public void testSearchUsersWithUidIdentityAttribute() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null); + when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid")); + ldapUserGroupProvider.onConfigured(configurationContext); + + assertEquals(8, ldapUserGroupProvider.getUsers().size()); + assertNotNull(ldapUserGroupProvider.getUserByIdentity("user1")); + assertTrue(ldapUserGroupProvider.getGroups().isEmpty()); + } + + @Test + public void testSearchUsersWithCnIdentityAttribute() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null); + when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn")); + ldapUserGroupProvider.onConfigured(configurationContext); + + assertEquals(8, ldapUserGroupProvider.getUsers().size()); + assertNotNull(ldapUserGroupProvider.getUserByIdentity("User 1")); + assertTrue(ldapUserGroupProvider.getGroups().isEmpty()); + } + + @Test + public void testSearchUsersObjectSearchScope() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null); + when(configurationContext.getProperty(PROP_USER_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue(SearchScope.OBJECT.name())); + ldapUserGroupProvider.onConfigured(configurationContext); + + assertTrue(ldapUserGroupProvider.getUsers().isEmpty()); + assertTrue(ldapUserGroupProvider.getGroups().isEmpty()); + } + + @Test + public void testSearchUsersSubtreeSearchScope() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration("o=nifi", null); + when(configurationContext.getProperty(PROP_USER_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue(SearchScope.SUBTREE.name())); + ldapUserGroupProvider.onConfigured(configurationContext); + + assertEquals(9, ldapUserGroupProvider.getUsers().size()); + assertTrue(ldapUserGroupProvider.getGroups().isEmpty()); + } + + @Test + public void testSearchUsersWithFilter() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null); + when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid")); + when(configurationContext.getProperty(PROP_USER_SEARCH_FILTER)).thenReturn(new StandardPropertyValue("(uid=user1)")); + ldapUserGroupProvider.onConfigured(configurationContext); + + assertEquals(1, ldapUserGroupProvider.getUsers().size()); + assertNotNull(ldapUserGroupProvider.getUserByIdentity("user1")); + assertTrue(ldapUserGroupProvider.getGroups().isEmpty()); + } + + @Test + public void testSearchUsersWithPaging() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null); + when(configurationContext.getProperty(PROP_PAGE_SIZE)).thenReturn(new StandardPropertyValue("1")); + ldapUserGroupProvider.onConfigured(configurationContext); + + assertEquals(8, ldapUserGroupProvider.getUsers().size()); + assertTrue(ldapUserGroupProvider.getGroups().isEmpty()); + } + + @Test + public void testSearchUsersWithGroupingNoGroupName() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null); + when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid")); + when(configurationContext.getProperty(PROP_USER_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue("description")); // using description in lieu of memberof + ldapUserGroupProvider.onConfigured(configurationContext); + + assertEquals(8, ldapUserGroupProvider.getUsers().size()); + assertEquals(3, ldapUserGroupProvider.getGroups().size()); + + final UserAndGroups user4AndGroups = ldapUserGroupProvider.getUserAndGroups("user4"); + assertNotNull(user4AndGroups.getUser()); + assertEquals(1, user4AndGroups.getGroups().size()); + assertEquals("cn=team1,ou=groups,o=nifi", user4AndGroups.getGroups().iterator().next().getName()); + + final UserAndGroups user7AndGroups = ldapUserGroupProvider.getUserAndGroups("user7"); + assertNotNull(user7AndGroups.getUser()); + assertEquals(1, user7AndGroups.getGroups().size()); + assertEquals("cn=team2,ou=groups,o=nifi", user7AndGroups.getGroups().iterator().next().getName()); + + final UserAndGroups user8AndGroups = ldapUserGroupProvider.getUserAndGroups("user8"); + assertNotNull(user8AndGroups.getUser()); + assertEquals(1, user8AndGroups.getGroups().size()); + assertEquals("cn=Team2,ou=groups,o=nifi", user8AndGroups.getGroups().iterator().next().getName()); + } + + @Test + public void testSearchUsersWithGroupingAndGroupName() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null); + when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid")); + when(configurationContext.getProperty(PROP_USER_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue("description")); // using description in lieu of memberof + when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn")); + ldapUserGroupProvider.onConfigured(configurationContext); + + assertEquals(8, ldapUserGroupProvider.getUsers().size()); + assertEquals(2, ldapUserGroupProvider.getGroups().size()); + + final UserAndGroups userAndGroups = ldapUserGroupProvider.getUserAndGroups("user4"); + assertNotNull(userAndGroups.getUser()); + assertEquals(1, userAndGroups.getGroups().size()); + assertEquals("team1", userAndGroups.getGroups().iterator().next().getName()); + } + + @Test(expected = SecurityProviderCreationException.class) + public void testSearchGroupsWithoutMemberAttribute() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE); + ldapUserGroupProvider.onConfigured(configurationContext); + } + + @Test(expected = SecurityProviderCreationException.class) + public void testGroupSearchBaseSpecifiedButNoGroupObjectClass() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE); + when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member")); + when(configurationContext.getProperty(PROP_GROUP_OBJECT_CLASS)).thenReturn(new StandardPropertyValue(null)); + ldapUserGroupProvider.onConfigured(configurationContext); + } + + @Test(expected = SecurityProviderCreationException.class) + public void testUserSearchBaseSpecifiedButNoGroupSearchScope() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE); + when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member")); + when(configurationContext.getProperty(PROP_GROUP_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue(null)); + ldapUserGroupProvider.onConfigured(configurationContext); + } + + @Test(expected = SecurityProviderCreationException.class) + public void testInvalidGroupSearchScope() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE); + when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member")); + when(configurationContext.getProperty(PROP_GROUP_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue("not-valid")); + ldapUserGroupProvider.onConfigured(configurationContext); + } + + @Test + public void testSearchGroupsWithNoNameAttribute() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE); + when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member")); + ldapUserGroupProvider.onConfigured(configurationContext); + + final Set groups = ldapUserGroupProvider.getGroups(); + assertEquals(5, groups.size()); + assertEquals(1, groups.stream().filter(group -> "cn=admins,ou=groups,o=nifi".equals(group.getName())).count()); + } + + @Test + public void testSearchGroupsWithPaging() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE); + when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member")); + when(configurationContext.getProperty(PROP_PAGE_SIZE)).thenReturn(new StandardPropertyValue("1")); + ldapUserGroupProvider.onConfigured(configurationContext); + + assertEquals(5, ldapUserGroupProvider.getGroups().size()); + } + + @Test + public void testSearchGroupsObjectSearchScope() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE); + when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member")); + when(configurationContext.getProperty(PROP_GROUP_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue(SearchScope.OBJECT.name())); + ldapUserGroupProvider.onConfigured(configurationContext); + + assertTrue(ldapUserGroupProvider.getUsers().isEmpty()); + assertTrue(ldapUserGroupProvider.getGroups().isEmpty()); + } + + @Test + public void testSearchGroupsSubtreeSearchScope() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, "o=nifi"); + when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member")); + when(configurationContext.getProperty(PROP_GROUP_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue(SearchScope.SUBTREE.name())); + ldapUserGroupProvider.onConfigured(configurationContext); + + assertEquals(5, ldapUserGroupProvider.getGroups().size()); + } + + @Test + public void testSearchGroupsWithNameAttribute() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE); + when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member")); + when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn")); + ldapUserGroupProvider.onConfigured(configurationContext); + + final Set groups = ldapUserGroupProvider.getGroups(); + assertEquals(5, groups.size()); + + final Group admins = groups.stream().filter(group -> "admins".equals(group.getName())).findFirst().orElse(null); + assertNotNull(admins); + assertFalse(admins.getUsers().isEmpty()); + assertEquals(1, admins.getUsers().stream().map( + userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter( + user -> "cn=User 1,ou=users,o=nifi".equals(user.getIdentity())).count()); + } + + @Test + public void testSearchGroupsWithNoNameAndUserIdentityUidAttribute() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE); + when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member")); + when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid")); + ldapUserGroupProvider.onConfigured(configurationContext); + + final Set groups = ldapUserGroupProvider.getGroups(); + assertEquals(5, groups.size()); + + final Group admins = groups.stream().filter(group -> "cn=admins,ou=groups,o=nifi".equals(group.getName())).findFirst().orElse(null); + assertNotNull(admins); + assertFalse(admins.getUsers().isEmpty()); + assertEquals(1, admins.getUsers().stream().map( + userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter( + user -> "user1".equals(user.getIdentity())).count()); + } + + @Test + public void testSearchGroupsWithNameAndUserIdentityCnAttribute() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE); + when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member")); + when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn")); + when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn")); + ldapUserGroupProvider.onConfigured(configurationContext); + + final Set groups = ldapUserGroupProvider.getGroups(); + assertEquals(5, groups.size()); + + final Group admins = groups.stream().filter(group -> "admins".equals(group.getName())).findFirst().orElse(null); + assertNotNull(admins); + assertFalse(admins.getUsers().isEmpty()); + assertEquals(1, admins.getUsers().stream().map( + userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter( + user -> "User 1".equals(user.getIdentity())).count()); + } + + @Test + public void testSearchGroupsWithFilter() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE); + when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member")); + when(configurationContext.getProperty(PROP_GROUP_SEARCH_FILTER)).thenReturn(new StandardPropertyValue("(cn=admins)")); + ldapUserGroupProvider.onConfigured(configurationContext); + + final Set groups = ldapUserGroupProvider.getGroups(); + assertEquals(1, groups.size()); + assertEquals(1, groups.stream().filter(group -> "cn=admins,ou=groups,o=nifi".equals(group.getName())).count()); + } + + @Test + public void testSearchUsersAndGroupsNoMembership() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, GROUP_SEARCH_BASE); + ldapUserGroupProvider.onConfigured(configurationContext); + + assertEquals(8, ldapUserGroupProvider.getUsers().size()); + + final Set groups = ldapUserGroupProvider.getGroups(); + assertEquals(5, groups.size()); + groups.forEach(group -> assertTrue(group.getUsers().isEmpty())); + } + + @Test + public void testSearchUsersAndGroupsMembershipThroughUsers() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, GROUP_SEARCH_BASE); + when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid")); + when(configurationContext.getProperty(PROP_USER_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue("description")); // using description in lieu of memberof + when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn")); + ldapUserGroupProvider.onConfigured(configurationContext); + + assertEquals(8, ldapUserGroupProvider.getUsers().size()); + + final Set groups = ldapUserGroupProvider.getGroups(); + assertEquals(5, groups.size()); + + final Group team1 = groups.stream().filter(group -> "team1".equals(group.getName())).findFirst().orElse(null); + assertNotNull(team1); + assertEquals(2, team1.getUsers().size()); + assertEquals(2, team1.getUsers().stream().map( + userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter( + user -> "user4".equals(user.getIdentity()) || "user5".equals(user.getIdentity())).count()); + + final Group team2 = groups.stream().filter(group -> "team2".equals(group.getName())).findFirst().orElse(null); + assertNotNull(team2); + assertEquals(2, team2.getUsers().size()); + assertEquals(2, team2.getUsers().stream().map( + userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter( + user -> "user6".equals(user.getIdentity()) || "user7".equals(user.getIdentity())).count()); + } + + @Test + public void testSearchUsersAndGroupsMembershipThroughGroups() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, GROUP_SEARCH_BASE); + when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid")); + when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member")); + when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn")); + ldapUserGroupProvider.onConfigured(configurationContext); + + assertEquals(8, ldapUserGroupProvider.getUsers().size()); + + final Set groups = ldapUserGroupProvider.getGroups(); + assertEquals(5, groups.size()); + + final Group admins = groups.stream().filter(group -> "admins".equals(group.getName())).findFirst().orElse(null); + assertNotNull(admins); + assertEquals(2, admins.getUsers().size()); + assertEquals(2, admins.getUsers().stream().map( + userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter( + user -> "user1".equals(user.getIdentity()) || "user3".equals(user.getIdentity())).count()); + + final Group readOnly = groups.stream().filter(group -> "read-only".equals(group.getName())).findFirst().orElse(null); + assertNotNull(readOnly); + assertEquals(1, readOnly.getUsers().size()); + assertEquals(1, readOnly.getUsers().stream().map( + userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter( + user -> "user2".equals(user.getIdentity())).count()); + + final Group team1 = groups.stream().filter(group -> "team1".equals(group.getName())).findFirst().orElse(null); + assertNotNull(team1); + assertEquals(1, team1.getUsers().size()); + assertEquals(1, team1.getUsers().stream().map( + userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter( + user -> "user1".equals(user.getIdentity())).count()); + + final Group team2 = groups.stream().filter(group -> "team2".equals(group.getName())).findFirst().orElse(null); + assertNotNull(team2); + assertEquals(1, team2.getUsers().size()); + assertEquals(1, team2.getUsers().stream().map( + userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter( + user -> "user1".equals(user.getIdentity())).count()); + } + + @Test + public void testSearchUsersAndGroupsMembershipThroughUsersAndGroups() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, GROUP_SEARCH_BASE); + when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid")); + when(configurationContext.getProperty(PROP_USER_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue("description")); // using description in lieu of memberof + when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member")); + when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn")); + ldapUserGroupProvider.onConfigured(configurationContext); + + assertEquals(8, ldapUserGroupProvider.getUsers().size()); + + final Set groups = ldapUserGroupProvider.getGroups(); + assertEquals(5, groups.size()); + + final Group admins = groups.stream().filter(group -> "admins".equals(group.getName())).findFirst().orElse(null); + assertNotNull(admins); + assertEquals(2, admins.getUsers().size()); + assertEquals(2, admins.getUsers().stream().map( + userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter( + user -> "user1".equals(user.getIdentity()) || "user3".equals(user.getIdentity())).count()); + + final Group readOnly = groups.stream().filter(group -> "read-only".equals(group.getName())).findFirst().orElse(null); + assertNotNull(readOnly); + assertEquals(1, readOnly.getUsers().size()); + assertEquals(1, readOnly.getUsers().stream().map( + userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter( + user -> "user2".equals(user.getIdentity())).count()); + + final Group team1 = groups.stream().filter(group -> "team1".equals(group.getName())).findFirst().orElse(null); + assertNotNull(team1); + assertEquals(3, team1.getUsers().size()); + assertEquals(3, team1.getUsers().stream().map( + userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter( + user -> "user1".equals(user.getIdentity()) || "user4".equals(user.getIdentity()) || "user5".equals(user.getIdentity())).count()); + + final Group team2 = groups.stream().filter(group -> "team2".equals(group.getName())).findFirst().orElse(null); + assertNotNull(team2); + assertEquals(3, team2.getUsers().size()); + assertEquals(3, team2.getUsers().stream().map( + userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter( + user -> "user1".equals(user.getIdentity()) || "user6".equals(user.getIdentity()) || "user7".equals(user.getIdentity())).count()); + } + + @Test + public void testUserIdentityMapping() throws Exception { + final Properties props = new Properties(); + props.setProperty("nifi.registry.security.identity.mapping.pattern.dn1", "^cn=(.*?),o=(.*?)$"); + props.setProperty("nifi.registry.security.identity.mapping.value.dn1", "$1"); + + final NiFiRegistryProperties properties = getNiFiProperties(props); + identityMapper = new DefaultIdentityMapper(properties); + ldapUserGroupProvider.setIdentityMapper(identityMapper); + + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null); + when(configurationContext.getProperty(PROP_USER_SEARCH_FILTER)).thenReturn(new StandardPropertyValue("(uid=user1)")); + ldapUserGroupProvider.onConfigured(configurationContext); + + assertEquals(1, ldapUserGroupProvider.getUsers().size()); + assertNotNull(ldapUserGroupProvider.getUserByIdentity("User 1,ou=users")); + } + + @Test + public void testUserIdentityMappingWithTransforms() throws Exception { + final Properties props = new Properties(); + props.setProperty("nifi.registry.security.identity.mapping.pattern.dn1", "^cn=(.*?),ou=(.*?),o=(.*?)$"); + props.setProperty("nifi.registry.security.identity.mapping.value.dn1", "$1"); + props.setProperty("nifi.registry.security.identity.mapping.transform.dn1", "UPPER"); + + final NiFiRegistryProperties properties = getNiFiProperties(props); + identityMapper = new DefaultIdentityMapper(properties); + ldapUserGroupProvider.setIdentityMapper(identityMapper); + + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null); + when(configurationContext.getProperty(PROP_USER_SEARCH_FILTER)).thenReturn(new StandardPropertyValue("(uid=user1)")); + ldapUserGroupProvider.onConfigured(configurationContext); + + assertEquals(1, ldapUserGroupProvider.getUsers().size()); + assertNotNull(ldapUserGroupProvider.getUserByIdentity("USER 1")); + } + + @Test + public void testUserIdentityAndGroupMappingWithTransforms() throws Exception { + final Properties props = new Properties(); + props.setProperty("nifi.registry.security.identity.mapping.pattern.dn1", "^cn=(.*?),ou=(.*?),o=(.*?)$"); + props.setProperty("nifi.registry.security.identity.mapping.value.dn1", "$1"); + props.setProperty("nifi.registry.security.identity.mapping.transform.dn1", "UPPER"); + props.setProperty("nifi.registry.security.group.mapping.pattern.dn1", "^cn=(.*?),ou=(.*?),o=(.*?)$"); + props.setProperty("nifi.registry.security.group.mapping.value.dn1", "$1"); + props.setProperty("nifi.registry.security.group.mapping.transform.dn1", "UPPER"); + + final NiFiRegistryProperties properties = getNiFiProperties(props); + identityMapper = new DefaultIdentityMapper(properties); + ldapUserGroupProvider.setIdentityMapper(identityMapper); + + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, GROUP_SEARCH_BASE); + when(configurationContext.getProperty(PROP_USER_SEARCH_FILTER)).thenReturn(new StandardPropertyValue("(uid=user1)")); + when(configurationContext.getProperty(PROP_GROUP_SEARCH_FILTER)).thenReturn(new StandardPropertyValue("(cn=admins)")); + ldapUserGroupProvider.onConfigured(configurationContext); + + assertEquals(1, ldapUserGroupProvider.getUsers().size()); + assertNotNull(ldapUserGroupProvider.getUserByIdentity("USER 1")); + + assertEquals(1, ldapUserGroupProvider.getGroups().size()); + assertEquals("ADMINS", ldapUserGroupProvider.getGroups().iterator().next().getName()); + } + + @Test(expected = SecurityProviderCreationException.class) + public void testReferencedGroupAttributeWithoutGroupSearchBase() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration("ou=users-2,o=nifi", null); + when(configurationContext.getProperty(PROP_USER_GROUP_REFERENCED_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn")); + ldapUserGroupProvider.onConfigured(configurationContext); + } + + @Test + public void testReferencedGroupWithoutDefiningReferencedAttribute() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration("ou=users-2,o=nifi", "ou=groups-2,o=nifi"); + when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid")); + when(configurationContext.getProperty(PROP_USER_OBJECT_CLASS)).thenReturn(new StandardPropertyValue("room")); // using room due to reqs of groupOfNames + when(configurationContext.getProperty(PROP_USER_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue("description")); // using description in lieu of member + when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn")); + when(configurationContext.getProperty(PROP_GROUP_OBJECT_CLASS)).thenReturn(new StandardPropertyValue("room")); // using room due to reqs of groupOfNames + ldapUserGroupProvider.onConfigured(configurationContext); + + final Set groups = ldapUserGroupProvider.getGroups(); + assertEquals(1, groups.size()); + + final Group team3 = groups.stream().filter(group -> "team3".equals(group.getName())).findFirst().orElse(null); + assertNotNull(team3); + assertTrue(team3.getUsers().isEmpty()); + } + + @Test + public void testReferencedGroupUsingReferencedAttribute() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration("ou=users-2,o=nifi", "ou=groups-2,o=nifi"); + when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid")); + when(configurationContext.getProperty(PROP_USER_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue("description")); // using description in lieu of member + when(configurationContext.getProperty(PROP_USER_GROUP_REFERENCED_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn")); + when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn")); + when(configurationContext.getProperty(PROP_GROUP_OBJECT_CLASS)).thenReturn(new StandardPropertyValue("room")); // using room because groupOfNames requires a member + ldapUserGroupProvider.onConfigured(configurationContext); + + final Set groups = ldapUserGroupProvider.getGroups(); + assertEquals(1, groups.size()); + + final Group team3 = groups.stream().filter(group -> "team3".equals(group.getName())).findFirst().orElse(null); + assertNotNull(team3); + assertEquals(1, team3.getUsers().size()); + assertEquals(1, team3.getUsers().stream().map( + userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter( + user -> "user9".equals(user.getIdentity())).count()); + } + + @Test(expected = SecurityProviderCreationException.class) + public void testReferencedUserWithoutUserSearchBase() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, "ou=groups-2,o=nifi"); + when(configurationContext.getProperty(PROP_GROUP_MEMBER_REFERENCED_USER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid")); + ldapUserGroupProvider.onConfigured(configurationContext); + } + + @Test + public void testReferencedUserWithoutDefiningReferencedAttribute() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration("ou=users-2,o=nifi", "ou=groups-2,o=nifi"); + when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid")); + when(configurationContext.getProperty(PROP_GROUP_OBJECT_CLASS)).thenReturn(new StandardPropertyValue("room")); // using room due to reqs of groupOfNames + when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("description")); // using description in lieu of member + when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn")); + ldapUserGroupProvider.onConfigured(configurationContext); + + final Set groups = ldapUserGroupProvider.getGroups(); + assertEquals(1, groups.size()); + + final Group team3 = groups.stream().filter(group -> "team3".equals(group.getName())).findFirst().orElse(null); + assertNotNull(team3); + assertTrue(team3.getUsers().isEmpty()); + } + + @Test + public void testReferencedUserUsingReferencedAttribute() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration("ou=users-2,o=nifi", "ou=groups-2,o=nifi"); + when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("sn")); + when(configurationContext.getProperty(PROP_GROUP_OBJECT_CLASS)).thenReturn(new StandardPropertyValue("room")); // using room due to reqs of groupOfNames + when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("description")); // using description in lieu of member + when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn")); + when(configurationContext.getProperty(PROP_GROUP_MEMBER_REFERENCED_USER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid")); // does not need to be the same as user id attr + ldapUserGroupProvider.onConfigured(configurationContext); + + final Set groups = ldapUserGroupProvider.getGroups(); + assertEquals(1, groups.size()); + + final Group team3 = groups.stream().filter(group -> "team3".equals(group.getName())).findFirst().orElse(null); + assertNotNull(team3); + assertEquals(1, team3.getUsers().size()); + assertEquals(1, team3.getUsers().stream().map( + userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter( + user -> "User9".equals(user.getIdentity())).count()); + } + + @Test + public void testSearchUsersAndGroupsMembershipThroughGroupsCaseInsensitive() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, GROUP_SEARCH_BASE); + when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid")); + when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member")); + when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn")); + when(configurationContext.getProperty(PROP_GROUP_MEMBERSHIP_ENFORCE_CASE_SENSITIVITY)).thenReturn(new StandardPropertyValue("false")); + ldapUserGroupProvider.onConfigured(configurationContext); + + assertEquals(8, ldapUserGroupProvider.getUsers().size()); + + final Set groups = ldapUserGroupProvider.getGroups(); + assertEquals(5, groups.size()); + + final Group team4 = groups.stream().filter(group -> "team4".equals(group.getName())).findFirst().orElse(null); + assertNotNull(team4); + assertEquals(2, team4.getUsers().size()); + assertEquals(1, team4.getUsers().stream().map( + userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter( + user -> "user1".equals(user.getIdentity())).count()); + assertEquals(1, team4.getUsers().stream().map( + userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter( + user -> "user2".equals(user.getIdentity())).count()); + } + + @Test + public void testSearchUsersAndGroupsMembershipThroughGroupsCaseSensitive() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, GROUP_SEARCH_BASE); + when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid")); + when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member")); + when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn")); + ldapUserGroupProvider.onConfigured(configurationContext); + + assertEquals(8, ldapUserGroupProvider.getUsers().size()); + + final Set groups = ldapUserGroupProvider.getGroups(); + assertEquals(5, groups.size()); + + final Group team4 = groups.stream().filter(group -> "team4".equals(group.getName())).findFirst().orElse(null); + assertNotNull(team4); + assertEquals(1, team4.getUsers().size()); + assertEquals(1, team4.getUsers().stream().map( + userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter( + user -> "user1".equals(user.getIdentity())).count()); + } + + @Test + public void testSearchUsersAndGroupsMembershipThroughUsersCaseInsensitive() throws Exception { + final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, GROUP_SEARCH_BASE); + when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid")); + when(configurationContext.getProperty(PROP_USER_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue("description")); // using description in lieu of memberof + when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn")); + when(configurationContext.getProperty(PROP_GROUP_MEMBERSHIP_ENFORCE_CASE_SENSITIVITY)).thenReturn(new StandardPropertyValue("false")); + ldapUserGroupProvider.onConfigured(configurationContext); + + assertEquals(8, ldapUserGroupProvider.getUsers().size()); + + final Set groups = ldapUserGroupProvider.getGroups(); + assertEquals(5, groups.size()); + + final Group team1 = groups.stream().filter(group -> "team1".equals(group.getName())).findFirst().orElse(null); + assertNotNull(team1); + assertEquals(2, team1.getUsers().size()); + assertEquals(2, team1.getUsers().stream().map( + userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter( + user -> "user4".equals(user.getIdentity()) || "user5".equals(user.getIdentity())).count()); + + final Group team2 = groups.stream().filter(group -> "team2".equals(group.getName())).findFirst().orElse(null); + assertNotNull(team2); + assertEquals(3, team2.getUsers().size()); + assertEquals(3, team2.getUsers().stream().map( + userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter( + user -> "user6".equals(user.getIdentity()) || "user7".equals(user.getIdentity()) || "user8".equals(user.getIdentity())).count()); + } + + private AuthorizerConfigurationContext getBaseConfiguration(final String userSearchBase, final String groupSearchBase) { + final AuthorizerConfigurationContext configurationContext = mock(AuthorizerConfigurationContext.class); + when(configurationContext.getProperty(PROP_URL)).thenReturn(new StandardPropertyValue("ldap://127.0.0.1:" + getLdapServer().getPort())); + when(configurationContext.getProperty(PROP_CONNECT_TIMEOUT)).thenReturn(new StandardPropertyValue("30 secs")); + when(configurationContext.getProperty(PROP_READ_TIMEOUT)).thenReturn(new StandardPropertyValue("30 secs")); + when(configurationContext.getProperty(PROP_REFERRAL_STRATEGY)).thenReturn(new StandardPropertyValue(ReferralStrategy.FOLLOW.name())); + when(configurationContext.getProperty(PROP_PAGE_SIZE)).thenReturn(new StandardPropertyValue(null)); + when(configurationContext.getProperty(PROP_SYNC_INTERVAL)).thenReturn(new StandardPropertyValue("30 mins")); + when(configurationContext.getProperty(PROP_GROUP_MEMBERSHIP_ENFORCE_CASE_SENSITIVITY)).thenReturn(new StandardPropertyValue("true")); + + when(configurationContext.getProperty(PROP_AUTHENTICATION_STRATEGY)).thenReturn(new StandardPropertyValue(LdapAuthenticationStrategy.SIMPLE.name())); + when(configurationContext.getProperty(PROP_MANAGER_DN)).thenReturn(new StandardPropertyValue("uid=admin,ou=system")); + when(configurationContext.getProperty(PROP_MANAGER_PASSWORD)).thenReturn(new StandardPropertyValue("secret")); + + when(configurationContext.getProperty(PROP_USER_SEARCH_BASE)).thenReturn(new StandardPropertyValue(userSearchBase)); + when(configurationContext.getProperty(PROP_USER_OBJECT_CLASS)).thenReturn(new StandardPropertyValue("person")); + when(configurationContext.getProperty(PROP_USER_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue(SearchScope.ONE_LEVEL.name())); + when(configurationContext.getProperty(PROP_USER_SEARCH_FILTER)).thenReturn(new StandardPropertyValue(null)); + when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue(null)); + when(configurationContext.getProperty(PROP_USER_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue(null)); + when(configurationContext.getProperty(PROP_USER_GROUP_REFERENCED_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue(null)); + + when(configurationContext.getProperty(PROP_GROUP_SEARCH_BASE)).thenReturn(new StandardPropertyValue(groupSearchBase)); + when(configurationContext.getProperty(PROP_GROUP_OBJECT_CLASS)).thenReturn(new StandardPropertyValue("groupOfNames")); + when(configurationContext.getProperty(PROP_GROUP_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue(SearchScope.ONE_LEVEL.name())); + when(configurationContext.getProperty(PROP_GROUP_SEARCH_FILTER)).thenReturn(new StandardPropertyValue(null)); + when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue(null)); + when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue(null)); + when(configurationContext.getProperty(PROP_GROUP_MEMBER_REFERENCED_USER_ATTRIBUTE)).thenReturn(new StandardPropertyValue(null)); + + return configurationContext; + } + + private NiFiRegistryProperties getNiFiProperties(final Properties properties) { + final NiFiRegistryProperties registryProperties = Mockito.mock(NiFiRegistryProperties.class); + when(registryProperties.getPropertyKeys()).thenReturn(properties.stringPropertyNames()); + when(registryProperties.getProperty(anyString())).then(invocationOnMock -> properties.getProperty((String) invocationOnMock.getArguments()[0])); + return registryProperties; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/serialization/TestExtensionSerializer.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/serialization/TestExtensionSerializer.java new file mode 100644 index 0000000000..7fea13a669 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/serialization/TestExtensionSerializer.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.serialization; + +import org.apache.nifi.registry.extension.component.manifest.Extension; +import org.junit.Before; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class TestExtensionSerializer { + + private Serializer serializer; + + @Before + public void setup() { + serializer = new ExtensionSerializer(); + } + + @Test + public void testSerializeAndDeserialize() { + final Extension extension = new Extension(); + extension.setName("org.apache.nifi.FooProcessor"); + extension.setDescription("This is the foo processor"); + + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + serializer.serialize(extension, outputStream); + + final ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray()); + final Extension deserialized = serializer.deserialize(inputStream); + assertNotNull(deserialized); + assertEquals(extension.getName(), deserialized.getName()); + assertEquals(extension.getDescription(), deserialized.getDescription()); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/serialization/TestFlowContentSerializer.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/serialization/TestFlowContentSerializer.java new file mode 100644 index 0000000000..21d3b43299 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/serialization/TestFlowContentSerializer.java @@ -0,0 +1,282 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.serialization; + +import org.apache.nifi.registry.flow.ExternalControllerServiceReference; +import org.apache.nifi.registry.flow.VersionedFlowSnapshot; +import org.apache.nifi.registry.flow.VersionedProcessGroup; +import org.apache.nifi.registry.flow.VersionedProcessor; +import org.junit.Before; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; +import static org.junit.Assert.assertTrue; + +public class TestFlowContentSerializer { + + private FlowContentSerializer serializer; + + @Before + public void setup() { + serializer = new FlowContentSerializer(); + } + + @Test + public void testSerializeDeserializeFlowContent() { + final VersionedProcessor processor1 = new VersionedProcessor(); + processor1.setIdentifier("processor1"); + processor1.setName("My Processor 1"); + + final VersionedProcessGroup processGroup1 = new VersionedProcessGroup(); + processGroup1.setIdentifier("pg1"); + processGroup1.setName("My Process Group"); + processGroup1.getProcessors().add(processor1); + + final VersionedFlowSnapshot snapshot = new VersionedFlowSnapshot(); + snapshot.setFlowContents(processGroup1); + + final FlowContent flowContent = new FlowContent(); + flowContent.setFlowSnapshot(snapshot); + + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + serializer.serializeFlowContent(flowContent, out); + + //final String json = new String(out.toByteArray(), StandardCharsets.UTF_8); + //System.out.println(json); + + final ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + + // make sure we can read the version from the input stream and it should be the current version + final Integer version = serializer.readDataModelVersion(in); + assertEquals(serializer.getCurrentDataModelVersion(), version); + assertEquals(false, serializer.isProcessGroupVersion(version)); + + // make sure we can deserialize back to FlowContent + final FlowContent deserializedFlowContent = serializer.deserializeFlowContent(version, in); + assertNotNull(deserializedFlowContent); + + final VersionedFlowSnapshot deserializedSnapshot = deserializedFlowContent.getFlowSnapshot(); + assertNotNull(deserializedSnapshot); + + final VersionedProcessGroup deserializedProcessGroup1 = deserializedSnapshot.getFlowContents(); + assertNotNull(deserializedProcessGroup1); + assertEquals(processGroup1.getIdentifier(), deserializedProcessGroup1.getIdentifier()); + assertEquals(processGroup1.getName(), deserializedProcessGroup1.getName()); + + assertEquals(1, deserializedProcessGroup1.getProcessors().size()); + + final VersionedProcessor deserializedProcessor1 = deserializedProcessGroup1.getProcessors().iterator().next(); + assertEquals(processor1.getIdentifier(), deserializedProcessor1.getIdentifier()); + assertEquals(processor1.getName(), deserializedProcessor1.getName()); + } + + @Test + public void testSerializeDeserializeWithExternalServices() throws SerializationException { + final VersionedProcessGroup processGroup1 = new VersionedProcessGroup(); + processGroup1.setIdentifier("pg1"); + processGroup1.setName("My Process Group"); + + final ExternalControllerServiceReference serviceReference1 = new ExternalControllerServiceReference(); + serviceReference1.setIdentifier("1"); + serviceReference1.setName("Service 1"); + + final ExternalControllerServiceReference serviceReference2 = new ExternalControllerServiceReference(); + serviceReference2.setIdentifier("2"); + serviceReference2.setName("Service 2"); + + final Map serviceReferences = new HashMap<>(); + serviceReferences.put(serviceReference1.getIdentifier(), serviceReference1); + serviceReferences.put(serviceReference2.getIdentifier(), serviceReference2); + + final VersionedFlowSnapshot snapshot = new VersionedFlowSnapshot(); + snapshot.setFlowContents(processGroup1); + snapshot.setExternalControllerServices(serviceReferences); + + final FlowContent flowContent = new FlowContent(); + flowContent.setFlowSnapshot(snapshot); + + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + serializer.serializeFlowContent(flowContent, out); + + final ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + + // make sure we can read the version from the input stream and it should be the current version + final Integer version = serializer.readDataModelVersion(in); + assertEquals(serializer.getCurrentDataModelVersion(), version); + + // make sure we can deserialize back to FlowContent + final FlowContent deserializedFlowContent = serializer.deserializeFlowContent(version, in); + assertNotNull(deserializedFlowContent); + + final VersionedFlowSnapshot deserializedSnapshot = deserializedFlowContent.getFlowSnapshot(); + assertNotNull(deserializedSnapshot); + + final VersionedProcessGroup deserializedProcessGroup = deserializedSnapshot.getFlowContents(); + assertEquals(processGroup1.getIdentifier(), deserializedProcessGroup.getIdentifier()); + assertEquals(processGroup1.getName(), deserializedProcessGroup.getName()); + + final Map deserializedServiceReferences = deserializedSnapshot.getExternalControllerServices(); + assertNotNull(deserializedServiceReferences); + assertEquals(2, deserializedServiceReferences.size()); + + final ExternalControllerServiceReference deserializedServiceReference1 = deserializedServiceReferences.get(serviceReference1.getIdentifier()); + assertNotNull(deserializedServiceReference1); + assertEquals(serviceReference1.getIdentifier(), deserializedServiceReference1.getIdentifier()); + assertEquals(serviceReference1.getName(), deserializedServiceReference1.getName()); + } + + @Test + public void testDeserializeJsonNonIntegerVersion() throws IOException { + final String file = "/serialization/json/non-integer-version.snapshot"; + try (final InputStream is = this.getClass().getResourceAsStream(file)) { + try { + serializer.readDataModelVersion(is); + fail("Should fail"); + } catch (SerializationException e) { + assertEquals("Unable to read the data model version for the flow content.", e.getMessage()); + } + } + } + + @Test + public void testDeserializeJsonNoVersion() throws IOException { + final String file = "/serialization/json/no-version.snapshot"; + try (final InputStream is = this.getClass().getResourceAsStream(file)) { + try { + serializer.readDataModelVersion(is); + fail("Should fail"); + } catch (SerializationException e) { + assertEquals("Unable to read the data model version for the flow content.", e.getMessage()); + } + } + } + + @Test + public void testDeserializeVer1() throws IOException { + final String file = "/serialization/ver1.snapshot"; + final VersionedProcessGroup processGroup; + try (final InputStream is = this.getClass().getResourceAsStream(file)) { + final Integer version = serializer.readDataModelVersion(is); + assertNotNull(version); + assertEquals(1, version.intValue()); + + if (serializer.isProcessGroupVersion(version)) { + processGroup = serializer.deserializeProcessGroup(version, is); + } else { + processGroup = null; + } + } + + assertNotNull(processGroup); + assertNotNull(processGroup.getProcessors()); + assertTrue(processGroup.getProcessors().size() > 0); + //System.out.printf("processGroup=" + processGroup); + } + + @Test + public void testDeserializeVer2() throws IOException { + final String file = "/serialization/ver2.snapshot"; + final VersionedProcessGroup processGroup; + try (final InputStream is = this.getClass().getResourceAsStream(file)) { + final Integer version = serializer.readDataModelVersion(is); + assertNotNull(version); + assertEquals(2, version.intValue()); + + if (serializer.isProcessGroupVersion(version)) { + processGroup = serializer.deserializeProcessGroup(version, is); + } else { + processGroup = null; + } + } + + assertNotNull(processGroup); + assertNotNull(processGroup.getProcessors()); + assertTrue(processGroup.getProcessors().size() > 0); + //System.out.printf("processGroup=" + processGroup); + } + + @Test + public void testDeserializeVer3() throws IOException { + final String file = "/serialization/ver3.snapshot"; + try (final InputStream is = this.getClass().getResourceAsStream(file)) { + final Integer version = serializer.readDataModelVersion(is); + assertNotNull(version); + assertEquals(3, version.intValue()); + assertFalse(serializer.isProcessGroupVersion(version)); + + final FlowContent flowContent = serializer.deserializeFlowContent(version, is); + assertNotNull(flowContent); + + final VersionedFlowSnapshot flowSnapshot = flowContent.getFlowSnapshot(); + assertNotNull(flowSnapshot); + + final VersionedProcessGroup processGroup = flowSnapshot.getFlowContents(); + assertNotNull(processGroup); + assertNotNull(processGroup.getProcessors()); + assertEquals(1, processGroup.getProcessors().size()); + } + } + + @Test + public void testDeserializeVer9999() throws IOException { + final String file = "/serialization/ver9999.snapshot"; + try (final InputStream is = this.getClass().getResourceAsStream(file)) { + final Integer version = serializer.readDataModelVersion(is); + assertNotNull(version); + assertEquals(9999, version.intValue()); + assertFalse(serializer.isProcessGroupVersion(version)); + + try { + serializer.deserializeFlowContent(version, is); + fail("Should fail"); + } catch (IllegalArgumentException e) { + assertEquals("No FlowContent serializer exists for data model version: " + version, e.getMessage()); + } + + try { + serializer.deserializeProcessGroup(version, is); + fail("Should fail"); + } catch (IllegalArgumentException e) { + assertEquals("No VersionedProcessGroup serializer exists for data model version: " + version, e.getMessage()); + } + } + } + + @Test + public void testDeserializeProcessGroupAsFlowContent() throws IOException { + final String file = "/serialization/ver2.snapshot"; + try (final InputStream is = this.getClass().getResourceAsStream(file)) { + final Integer version = serializer.readDataModelVersion(is); + assertNotNull(version); + assertEquals(2, version.intValue()); + assertTrue(serializer.isProcessGroupVersion(version)); + + final VersionedProcessGroup processGroup = serializer.deserializeProcessGroup(version, is); + assertNotNull(processGroup); + } + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/serialization/jaxb/TestJAXBVersionedProcessGroupSerializer.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/serialization/jaxb/TestJAXBVersionedProcessGroupSerializer.java new file mode 100644 index 0000000000..916e0531d1 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/serialization/jaxb/TestJAXBVersionedProcessGroupSerializer.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.serialization.jaxb; + +import org.apache.nifi.registry.flow.VersionedProcessGroup; +import org.apache.nifi.registry.flow.VersionedProcessor; +import org.apache.nifi.registry.serialization.SerializationException; +import org.apache.nifi.registry.serialization.VersionedSerializer; +import org.junit.Assert; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; + +public class TestJAXBVersionedProcessGroupSerializer { + + @Test + public void testSerializeDeserializeFlowSnapshot() throws SerializationException { + final VersionedSerializer serializer = new JAXBVersionedProcessGroupSerializer(); + + final VersionedProcessGroup processGroup1 = new VersionedProcessGroup(); + processGroup1.setIdentifier("pg1"); + processGroup1.setName("My Process Group"); + + final VersionedProcessor processor1 = new VersionedProcessor(); + processor1.setIdentifier("processor1"); + processor1.setName("My Processor 1"); + + // make sure nested objects are serialized/deserialized + processGroup1.getProcessors().add(processor1); + + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + serializer.serialize(1, processGroup1, out); + + final String snapshotStr = new String(out.toByteArray(), StandardCharsets.UTF_8); + //System.out.println(snapshotStr); + + final ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + in.mark(1024); + final int version = serializer.readDataModelVersion(in); + + Assert.assertEquals(1, version); + + in.reset(); + final VersionedProcessGroup deserializedProcessGroup1 = serializer.deserialize(in); + + Assert.assertEquals(processGroup1.getIdentifier(), deserializedProcessGroup1.getIdentifier()); + Assert.assertEquals(processGroup1.getName(), deserializedProcessGroup1.getName()); + + Assert.assertEquals(1, deserializedProcessGroup1.getProcessors().size()); + + final VersionedProcessor deserializedProcessor1 = deserializedProcessGroup1.getProcessors().iterator().next(); + Assert.assertEquals(processor1.getIdentifier(), deserializedProcessor1.getIdentifier()); + Assert.assertEquals(processor1.getName(), deserializedProcessor1.getName()); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/TestRegistryService.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/TestRegistryService.java new file mode 100644 index 0000000000..dd091701ee --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/TestRegistryService.java @@ -0,0 +1,1364 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.service; + +import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.db.entity.BucketEntity; +import org.apache.nifi.registry.db.entity.FlowEntity; +import org.apache.nifi.registry.db.entity.FlowSnapshotEntity; +import org.apache.nifi.registry.diff.ComponentDifference; +import org.apache.nifi.registry.diff.ComponentDifferenceGroup; +import org.apache.nifi.registry.diff.VersionedFlowDifference; +import org.apache.nifi.registry.exception.ResourceNotFoundException; +import org.apache.nifi.registry.extension.BundlePersistenceProvider; +import org.apache.nifi.registry.flow.FlowPersistenceProvider; +import org.apache.nifi.registry.flow.VersionedFlow; +import org.apache.nifi.registry.flow.VersionedFlowSnapshot; +import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata; +import org.apache.nifi.registry.flow.VersionedProcessGroup; +import org.apache.nifi.registry.flow.VersionedProcessor; +import org.apache.nifi.registry.serialization.FlowContent; +import org.apache.nifi.registry.serialization.FlowContentSerializer; +import org.apache.nifi.registry.service.alias.RegistryUrlAliasService; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import javax.validation.ConstraintViolationException; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.SortedSet; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class TestRegistryService { + + private MetadataService metadataService; + private FlowPersistenceProvider flowPersistenceProvider; + private BundlePersistenceProvider bundlePersistenceProvider; + private FlowContentSerializer flowContentSerializer; + private Validator validator; + private RegistryUrlAliasService registryUrlAliasService; + + private RegistryService registryService; + + @Before + public void setup() { + metadataService = mock(MetadataService.class); + flowPersistenceProvider = mock(FlowPersistenceProvider.class); + bundlePersistenceProvider = mock(BundlePersistenceProvider.class); + flowContentSerializer = mock(FlowContentSerializer.class); + registryUrlAliasService = mock(RegistryUrlAliasService.class); + + final ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory(); + validator = validatorFactory.getValidator(); + + registryService = new RegistryService(metadataService, flowPersistenceProvider, bundlePersistenceProvider, + flowContentSerializer, validator, registryUrlAliasService); + } + + // ---------------------- Test Bucket methods --------------------------------------------- + + @Test + public void testCreateBucketValid() { + final Bucket bucket = new Bucket(); + bucket.setIdentifier("1"); + bucket.setName("My Bucket"); + bucket.setDescription("This is my bucket."); + + when(metadataService.getBucketsByName(bucket.getName())).thenReturn(Collections.emptyList()); + + doAnswer(createBucketAnswer()).when(metadataService).createBucket(any(BucketEntity.class)); + + final Bucket createdBucket = registryService.createBucket(bucket); + assertNotNull(createdBucket); + assertNotNull(createdBucket.getIdentifier()); + assertNotNull(createdBucket.getCreatedTimestamp()); + + assertEquals(bucket.getIdentifier(), createdBucket.getIdentifier()); + assertEquals(bucket.getName(), createdBucket.getName()); + assertEquals(bucket.getDescription(), createdBucket.getDescription()); + } + + @Test(expected = IllegalStateException.class) + public void testCreateBucketWithSameName() { + final Bucket bucket = new Bucket(); + bucket.setIdentifier("b2"); + bucket.setName("My Bucket"); + bucket.setDescription("This is my bucket."); + + final BucketEntity existingBucket = new BucketEntity(); + existingBucket.setId("b1"); + existingBucket.setName("My Bucket"); + existingBucket.setCreated(new Date()); + + when(metadataService.getBucketsByName(bucket.getName())).thenReturn(Collections.singletonList(existingBucket)); + + // should throw exception since a bucket with the same name exists + registryService.createBucket(bucket); + } + + @Test(expected = ConstraintViolationException.class) + public void testCreateBucketWithMissingName() { + final Bucket bucket = new Bucket(); + when(metadataService.getBucketsByName(bucket.getName())).thenReturn(Collections.emptyList()); + registryService.createBucket(bucket); + } + + @Test + public void testGetExistingBucket() { + final BucketEntity existingBucket = new BucketEntity(); + existingBucket.setId("b1"); + existingBucket.setName("My Bucket"); + existingBucket.setDescription("This is my bucket"); + existingBucket.setCreated(new Date()); + + when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket); + + final Bucket bucket = registryService.getBucket(existingBucket.getId()); + assertNotNull(bucket); + assertEquals(existingBucket.getId(), bucket.getIdentifier()); + assertEquals(existingBucket.getName(), bucket.getName()); + assertEquals(existingBucket.getDescription(), bucket.getDescription()); + assertEquals(existingBucket.getCreated().getTime(), bucket.getCreatedTimestamp()); + } + + @Test(expected = ResourceNotFoundException.class) + public void testGetBucketDoesNotExist() { + when(metadataService.getBucketById(any(String.class))).thenReturn(null); + registryService.getBucket("does-not-exist"); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateBucketWithoutId() { + final Bucket bucket = new Bucket(); + bucket.setName("My Bucket"); + bucket.setDescription("This is my bucket."); + registryService.updateBucket(bucket); + } + + @Test(expected = ResourceNotFoundException.class) + public void testUpdateBucketDoesNotExist() { + final Bucket bucket = new Bucket(); + bucket.setIdentifier("b1"); + bucket.setName("My Bucket"); + bucket.setDescription("This is my bucket."); + registryService.updateBucket(bucket); + + when(metadataService.getBucketById(any(String.class))).thenReturn(null); + registryService.updateBucket(bucket); + } + + @Test(expected = IllegalStateException.class) + public void testUpdateBucketWithSameNameAsExistingBucket() { + final BucketEntity bucketToUpdate = new BucketEntity(); + bucketToUpdate.setId("b1"); + bucketToUpdate.setName("My Bucket"); + bucketToUpdate.setDescription("This is my bucket"); + bucketToUpdate.setCreated(new Date()); + + when(metadataService.getBucketById(bucketToUpdate.getId())).thenReturn(bucketToUpdate); + + final BucketEntity otherBucket = new BucketEntity(); + otherBucket.setId("b2"); + otherBucket.setName("My Bucket #2"); + otherBucket.setDescription("This is my bucket"); + otherBucket.setCreated(new Date()); + + when(metadataService.getBucketsByName(otherBucket.getName())).thenReturn(Collections.singletonList(otherBucket)); + + // should fail because other bucket has the same name + final Bucket updatedBucket = new Bucket(); + updatedBucket.setIdentifier(bucketToUpdate.getId()); + updatedBucket.setName("My Bucket #2"); + updatedBucket.setDescription(bucketToUpdate.getDescription()); + + registryService.updateBucket(updatedBucket); + } + + @Test + public void testUpdateBucket() { + final BucketEntity bucketToUpdate = new BucketEntity(); + bucketToUpdate.setId("b1"); + bucketToUpdate.setName("My Bucket"); + bucketToUpdate.setDescription("This is my bucket"); + bucketToUpdate.setCreated(new Date()); + + when(metadataService.getBucketById(bucketToUpdate.getId())).thenReturn(bucketToUpdate); + + doAnswer(updateBucketAnswer()).when(metadataService).updateBucket(any(BucketEntity.class)); + + final Bucket updatedBucket = new Bucket(); + updatedBucket.setIdentifier(bucketToUpdate.getId()); + updatedBucket.setName("Updated Name"); + updatedBucket.setDescription("Updated Description"); + + final Bucket result = registryService.updateBucket(updatedBucket); + assertNotNull(result); + assertEquals(updatedBucket.getName(), result.getName()); + assertEquals(updatedBucket.getDescription(), result.getDescription()); + } + + @Test + public void testUpdateBucketPartial() { + final BucketEntity bucketToUpdate = new BucketEntity(); + bucketToUpdate.setId("b1"); + bucketToUpdate.setName("My Bucket"); + bucketToUpdate.setDescription("This is my bucket"); + bucketToUpdate.setCreated(new Date()); + + when(metadataService.getBucketById(bucketToUpdate.getId())).thenReturn(bucketToUpdate); + + doAnswer(updateBucketAnswer()).when(metadataService).updateBucket(any(BucketEntity.class)); + + final Bucket updatedBucket = new Bucket(); + updatedBucket.setIdentifier(bucketToUpdate.getId()); + updatedBucket.setName("Updated Name"); + updatedBucket.setDescription(null); + + // name should be updated but description should not be changed + final Bucket result = registryService.updateBucket(updatedBucket); + assertNotNull(result); + assertEquals(updatedBucket.getName(), result.getName()); + assertEquals(bucketToUpdate.getDescription(), result.getDescription()); + } + + @Test(expected = ResourceNotFoundException.class) + public void testDeleteBucketDoesNotExist() { + final String bucketId = "b1"; + when(metadataService.getBucketById(bucketId)).thenReturn(null); + registryService.deleteBucket(bucketId); + } + + @Test + public void testDeleteBucketWithFlows() { + final BucketEntity bucketToDelete = new BucketEntity(); + bucketToDelete.setId("b1"); + bucketToDelete.setName("My Bucket"); + bucketToDelete.setDescription("This is my bucket"); + bucketToDelete.setCreated(new Date()); + + final FlowEntity flowToDelete = new FlowEntity(); + flowToDelete.setId("flow1"); + flowToDelete.setName("Flow 1"); + flowToDelete.setDescription("This is flow 1"); + flowToDelete.setCreated(new Date()); + + final List flows = new ArrayList<>(); + flows.add(flowToDelete); + + when(metadataService.getBucketById(bucketToDelete.getId())).thenReturn(bucketToDelete); + + when(metadataService.getFlowsByBucket(bucketToDelete.getId())).thenReturn(flows); + + final Bucket deletedBucket = registryService.deleteBucket(bucketToDelete.getId()); + assertNotNull(deletedBucket); + assertEquals(bucketToDelete.getId(), deletedBucket.getIdentifier()); + + verify(flowPersistenceProvider, times(1)) + .deleteAllFlowContent(eq(bucketToDelete.getId()), eq(flowToDelete.getId())); + } + + // ---------------------- Test VersionedFlow methods --------------------------------------------- + + @Test(expected = ConstraintViolationException.class) + public void testCreateFlowInvalid() { + final VersionedFlow versionedFlow = new VersionedFlow(); + registryService.createFlow("b1", versionedFlow); + } + + @Test(expected = ResourceNotFoundException.class) + public void testCreateFlowBucketDoesNotExist() { + + when(metadataService.getBucketById(any(String.class))).thenReturn(null); + + final VersionedFlow versionedFlow = new VersionedFlow(); + versionedFlow.setIdentifier("f1"); + versionedFlow.setName("My Flow"); + versionedFlow.setBucketIdentifier("b1"); + + registryService.createFlow(versionedFlow.getBucketIdentifier(), versionedFlow); + } + + @Test(expected = IllegalStateException.class) + public void testCreateFlowWithSameName() { + final BucketEntity existingBucket = new BucketEntity(); + existingBucket.setId("b1"); + existingBucket.setName("My Bucket"); + existingBucket.setDescription("This is my bucket"); + existingBucket.setCreated(new Date()); + + when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket); + + // setup a flow with the same name that already exists + + final FlowEntity flowWithSameName = new FlowEntity(); + flowWithSameName.setId("flow1"); + flowWithSameName.setName("Flow 1"); + flowWithSameName.setDescription("This is flow 1"); + flowWithSameName.setCreated(new Date()); + flowWithSameName.setModified(new Date()); + + when(metadataService.getFlowsByName(existingBucket.getId(), flowWithSameName.getName())).thenReturn(Collections.singletonList(flowWithSameName)); + + final VersionedFlow versionedFlow = new VersionedFlow(); + versionedFlow.setIdentifier("flow2"); + versionedFlow.setName(flowWithSameName.getName()); + versionedFlow.setBucketIdentifier("b1"); + + registryService.createFlow(versionedFlow.getBucketIdentifier(), versionedFlow); + } + + @Test + public void testCreateFlowValid() { + final BucketEntity existingBucket = new BucketEntity(); + existingBucket.setId("b1"); + existingBucket.setName("My Bucket"); + existingBucket.setDescription("This is my bucket"); + existingBucket.setCreated(new Date()); + + when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket); + + final VersionedFlow versionedFlow = new VersionedFlow(); + versionedFlow.setIdentifier("f1"); + versionedFlow.setName("My Flow"); + versionedFlow.setBucketIdentifier("b1"); + + doAnswer(createFlowAnswer()).when(metadataService).createFlow(any(FlowEntity.class)); + + final VersionedFlow createdFlow = registryService.createFlow(versionedFlow.getBucketIdentifier(), versionedFlow); + assertNotNull(createdFlow); + assertNotNull(createdFlow.getIdentifier()); + assertTrue(createdFlow.getCreatedTimestamp() > 0); + assertTrue(createdFlow.getModifiedTimestamp() > 0); + assertEquals(versionedFlow.getIdentifier(), createdFlow.getIdentifier()); + assertEquals(versionedFlow.getName(), createdFlow.getName()); + assertEquals(versionedFlow.getBucketIdentifier(), createdFlow.getBucketIdentifier()); + assertEquals(versionedFlow.getDescription(), createdFlow.getDescription()); + } + + @Test(expected = ResourceNotFoundException.class) + public void testGetFlowDoesNotExist() { + when(metadataService.getFlowById(any(String.class))).thenReturn(null); + registryService.getFlow("bucket1","flow1"); + } + + @Test(expected = ResourceNotFoundException.class) + public void testGetFlowDirectDoesNotExist() { + when(metadataService.getFlowById(any(String.class))).thenReturn(null); + registryService.getFlow("flow1"); + } + + @Test + public void testGetFlowExists() { + final BucketEntity existingBucket = new BucketEntity(); + existingBucket.setId("b1"); + existingBucket.setName("My Bucket"); + existingBucket.setDescription("This is my bucket"); + existingBucket.setCreated(new Date()); + + when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket); + + final FlowEntity flowEntity = new FlowEntity(); + flowEntity.setId("flow1"); + flowEntity.setName("My Flow"); + flowEntity.setDescription("This is my flow."); + flowEntity.setCreated(new Date()); + flowEntity.setModified(new Date()); + flowEntity.setBucketId(existingBucket.getId()); + + when(metadataService.getFlowByIdWithSnapshotCounts(flowEntity.getId())).thenReturn(flowEntity); + + final VersionedFlow versionedFlow = registryService.getFlow(existingBucket.getId(), flowEntity.getId()); + assertNotNull(versionedFlow); + assertEquals(flowEntity.getId(), versionedFlow.getIdentifier()); + assertEquals(flowEntity.getName(), versionedFlow.getName()); + assertEquals(flowEntity.getDescription(), versionedFlow.getDescription()); + assertEquals(flowEntity.getBucketId(), versionedFlow.getBucketIdentifier()); + assertEquals(existingBucket.getName(), versionedFlow.getBucketName()); + assertEquals(flowEntity.getCreated().getTime(), versionedFlow.getCreatedTimestamp()); + assertEquals(flowEntity.getModified().getTime(), versionedFlow.getModifiedTimestamp()); + } + + @Test + public void testGetFlowDirectExists() { + final BucketEntity existingBucket = new BucketEntity(); + existingBucket.setId("b1"); + existingBucket.setName("My Bucket"); + existingBucket.setDescription("This is my bucket"); + existingBucket.setCreated(new Date()); + + final FlowEntity flowEntity = new FlowEntity(); + flowEntity.setId("flow1"); + flowEntity.setName("My Flow"); + flowEntity.setDescription("This is my flow."); + flowEntity.setCreated(new Date()); + flowEntity.setModified(new Date()); + flowEntity.setBucketId(existingBucket.getId()); + + when(metadataService.getFlowByIdWithSnapshotCounts(flowEntity.getId())).thenReturn(flowEntity); + when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket); + + final VersionedFlow versionedFlow = registryService.getFlow(flowEntity.getId()); + assertNotNull(versionedFlow); + assertEquals(flowEntity.getId(), versionedFlow.getIdentifier()); + assertEquals(flowEntity.getName(), versionedFlow.getName()); + assertEquals(flowEntity.getDescription(), versionedFlow.getDescription()); + assertEquals(flowEntity.getBucketId(), versionedFlow.getBucketIdentifier()); + assertEquals(existingBucket.getName(), versionedFlow.getBucketName()); + assertEquals(flowEntity.getCreated().getTime(), versionedFlow.getCreatedTimestamp()); + assertEquals(flowEntity.getModified().getTime(), versionedFlow.getModifiedTimestamp()); + } + + @Test(expected = ResourceNotFoundException.class) + public void testGetFlowsByBucketDoesNotExist() { + when(metadataService.getBucketById(any(String.class))).thenReturn(null); + registryService.getFlows("b1"); + } + + @Test + public void testGetFlowsByBucketExists() { + final BucketEntity existingBucket = new BucketEntity(); + existingBucket.setId("b1"); + existingBucket.setName("My Bucket"); + existingBucket.setDescription("This is my bucket"); + existingBucket.setCreated(new Date()); + + when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket); + + final FlowEntity flowEntity1 = new FlowEntity(); + flowEntity1.setId("flow1"); + flowEntity1.setName("My Flow"); + flowEntity1.setDescription("This is my flow."); + flowEntity1.setCreated(new Date()); + flowEntity1.setModified(new Date()); + flowEntity1.setBucketId(existingBucket.getId()); + + final FlowEntity flowEntity2 = new FlowEntity(); + flowEntity2.setId("flow2"); + flowEntity2.setName("My Flow 2"); + flowEntity2.setDescription("This is my flow 2."); + flowEntity2.setCreated(new Date()); + flowEntity2.setModified(new Date()); + flowEntity2.setBucketId(existingBucket.getId()); + + final List flows = new ArrayList<>(); + flows.add(flowEntity1); + flows.add(flowEntity2); + + when(metadataService.getFlowsByBucket(eq(existingBucket.getId()))).thenReturn(flows); + + final List allFlows = registryService.getFlows(existingBucket.getId()); + assertNotNull(allFlows); + assertEquals(2, allFlows.size()); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateFlowWithoutId() { + final VersionedFlow versionedFlow = new VersionedFlow(); + registryService.updateFlow(versionedFlow); + } + + @Test(expected = ResourceNotFoundException.class) + public void testUpdateFlowDoesNotExist() { + final VersionedFlow versionedFlow = new VersionedFlow(); + versionedFlow.setBucketIdentifier("b1"); + versionedFlow.setIdentifier("flow1"); + + when(metadataService.getFlowById(versionedFlow.getIdentifier())).thenReturn(null); + + registryService.updateFlow(versionedFlow); + } + + @Test(expected = IllegalStateException.class) + public void testUpdateFlowWithSameNameAsExistingFlow() { + final BucketEntity existingBucket = new BucketEntity(); + existingBucket.setId("b1"); + existingBucket.setName("My Bucket"); + existingBucket.setDescription("This is my bucket"); + existingBucket.setCreated(new Date()); + + when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket); + + final FlowEntity flowToUpdate = new FlowEntity(); + flowToUpdate.setId("flow1"); + flowToUpdate.setName("My Flow"); + flowToUpdate.setDescription("This is my flow."); + flowToUpdate.setCreated(new Date()); + flowToUpdate.setModified(new Date()); + flowToUpdate.setBucketId(existingBucket.getId()); + + when(metadataService.getFlowByIdWithSnapshotCounts(flowToUpdate.getId())).thenReturn(flowToUpdate); + + final FlowEntity otherFlow = new FlowEntity(); + otherFlow.setId("flow2"); + otherFlow.setName("My Flow 2"); + otherFlow.setDescription("This is my flow 2."); + otherFlow.setCreated(new Date()); + otherFlow.setModified(new Date()); + otherFlow.setBucketId(existingBucket.getId()); + + when(metadataService.getFlowsByName(existingBucket.getId(), otherFlow.getName())).thenReturn(Collections.singletonList(otherFlow)); + + final VersionedFlow versionedFlow = new VersionedFlow(); + versionedFlow.setIdentifier(flowToUpdate.getId()); + versionedFlow.setBucketIdentifier(existingBucket.getId()); + versionedFlow.setName(otherFlow.getName()); + + registryService.updateFlow(versionedFlow); + } + + @Test + public void testUpdateFlow() throws InterruptedException { + final BucketEntity existingBucket = new BucketEntity(); + existingBucket.setId("b1"); + existingBucket.setName("My Bucket"); + existingBucket.setDescription("This is my bucket"); + existingBucket.setCreated(new Date()); + + final FlowEntity flowToUpdate = new FlowEntity(); + flowToUpdate.setId("flow1"); + flowToUpdate.setName("My Flow"); + flowToUpdate.setDescription("This is my flow."); + flowToUpdate.setCreated(new Date()); + flowToUpdate.setModified(new Date()); + flowToUpdate.setBucketId(existingBucket.getId()); + + when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket); + when(metadataService.getFlowByIdWithSnapshotCounts(flowToUpdate.getId())).thenReturn(flowToUpdate); + when(metadataService.getFlowsByName(flowToUpdate.getName())).thenReturn(Collections.singletonList(flowToUpdate)); + + doAnswer(updateFlowAnswer()).when(metadataService).updateFlow(any(FlowEntity.class)); + + final VersionedFlow versionedFlow = new VersionedFlow(); + versionedFlow.setBucketIdentifier(flowToUpdate.getBucketId()); + versionedFlow.setIdentifier(flowToUpdate.getId()); + versionedFlow.setName("New Flow Name"); + versionedFlow.setDescription("This is a new description"); + + Thread.sleep(10); + + final VersionedFlow updatedFlow = registryService.updateFlow(versionedFlow); + assertNotNull(updatedFlow); + assertEquals(versionedFlow.getIdentifier(), updatedFlow.getIdentifier()); + + // name and description should be updated + assertEquals(versionedFlow.getName(), updatedFlow.getName()); + assertEquals(versionedFlow.getDescription(), updatedFlow.getDescription()); + + // other fields should not be updated + assertEquals(flowToUpdate.getBucketId(), updatedFlow.getBucketIdentifier()); + assertEquals(flowToUpdate.getCreated().getTime(), updatedFlow.getCreatedTimestamp()); + } + + @Test(expected = ResourceNotFoundException.class) + public void testDeleteFlowDoesNotExist() { + when(metadataService.getFlowById(any(String.class))).thenReturn(null); + registryService.deleteFlow("b1", "flow1"); + } + + @Test + public void testDeleteFlowWithSnapshots() { + final BucketEntity existingBucket = new BucketEntity(); + existingBucket.setId("b1"); + existingBucket.setName("My Bucket"); + existingBucket.setDescription("This is my bucket"); + existingBucket.setCreated(new Date()); + + final FlowEntity flowToDelete = new FlowEntity(); + flowToDelete.setId("flow1"); + flowToDelete.setName("My Flow"); + flowToDelete.setDescription("This is my flow."); + flowToDelete.setCreated(new Date()); + flowToDelete.setModified(new Date()); + flowToDelete.setBucketId(existingBucket.getId()); + + when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket); + when(metadataService.getFlowById(flowToDelete.getId())).thenReturn(flowToDelete); + when(metadataService.getFlowsByName(flowToDelete.getName())).thenReturn(Collections.singletonList(flowToDelete)); + + final VersionedFlow deletedFlow = registryService.deleteFlow(existingBucket.getId(), flowToDelete.getId()); + assertNotNull(deletedFlow); + assertEquals(flowToDelete.getId(), deletedFlow.getIdentifier()); + + verify(flowPersistenceProvider, times(1)) + .deleteAllFlowContent(flowToDelete.getBucketId(), flowToDelete.getId()); + + verify(metadataService, times(1)).deleteFlow(flowToDelete); + } + + // ---------------------- Test VersionedFlowSnapshot methods --------------------------------------------- + + private VersionedFlowSnapshot createSnapshot() { + final VersionedFlowSnapshotMetadata snapshotMetadata = new VersionedFlowSnapshotMetadata(); + snapshotMetadata.setFlowIdentifier("flow1"); + snapshotMetadata.setVersion(1); + snapshotMetadata.setComments("This is the first snapshot"); + snapshotMetadata.setBucketIdentifier("b1"); + snapshotMetadata.setAuthor("user1"); + + final VersionedProcessGroup processGroup = new VersionedProcessGroup(); + processGroup.setIdentifier("pg1"); + processGroup.setName("My Process Group"); + + final VersionedFlowSnapshot snapshot = new VersionedFlowSnapshot(); + snapshot.setSnapshotMetadata(snapshotMetadata); + snapshot.setFlowContents(processGroup); + + return snapshot; + } + + @Test(expected = ConstraintViolationException.class) + public void testCreateSnapshotInvalidMetadata() { + final VersionedFlowSnapshot snapshot = createSnapshot(); + snapshot.getSnapshotMetadata().setFlowIdentifier(null); + registryService.createFlowSnapshot(snapshot); + } + + @Test(expected = ConstraintViolationException.class) + public void testCreateSnapshotInvalidFlowContents() { + final VersionedFlowSnapshot snapshot = createSnapshot(); + snapshot.setFlowContents(null); + registryService.createFlowSnapshot(snapshot); + } + + @Test(expected = ConstraintViolationException.class) + public void testCreateSnapshotNullMetadata() { + final VersionedFlowSnapshot snapshot = createSnapshot(); + snapshot.setSnapshotMetadata(null); + registryService.createFlowSnapshot(snapshot); + } + + @Test(expected = ConstraintViolationException.class) + public void testCreateSnapshotNullFlowContents() { + final VersionedFlowSnapshot snapshot = createSnapshot(); + snapshot.setFlowContents(null); + registryService.createFlowSnapshot(snapshot); + } + + @Test(expected = ResourceNotFoundException.class) + public void testCreateSnapshotBucketDoesNotExist() { + when(metadataService.getBucketById(any(String.class))).thenReturn(null); + + final VersionedFlowSnapshot snapshot = createSnapshot(); + registryService.createFlowSnapshot(snapshot); + } + + @Test(expected = ResourceNotFoundException.class) + public void testCreateSnapshotFlowDoesNotExist() { + final VersionedFlowSnapshot snapshot = createSnapshot(); + + final BucketEntity existingBucket = new BucketEntity(); + existingBucket.setId("b1"); + existingBucket.setName("My Bucket"); + existingBucket.setDescription("This is my bucket"); + existingBucket.setCreated(new Date()); + + when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket); + + when(metadataService.getFlowById(snapshot.getSnapshotMetadata().getFlowIdentifier())).thenReturn(null); + + registryService.createFlowSnapshot(snapshot); + } + + @Test(expected = IllegalStateException.class) + public void testCreateSnapshotVersionAlreadyExists() { + final VersionedFlowSnapshot snapshot = createSnapshot(); + + final BucketEntity existingBucket = new BucketEntity(); + existingBucket.setId("b1"); + existingBucket.setName("My Bucket"); + existingBucket.setDescription("This is my bucket"); + existingBucket.setCreated(new Date()); + + when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket); + + // return a flow with the existing snapshot when getFlowById is called + final FlowEntity existingFlow = new FlowEntity(); + existingFlow.setId("flow1"); + existingFlow.setName("My Flow"); + existingFlow.setDescription("This is my flow."); + existingFlow.setCreated(new Date()); + existingFlow.setModified(new Date()); + existingFlow.setBucketId(existingBucket.getId()); + + when(metadataService.getFlowById(existingFlow.getId())).thenReturn(existingFlow); + + // make a snapshot that has the same version as the one being created + final FlowSnapshotEntity existingSnapshot = new FlowSnapshotEntity(); + existingSnapshot.setFlowId(snapshot.getSnapshotMetadata().getFlowIdentifier()); + existingSnapshot.setVersion(snapshot.getSnapshotMetadata().getVersion()); + existingSnapshot.setComments("This is an existing snapshot"); + existingSnapshot.setCreated(new Date()); + existingSnapshot.setCreatedBy("test-user"); + + final List existingSnapshots = Arrays.asList(existingSnapshot); + when(metadataService.getSnapshots(existingFlow.getId())).thenReturn(existingSnapshots); + + registryService.createFlowSnapshot(snapshot); + } + + @Test(expected = IllegalStateException.class) + public void testCreateSnapshotVersionNotNextVersion() { + final VersionedFlowSnapshot snapshot = createSnapshot(); + + final BucketEntity existingBucket = new BucketEntity(); + existingBucket.setId("b1"); + existingBucket.setName("My Bucket"); + existingBucket.setDescription("This is my bucket"); + existingBucket.setCreated(new Date()); + + when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket); + + // return a flow with the existing snapshot when getFlowById is called + final FlowEntity existingFlow = new FlowEntity(); + existingFlow.setId("flow1"); + existingFlow.setName("My Flow"); + existingFlow.setDescription("This is my flow."); + existingFlow.setCreated(new Date()); + existingFlow.setModified(new Date()); + existingFlow.setBucketId(existingBucket.getId()); + + // make a snapshot that has the same version as the one being created + final FlowSnapshotEntity existingSnapshot = new FlowSnapshotEntity(); + existingSnapshot.setFlowId(snapshot.getSnapshotMetadata().getFlowIdentifier()); + existingSnapshot.setVersion(snapshot.getSnapshotMetadata().getVersion()); + existingSnapshot.setComments("This is an existing snapshot"); + existingSnapshot.setCreated(new Date()); + existingSnapshot.setCreatedBy("test-user"); + + when(metadataService.getFlowById(existingFlow.getId())).thenReturn(existingFlow); + + // set the version to something that is not the next one-up version + snapshot.getSnapshotMetadata().setVersion(100); + registryService.createFlowSnapshot(snapshot); + } + + @Test + public void testCreateFirstSnapshot() { + final VersionedFlowSnapshot snapshot = createSnapshot(); + + final BucketEntity existingBucket = new BucketEntity(); + existingBucket.setId("b1"); + existingBucket.setName("My Bucket"); + existingBucket.setDescription("This is my bucket"); + existingBucket.setCreated(new Date()); + + when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket); + + // return a flow with the existing snapshot when getFlowById is called + final FlowEntity existingFlow = new FlowEntity(); + existingFlow.setId("flow1"); + existingFlow.setName("My Flow"); + existingFlow.setDescription("This is my flow."); + existingFlow.setCreated(new Date()); + existingFlow.setModified(new Date()); + existingFlow.setBucketId(existingBucket.getId()); + + when(metadataService.getFlowById(existingFlow.getId())).thenReturn(existingFlow); + when(metadataService.getFlowByIdWithSnapshotCounts(existingFlow.getId())).thenReturn(existingFlow); + + final VersionedFlowSnapshot createdSnapshot = registryService.createFlowSnapshot(snapshot); + assertNotNull(createdSnapshot); + assertNotNull(createdSnapshot.getSnapshotMetadata()); + assertNotNull(createdSnapshot.getFlow()); + assertNotNull(createdSnapshot.getBucket()); + + verify(flowContentSerializer, times(1)).serializeFlowContent(any(FlowContent.class), any(OutputStream.class)); + verify(flowPersistenceProvider, times(1)).saveFlowContent(any(), any()); + verify(metadataService, times(1)).createFlowSnapshot(any(FlowSnapshotEntity.class)); + } + + @Test(expected = IllegalStateException.class) + public void testCreateFirstSnapshotWithBadVersion() { + final VersionedFlowSnapshot snapshot = createSnapshot(); + + final BucketEntity existingBucket = new BucketEntity(); + existingBucket.setId("b1"); + existingBucket.setName("My Bucket"); + existingBucket.setDescription("This is my bucket"); + existingBucket.setCreated(new Date()); + + when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket); + + // return a flow with the existing snapshot when getFlowById is called + final FlowEntity existingFlow = new FlowEntity(); + existingFlow.setId("flow1"); + existingFlow.setName("My Flow"); + existingFlow.setDescription("This is my flow."); + existingFlow.setCreated(new Date()); + existingFlow.setModified(new Date()); + existingFlow.setBucketId(existingBucket.getId()); + + when(metadataService.getFlowById(existingFlow.getId())).thenReturn(existingFlow); + + // set the first version to something other than 1 + snapshot.getSnapshotMetadata().setVersion(100); + registryService.createFlowSnapshot(snapshot); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateFirstSnapshotWithZeroVersion() { + final VersionedFlowSnapshot snapshot = createSnapshot(); + + final BucketEntity existingBucket = new BucketEntity(); + existingBucket.setId("b1"); + existingBucket.setName("My Bucket"); + existingBucket.setDescription("This is my bucket"); + existingBucket.setCreated(new Date()); + + when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket); + + // return a flow with the existing snapshot when getFlowById is called + final FlowEntity existingFlow = new FlowEntity(); + existingFlow.setId("flow1"); + existingFlow.setName("My Flow"); + existingFlow.setDescription("This is my flow."); + existingFlow.setCreated(new Date()); + existingFlow.setModified(new Date()); + existingFlow.setBucketId(existingBucket.getId()); + + when(metadataService.getFlowById(existingFlow.getId())).thenReturn(existingFlow); + + // set the first version to something other than 1 + snapshot.getSnapshotMetadata().setVersion(0); + registryService.createFlowSnapshot(snapshot); + } + + @Test + public void testCreateFirstSnapshotWithLatestVersionWhenVersionExist() { + final VersionedFlowSnapshot snapshot = createSnapshot(); + + final BucketEntity existingBucket = new BucketEntity(); + existingBucket.setId("b1"); + existingBucket.setName("My Bucket"); + existingBucket.setDescription("This is my bucket"); + existingBucket.setCreated(new Date()); + + when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket); + + // return a flow with the existing snapshot when getFlowById is called + final FlowEntity existingFlow = new FlowEntity(); + existingFlow.setId("flow1"); + existingFlow.setName("My Flow"); + existingFlow.setDescription("This is my flow."); + existingFlow.setCreated(new Date()); + existingFlow.setModified(new Date()); + existingFlow.setBucketId(existingBucket.getId()); + + when(metadataService.getFlowById(existingFlow.getId())).thenReturn(existingFlow); + when(metadataService.getFlowByIdWithSnapshotCounts(existingFlow.getId())).thenReturn(existingFlow); + + // make a snapshot that has the same version as the one being created + final FlowSnapshotEntity existingSnapshot = new FlowSnapshotEntity(); + existingSnapshot.setFlowId(snapshot.getSnapshotMetadata().getFlowIdentifier()); + existingSnapshot.setVersion(snapshot.getSnapshotMetadata().getVersion()); + existingSnapshot.setComments("This is an existing snapshot"); + existingSnapshot.setCreated(new Date()); + existingSnapshot.setCreatedBy("test-user"); + + final List existingSnapshots = Arrays.asList(existingSnapshot); + when(metadataService.getSnapshots(existingFlow.getId())).thenReturn(existingSnapshots); + + // set the version to -1 to indicate that registry should make this the latest version + snapshot.getSnapshotMetadata().setVersion(-1); + registryService.createFlowSnapshot(snapshot); + + final VersionedFlowSnapshot createdSnapshot = registryService.createFlowSnapshot(snapshot); + assertNotNull(createdSnapshot); + assertNotNull(createdSnapshot.getSnapshotMetadata()); + assertEquals(2, createdSnapshot.getSnapshotMetadata().getVersion()); + } + + @Test + public void testCreateFirstSnapshotWithLatestVersionWhenNoVersionsExist() { + final VersionedFlowSnapshot snapshot = createSnapshot(); + + final BucketEntity existingBucket = new BucketEntity(); + existingBucket.setId("b1"); + existingBucket.setName("My Bucket"); + existingBucket.setDescription("This is my bucket"); + existingBucket.setCreated(new Date()); + + when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket); + + // return a flow with the existing snapshot when getFlowById is called + final FlowEntity existingFlow = new FlowEntity(); + existingFlow.setId("flow1"); + existingFlow.setName("My Flow"); + existingFlow.setDescription("This is my flow."); + existingFlow.setCreated(new Date()); + existingFlow.setModified(new Date()); + existingFlow.setBucketId(existingBucket.getId()); + + when(metadataService.getFlowById(existingFlow.getId())).thenReturn(existingFlow); + when(metadataService.getFlowByIdWithSnapshotCounts(existingFlow.getId())).thenReturn(existingFlow); + when(metadataService.getSnapshots(existingFlow.getId())).thenReturn(Collections.emptyList()); + + // set the version to -1 to indicate that registry should make this the latest version + snapshot.getSnapshotMetadata().setVersion(-1); + registryService.createFlowSnapshot(snapshot); + + final VersionedFlowSnapshot createdSnapshot = registryService.createFlowSnapshot(snapshot); + assertNotNull(createdSnapshot); + assertNotNull(createdSnapshot.getSnapshotMetadata()); + assertEquals(1, createdSnapshot.getSnapshotMetadata().getVersion()); + } + + @Test + public void testGetFlowSnapshots() { + final BucketEntity existingBucket = new BucketEntity(); + existingBucket.setId("b1"); + existingBucket.setName("My Bucket"); + existingBucket.setDescription("This is my bucket"); + existingBucket.setCreated(new Date()); + + when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket); + + // return a flow with the existing snapshot when getFlowById is called + final FlowEntity existingFlow = new FlowEntity(); + existingFlow.setId("flow1"); + existingFlow.setName("My Flow"); + existingFlow.setDescription("This is my flow."); + existingFlow.setCreated(new Date()); + existingFlow.setModified(new Date()); + existingFlow.setBucketId(existingBucket.getId()); + + when(metadataService.getFlowById(existingFlow.getId())).thenReturn(existingFlow); + + final FlowSnapshotEntity existingSnapshot1 = new FlowSnapshotEntity(); + existingSnapshot1.setVersion(1); + existingSnapshot1.setFlowId(existingFlow.getId()); + existingSnapshot1.setCreatedBy("user1"); + existingSnapshot1.setCreated(new Date()); + existingSnapshot1.setComments("This is snapshot 1"); + + final FlowSnapshotEntity existingSnapshot2 = new FlowSnapshotEntity(); + existingSnapshot2.setVersion(2); + existingSnapshot2.setFlowId(existingFlow.getId()); + existingSnapshot2.setCreatedBy("user2"); + existingSnapshot2.setCreated(new Date()); + existingSnapshot2.setComments("This is snapshot 2"); + + final List snapshots = new ArrayList<>(); + snapshots.add(existingSnapshot1); + snapshots.add(existingSnapshot2); + + when(metadataService.getSnapshots(existingFlow.getId())).thenReturn(snapshots); + + final SortedSet retrievedSnapshots = registryService.getFlowSnapshots(existingBucket.getId(), existingFlow.getId()); + assertNotNull(retrievedSnapshots); + assertEquals(2, retrievedSnapshots.size()); + // check that sorted set order is reversed + assertEquals(2, retrievedSnapshots.first().getVersion()); + assertEquals(1, retrievedSnapshots.last().getVersion()); + } + + @Test + public void testGetFlowSnapshotsWhenNoSnapshots() { + final BucketEntity existingBucket = new BucketEntity(); + existingBucket.setId("b1"); + existingBucket.setName("My Bucket"); + existingBucket.setDescription("This is my bucket"); + existingBucket.setCreated(new Date()); + + when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket); + + // return a flow with the existing snapshot when getFlowById is called + final FlowEntity existingFlow = new FlowEntity(); + existingFlow.setId("flow1"); + existingFlow.setName("My Flow"); + existingFlow.setDescription("This is my flow."); + existingFlow.setCreated(new Date()); + existingFlow.setModified(new Date()); + existingFlow.setBucketId(existingBucket.getId()); + + final Set snapshots = new HashSet<>(); + + when(metadataService.getFlowById(existingFlow.getId())).thenReturn(existingFlow); + + final SortedSet retrievedSnapshots = registryService.getFlowSnapshots(existingBucket.getId(), existingFlow.getId()); + assertNotNull(retrievedSnapshots); + assertEquals(0, retrievedSnapshots.size()); + } + + @Test + public void testGetLatestSnapshotMetadataWhenVersionsExist() { + final BucketEntity existingBucket = new BucketEntity(); + existingBucket.setId("b1"); + existingBucket.setName("My Bucket"); + existingBucket.setDescription("This is my bucket"); + existingBucket.setCreated(new Date()); + + when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket); + + // return a flow with the existing snapshot when getFlowById is called + final FlowEntity existingFlow = new FlowEntity(); + existingFlow.setId("flow1"); + existingFlow.setName("My Flow"); + existingFlow.setDescription("This is my flow."); + existingFlow.setCreated(new Date()); + existingFlow.setModified(new Date()); + existingFlow.setBucketId(existingBucket.getId()); + + when(metadataService.getFlowById(existingFlow.getId())).thenReturn(existingFlow); + + final FlowSnapshotEntity existingSnapshot1 = new FlowSnapshotEntity(); + existingSnapshot1.setVersion(1); + existingSnapshot1.setFlowId(existingFlow.getId()); + existingSnapshot1.setCreatedBy("user1"); + existingSnapshot1.setCreated(new Date()); + existingSnapshot1.setComments("This is snapshot 1"); + + when(metadataService.getLatestSnapshot(existingFlow.getId())).thenReturn(existingSnapshot1); + + VersionedFlowSnapshotMetadata latestMetadata = registryService.getLatestFlowSnapshotMetadata(existingBucket.getId(), existingFlow.getId()); + assertNotNull(latestMetadata); + assertEquals(1, latestMetadata.getVersion()); + } + + @Test + public void testGetLatestSnapshotMetadataWhenNoVersionsExist() { + final BucketEntity existingBucket = new BucketEntity(); + existingBucket.setId("b1"); + existingBucket.setName("My Bucket"); + existingBucket.setDescription("This is my bucket"); + existingBucket.setCreated(new Date()); + + when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket); + + // return a flow with the existing snapshot when getFlowById is called + final FlowEntity existingFlow = new FlowEntity(); + existingFlow.setId("flow1"); + existingFlow.setName("My Flow"); + existingFlow.setDescription("This is my flow."); + existingFlow.setCreated(new Date()); + existingFlow.setModified(new Date()); + existingFlow.setBucketId(existingBucket.getId()); + + when(metadataService.getFlowById(existingFlow.getId())).thenReturn(existingFlow); + + final FlowSnapshotEntity existingSnapshot1 = new FlowSnapshotEntity(); + existingSnapshot1.setVersion(1); + existingSnapshot1.setFlowId(existingFlow.getId()); + existingSnapshot1.setCreatedBy("user1"); + existingSnapshot1.setCreated(new Date()); + existingSnapshot1.setComments("This is snapshot 1"); + + when(metadataService.getLatestSnapshot(existingFlow.getId())).thenReturn(null); + + try { + registryService.getLatestFlowSnapshotMetadata(existingBucket.getId(), existingFlow.getId()); + Assert.fail("Should have thrown exception"); + } catch (ResourceNotFoundException e) { + assertEquals("The specified flow ID has no versions", e.getMessage()); + } + } + + @Test(expected = ResourceNotFoundException.class) + public void testGetSnapshotDoesNotExistInMetadataProvider() { + final String bucketId = "b1"; + final String flowId = "flow1"; + final Integer version = 1; + when(metadataService.getFlowSnapshot(flowId, version)).thenReturn(null); + registryService.getFlowSnapshot(bucketId, flowId, version); + } + + @Test(expected = IllegalStateException.class) + public void testGetSnapshotDoesNotExistInPersistenceProvider() { + final BucketEntity existingBucket = createBucketEntity("b1"); + final FlowEntity existingFlow = createFlowEntity(existingBucket.getId()); + final FlowSnapshotEntity existingSnapshot = createFlowSnapshotEntity(existingFlow.getId()); + + existingFlow.setSnapshotCount(10); + + when(metadataService.getBucketById(existingBucket.getId())) + .thenReturn(existingBucket); + + when(metadataService.getFlowByIdWithSnapshotCounts(existingFlow.getId())) + .thenReturn(existingFlow); + + when(metadataService.getFlowSnapshot(existingFlow.getId(), existingSnapshot.getVersion())) + .thenReturn(existingSnapshot); + + when(flowPersistenceProvider.getFlowContent( + existingBucket.getId(), + existingSnapshot.getFlowId(), + existingSnapshot.getVersion() + )).thenReturn(null); + + registryService.getFlowSnapshot(existingBucket.getId(), existingSnapshot.getFlowId(), existingSnapshot.getVersion()); + } + + @Test + public void testGetSnapshotExists() { + final BucketEntity existingBucket = createBucketEntity("b1"); + final FlowEntity existingFlow = createFlowEntity(existingBucket.getId()); + final FlowSnapshotEntity existingSnapshot = createFlowSnapshotEntity(existingFlow.getId()); + + existingFlow.setSnapshotCount(10); + + when(metadataService.getBucketById(existingBucket.getId())) + .thenReturn(existingBucket); + + when(metadataService.getFlowByIdWithSnapshotCounts(existingFlow.getId())) + .thenReturn(existingFlow); + + when(metadataService.getFlowSnapshot(existingFlow.getId(), existingSnapshot.getVersion())) + .thenReturn(existingSnapshot); + + // return a non-null, non-zero-length array so something gets passed to the serializer + when(flowPersistenceProvider.getFlowContent( + existingBucket.getId(), + existingSnapshot.getFlowId(), + existingSnapshot.getVersion() + )).thenReturn(new byte[10]); + + final FlowContent flowContent = new FlowContent(); + flowContent.setFlowSnapshot(createSnapshot()); + when(flowContentSerializer.readDataModelVersion(any(InputStream.class))).thenReturn(3); + when(flowContentSerializer.deserializeFlowContent(eq(3), any(InputStream.class))).thenReturn(flowContent); + + final VersionedFlowSnapshot returnedSnapshot = registryService.getFlowSnapshot( + existingBucket.getId(), existingSnapshot.getFlowId(), existingSnapshot.getVersion()); + assertNotNull(returnedSnapshot); + assertNotNull(returnedSnapshot.getSnapshotMetadata()); + + final VersionedFlowSnapshotMetadata snapshotMetadata = returnedSnapshot.getSnapshotMetadata(); + assertEquals(existingSnapshot.getVersion().intValue(), snapshotMetadata.getVersion()); + assertEquals(existingBucket.getId(), snapshotMetadata.getBucketIdentifier()); + assertEquals(existingSnapshot.getFlowId(), snapshotMetadata.getFlowIdentifier()); + assertEquals(existingSnapshot.getCreated(), new Date(snapshotMetadata.getTimestamp())); + assertEquals(existingSnapshot.getCreatedBy(), snapshotMetadata.getAuthor()); + assertEquals(existingSnapshot.getComments(), snapshotMetadata.getComments()); + + final VersionedFlow versionedFlow = returnedSnapshot.getFlow(); + assertNotNull(versionedFlow); + assertNotNull(versionedFlow.getVersionCount()); + assertTrue(versionedFlow.getVersionCount() > 0); + + final Bucket bucket = returnedSnapshot.getBucket(); + assertNotNull(bucket); + } + + @Test(expected = ResourceNotFoundException.class) + public void testDeleteSnapshotDoesNotExist() { + final String bucketId = "b1"; + final String flowId = "flow1"; + final Integer version = 1; + when(metadataService.getFlowSnapshot(flowId, version)).thenReturn(null); + registryService.deleteFlowSnapshot(bucketId, flowId, version); + } + + @Test + public void testDeleteSnapshotExists() { + final BucketEntity existingBucket = createBucketEntity("b1"); + final FlowEntity existingFlow = createFlowEntity(existingBucket.getId()); + final FlowSnapshotEntity existingSnapshot = createFlowSnapshotEntity(existingFlow.getId()); + + when(metadataService.getBucketById(existingBucket.getId())) + .thenReturn(existingBucket); + + when(metadataService.getFlowById(existingFlow.getId())) + .thenReturn(existingFlow); + + when(metadataService.getFlowSnapshot(existingSnapshot.getFlowId(), existingSnapshot.getVersion())) + .thenReturn(existingSnapshot); + + final VersionedFlowSnapshotMetadata deletedSnapshot = registryService.deleteFlowSnapshot( + existingBucket.getId(), existingSnapshot.getFlowId(), existingSnapshot.getVersion()); + + assertNotNull(deletedSnapshot); + assertEquals(existingSnapshot.getFlowId(), deletedSnapshot.getFlowIdentifier()); + + verify(flowPersistenceProvider, times(1)).deleteFlowContent( + existingBucket.getId(), + existingSnapshot.getFlowId(), + existingSnapshot.getVersion() + ); + + verify(metadataService, times(1)).deleteFlowSnapshot(existingSnapshot); + } + + private FlowSnapshotEntity createFlowSnapshotEntity(final String flowId) { + final FlowSnapshotEntity existingSnapshot = new FlowSnapshotEntity(); + existingSnapshot.setVersion(1); + existingSnapshot.setFlowId(flowId); + existingSnapshot.setComments("This is an existing snapshot"); + existingSnapshot.setCreated(new Date()); + existingSnapshot.setCreatedBy("test-user"); + return existingSnapshot; + } + + private FlowEntity createFlowEntity(final String bucketId) { + final FlowEntity existingFlow = new FlowEntity(); + existingFlow.setId("flow1"); + existingFlow.setName("My Flow"); + existingFlow.setDescription("This is my flow."); + existingFlow.setCreated(new Date()); + existingFlow.setModified(new Date()); + existingFlow.setBucketId(bucketId); + return existingFlow; + } + + private BucketEntity createBucketEntity(final String bucketId) { + final BucketEntity existingBucket = new BucketEntity(); + existingBucket.setId(bucketId); + existingBucket.setName("My Bucket"); + existingBucket.setDescription("This is my bucket"); + existingBucket.setCreated(new Date()); + return existingBucket; + } + + // -----------------Test Flow Diff Service Method--------------------- + @Test + public void testGetDiffReturnsRemovedComponentChanges() { + when(flowPersistenceProvider.getFlowContent( + anyString(), anyString(), anyInt() + )).thenReturn(new byte[10], new byte[10]); + + final VersionedProcessGroup pgA = createVersionedProcessGroupA(); + final VersionedProcessGroup pgB = createVersionedProcessGroupB(); + when(flowContentSerializer.readDataModelVersion(any(InputStream.class))).thenReturn(2); + when(flowContentSerializer.isProcessGroupVersion(eq(2))).thenReturn(true); + when(flowContentSerializer.deserializeProcessGroup(eq(2),any())).thenReturn(pgA, pgB); + + final VersionedFlowDifference diff = registryService.getFlowDiff( + "bucketIdentifier", "flowIdentifier", 1, 2); + + assertNotNull(diff); + Optional removedComponent = diff.getComponentDifferenceGroups().stream() + .filter(p->p.getComponentId().equals("ID-pg1")).findFirst(); + + assertTrue(removedComponent.isPresent()); + assertTrue(removedComponent.get().getDifferences().iterator().next().getDifferenceType().equals("COMPONENT_REMOVED")); + } + + @Test + public void testGetDiffReturnsChangesInChronologicalOrder() { + when(flowPersistenceProvider.getFlowContent( + anyString(), anyString(), anyInt() + )).thenReturn(new byte[10], new byte[10]); + + final VersionedProcessGroup pgA = createVersionedProcessGroupA(); + final VersionedProcessGroup pgB = createVersionedProcessGroupB(); + when(flowContentSerializer.readDataModelVersion(any(InputStream.class))).thenReturn(2); + when(flowContentSerializer.isProcessGroupVersion(eq(2))).thenReturn(true); + when(flowContentSerializer.deserializeProcessGroup(eq(2),any())).thenReturn(pgA, pgB); + + // getFlowDiff orders the changes in ascending order of version number regardless of param order + final VersionedFlowDifference diff = registryService.getFlowDiff( + "bucketIdentifier", "flowIdentifier", 2,1); + + assertNotNull(diff); + Optional nameChangedComponent = diff.getComponentDifferenceGroups().stream() + .filter(p->p.getComponentId().equals("ProcessorFirstV1")).findFirst(); + + assertTrue(nameChangedComponent.isPresent()); + + ComponentDifference nameChangeDifference = nameChangedComponent.get().getDifferences().stream() + .filter(d-> d.getDifferenceType().equals("NAME_CHANGED")).findFirst().get(); + + assertEquals("ProcessorFirstV1", nameChangeDifference.getValueA()); + assertEquals("ProcessorFirstV2", nameChangeDifference.getValueB()); + } + + private VersionedProcessGroup createVersionedProcessGroupA() { + VersionedProcessGroup root = new VersionedProcessGroup(); + root.setProcessGroups(new HashSet<>(Arrays.asList(createProcessGroup("ID-pg1"), createProcessGroup("ID-pg2")))); + // Add processors + root.setProcessors(new HashSet<>(Arrays.asList(createVersionedProcessor("ProcessorFirstV1"), createVersionedProcessor("ProcessorSecondV1")))); + return root; + } + + private VersionedProcessGroup createProcessGroup(String identifier){ + VersionedProcessGroup processGroup = new VersionedProcessGroup(); + processGroup.setIdentifier(identifier); + return processGroup; + } + private VersionedProcessGroup createVersionedProcessGroupB() { + VersionedProcessGroup updated = createVersionedProcessGroupA(); + // remove a process group + updated.getProcessGroups().removeIf(pg->pg.getIdentifier().equals("ID-pg1")); + // change the name of a processor + updated.getProcessors().stream().forEach(p->p.setPenaltyDuration(p.getName().equals("ProcessorFirstV1") ? "1" : "2")); + updated.getProcessors().stream().forEach(p->p.setName(p.getName().equals("ProcessorFirstV1") ? "ProcessorFirstV2" : p.getName())); + return updated; + } + + private VersionedProcessor createVersionedProcessor(String name){ + VersionedProcessor processor = new VersionedProcessor(); + processor.setName(name); + processor.setIdentifier(name); + processor.setProperties(new HashMap<>()); + return processor; + } + // ------------------------------------------------------------------- + + private Answer createBucketAnswer() { + return (InvocationOnMock invocation) -> { + BucketEntity bucketEntity = (BucketEntity) invocation.getArguments()[0]; + return bucketEntity; + }; + } + + private Answer updateBucketAnswer() { + return (InvocationOnMock invocation) -> { + BucketEntity bucketEntity = (BucketEntity) invocation.getArguments()[0]; + return bucketEntity; + }; + } + + private Answer createFlowAnswer() { + return (InvocationOnMock invocation) -> { + final FlowEntity flowEntity = (FlowEntity) invocation.getArguments()[0]; + return flowEntity; + }; + } + + private Answer updateFlowAnswer() { + return (InvocationOnMock invocation) -> { + final FlowEntity flowEntity = (FlowEntity) invocation.getArguments()[0]; + return flowEntity; + }; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/alias/RegistryUrlAliasServiceTest.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/alias/RegistryUrlAliasServiceTest.java new file mode 100644 index 0000000000..a1782c4565 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/alias/RegistryUrlAliasServiceTest.java @@ -0,0 +1,171 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.service.alias; + +import org.apache.nifi.registry.url.aliaser.generated.Alias; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.Assert.assertEquals; + +public class RegistryUrlAliasServiceTest { + private static Alias createAlias(String internal, String external) { + Alias result = new Alias(); + result.setInternal(internal); + result.setExternal(external); + return result; + } + + @Test + public void testNoAliases() { + RegistryUrlAliasService aliaser = new RegistryUrlAliasService(Collections.emptyList()); + + String url = "https://registry.com:18080"; + + assertEquals(url, aliaser.getExternal(url)); + assertEquals(url, aliaser.getInternal(url)); + } + + @Test(expected = IllegalArgumentException.class) + public void testMalformedExternal() { + new RegistryUrlAliasService(Collections.singletonList(createAlias("https://registry.com:18080", "registry.com:18080"))); + } + + @Test + public void testSingleAliasUrl() { + String internal = "https://registry-1.com:18443"; + String external = "http://localhost:18080"; + String unchanged = "https://registry-2.com:18443"; + + RegistryUrlAliasService aliaser = new RegistryUrlAliasService(Collections.singletonList(createAlias(internal, external))); + + assertEquals(external, aliaser.getExternal(internal)); + assertEquals(internal, aliaser.getInternal(external)); + + assertEquals(unchanged, aliaser.getExternal(unchanged)); + assertEquals(unchanged, aliaser.getInternal(unchanged)); + + // Ensure replacement is only the prefix + internal += "/nifi-registry/"; + external += "/nifi-registry/"; + unchanged += "/nifi-registry/"; + + assertEquals(external, aliaser.getExternal(internal)); + assertEquals(internal, aliaser.getInternal(external)); + + assertEquals(unchanged, aliaser.getExternal(unchanged)); + assertEquals(unchanged, aliaser.getInternal(unchanged)); + } + + @Test + public void testSingleAliasToken() { + String internal = "THIS_NIFI_REGISTRY"; + String external = "http://localhost:18080"; + String unchanged = "https://registry-2.com:18443"; + + RegistryUrlAliasService aliaser = new RegistryUrlAliasService(Collections.singletonList(createAlias(internal, external))); + + assertEquals(external, aliaser.getExternal(internal)); + assertEquals(internal, aliaser.getInternal(external)); + + assertEquals(unchanged, aliaser.getExternal(unchanged)); + assertEquals(unchanged, aliaser.getInternal(unchanged)); + + // Ensure replacement is only the prefix + internal += "/nifi-registry/"; + external += "/nifi-registry/"; + unchanged += "/nifi-registry/"; + + assertEquals(external, aliaser.getExternal(internal)); + assertEquals(internal, aliaser.getInternal(external)); + + assertEquals(unchanged, aliaser.getExternal(unchanged)); + assertEquals(unchanged, aliaser.getInternal(unchanged)); + } + + @Test + public void testMultipleAliases() { + String internal1 = "https://registry-1.com:18443"; + String external1 = "http://localhost:18080"; + String internal2 = "https://registry-2.com:18443"; + String external2 = "http://localhost:18081"; + String internal3 = "THIS_NIFI_REGISTRY"; + String external3 = "http://localhost:18082"; + + String unchanged = "https://registry-3.com:18443"; + + RegistryUrlAliasService aliaser = new RegistryUrlAliasService(Arrays.asList(createAlias(internal1, external1), createAlias(internal2, external2), createAlias(internal3, external3))); + + assertEquals(external1, aliaser.getExternal(internal1)); + assertEquals(external2, aliaser.getExternal(internal2)); + assertEquals(external3, aliaser.getExternal(internal3)); + + assertEquals(internal1, aliaser.getInternal(external1)); + assertEquals(internal2, aliaser.getInternal(external2)); + assertEquals(internal3, aliaser.getInternal(external3)); + + assertEquals(unchanged, aliaser.getExternal(unchanged)); + assertEquals(unchanged, aliaser.getInternal(unchanged)); + + // Ensure replacement is only the prefix + internal1 += "/nifi-registry/"; + internal2 += "/nifi-registry/"; + internal3 += "/nifi-registry/"; + + external1 += "/nifi-registry/"; + external2 += "/nifi-registry/"; + external3 += "/nifi-registry/"; + + unchanged += "/nifi-registry/"; + + assertEquals(external1, aliaser.getExternal(internal1)); + assertEquals(external2, aliaser.getExternal(internal2)); + assertEquals(external3, aliaser.getExternal(internal3)); + + assertEquals(internal1, aliaser.getInternal(external1)); + assertEquals(internal2, aliaser.getInternal(external2)); + assertEquals(internal3, aliaser.getInternal(external3)); + + assertEquals(unchanged, aliaser.getExternal(unchanged)); + assertEquals(unchanged, aliaser.getInternal(unchanged)); + } + + @Test + public void testMigrationPath() { + String internal1 = "INTERNAL_TOKEN"; + String internal2 = "http://old.registry.url"; + String external = "https://new.registry.url"; + + RegistryUrlAliasService aliaser = new RegistryUrlAliasService(Arrays.asList(createAlias(internal1, external), createAlias(internal2, external))); + + assertEquals(internal1, aliaser.getInternal(external)); + + assertEquals(external, aliaser.getExternal(internal1)); + assertEquals(external, aliaser.getExternal(internal2)); + } + + @Test(expected = IllegalArgumentException.class) + public void testDuplicateInternalTokens() { + String internal = "THIS_NIFI_REGISTRY"; + String external1 = "http://localhost:18080"; + String external2 = "http://localhost:18081"; + + new RegistryUrlAliasService(Arrays.asList(createAlias(internal, external1), createAlias(internal, external2))); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/extension/docs/TestHtmlExtensionDocWriter.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/extension/docs/TestHtmlExtensionDocWriter.java new file mode 100644 index 0000000000..7d39b55620 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/extension/docs/TestHtmlExtensionDocWriter.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.service.extension.docs; + +import org.apache.commons.io.IOUtils; +import org.apache.nifi.registry.db.entity.ExtensionEntity; +import org.apache.nifi.registry.extension.bundle.BundleType; +import org.apache.nifi.registry.extension.component.ExtensionMetadata; +import org.apache.nifi.registry.extension.component.manifest.Extension; +import org.apache.nifi.registry.serialization.ExtensionSerializer; +import org.apache.nifi.registry.serialization.Serializer; +import org.apache.nifi.registry.serialization.jackson.ObjectMapperProvider; +import org.apache.nifi.registry.service.mapper.ExtensionMappings; +import org.junit.Before; +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +import static org.junit.Assert.assertNotNull; + +public class TestHtmlExtensionDocWriter { + + private ExtensionDocWriter docWriter; + private Serializer extensionSerializer; + + @Before + public void setup() { + docWriter = new HtmlExtensionDocWriter(); + extensionSerializer = new ExtensionSerializer(); + } + + @Test + public void testWriteDocsForConsumeKafkaRecord() throws IOException { + final File rawExtensionJson = new File("src/test/resources/extensions/ConsumeKafkaRecord_1_0.json"); + final String serializedExtension = getSerializedExtension(rawExtensionJson); + + final ExtensionEntity entity = new ExtensionEntity(); + entity.setContent(serializedExtension); + entity.setBucketId(UUID.randomUUID().toString()); + entity.setBucketName("My Bucket"); + entity.setGroupId("org.apache.nifi"); + entity.setArtifactId("nifi-kakfa-bundle"); + entity.setVersion("1.9.1"); + entity.setSystemApiVersion("1.9.1"); + entity.setBundleId(UUID.randomUUID().toString()); + entity.setBundleType(BundleType.NIFI_NAR); + entity.setDisplayName("ConsumeKafkaRecord_1_0"); + + final ExtensionMetadata metadata = ExtensionMappings.mapToMetadata(entity, extensionSerializer); + assertNotNull(entity); + + final Extension extension = ExtensionMappings.map(entity, extensionSerializer); + assertNotNull(extension); + + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + docWriter.write(metadata, extension, out); + + final String docsResult = new String(out.toByteArray(), StandardCharsets.UTF_8); + assertNotNull(docsResult); + + XmlValidator.assertXmlValid(docsResult); + XmlValidator.assertContains(docsResult, entity.getDisplayName()); + } + + private String getSerializedExtension(final File rawExtensionJson) throws IOException { + final ByteArrayOutputStream serializedExtension = new ByteArrayOutputStream(); + try (final InputStream inputStream = new FileInputStream(rawExtensionJson)) { + final String rawJson = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + final Extension tempExtension = ObjectMapperProvider.getMapper().readValue(rawJson, Extension.class); + extensionSerializer.serialize(tempExtension, serializedExtension); + } + + return serializedExtension.toString("UTF-8"); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/extension/docs/XmlValidator.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/extension/docs/XmlValidator.java new file mode 100644 index 0000000000..41cb6578f5 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/extension/docs/XmlValidator.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.service.extension.docs; + +import org.junit.Assert; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; +import java.io.StringReader; + +public class XmlValidator { + + public static void assertXmlValid(String xml) { + try { + final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + dbf.newDocumentBuilder().parse(new InputSource(new StringReader(xml))); + } catch (SAXException | IOException | ParserConfigurationException e) { + Assert.fail(e.getMessage()); + } + } + + public static void assertContains(String original, String subword) { + Assert.assertTrue(original + " did not contain: " + subword, original.contains(subword)); + } + + public static void assertNotContains(String original, String subword) { + Assert.assertFalse(original + " did contain: " + subword, original.contains(subword)); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/application.properties b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/application.properties new file mode 100644 index 0000000000..cbeee4f707 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/application.properties @@ -0,0 +1,28 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +# Properties for Spring Boot tests + +# Properties for Spring Boot integration tests +# Documentation for commoon Spring Boot application properties can be found at: +# https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html + +# These verbose log levels can be enabled locally for dev testing, but disable them in the repo to minimize travis logs. +#logging.level.org.springframework.core.io.support: DEBUG +#logging.level.org.springframework.context.annotation: DEBUG +#logging.level.org.springframework.web: DEBUG + +# Controls logging of SQL queries and parameters +# logging.level.org.springframework.jdbc: TRACE \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/db/migration/common/V999999.1__test-setup.sql b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/db/migration/common/V999999.1__test-setup.sql new file mode 100644 index 0000000000..c2218b7bd7 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/db/migration/common/V999999.1__test-setup.sql @@ -0,0 +1,313 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You 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. + +-- test data for buckets + +insert into BUCKET (id, name, description, created) + values ('1', 'Bucket 1', 'This is test bucket 1', DATE'2017-09-11'); + +insert into BUCKET (id, name, description, created) + values ('2', 'Bucket 2', 'This is test bucket 2', DATE'2017-09-12'); + +insert into BUCKET (id, name, description, created) + values ('3', 'Bucket 3', 'This is test bucket 3', DATE'2017-09-13'); + +insert into BUCKET (id, name, description, created) + values ('4', 'Bucket 4', 'This is test bucket 4', DATE'2017-09-14'); + +insert into BUCKET (id, name, description, created) + values ('5', 'Bucket 5', 'This is test bucket 5', DATE'2017-09-15'); + +insert into BUCKET (id, name, description, created) + values ('6', 'Bucket 6', 'This is test bucket 6', DATE'2017-09-16'); + + +-- test data for flows + +insert into BUCKET_ITEM (id, name, description, created, modified, item_type, bucket_id) + values ('1', 'Flow 1', 'This is flow 1 bucket 1', DATE'2017-09-11', DATE'2017-09-11', 'FLOW', '1'); + +insert into FLOW (id) values ('1'); + +insert into BUCKET_ITEM (id, name, description, created, modified, item_type, bucket_id) + values ('2', 'Flow 2', 'This is flow 2 bucket 1', DATE'2017-09-11', DATE'2017-09-11', 'FLOW', '1'); + +insert into FLOW (id) values ('2'); + +insert into BUCKET_ITEM (id, name, description, created, modified, item_type, bucket_id) + values ('3', 'Flow 1', 'This is flow 1 bucket 2', DATE'2017-09-11', DATE'2017-09-11', 'FLOW', '2'); + +insert into FLOW (id) values ('3'); + + +-- test data for flow snapshots + +insert into FLOW_SNAPSHOT (flow_id, version, created, created_by, comments) + values ('1', 1, DATE'2017-09-11', 'user1', 'This is flow 1 snapshot 1'); + +insert into FLOW_SNAPSHOT (flow_id, version, created, created_by, comments) + values ('1', 2, DATE'2017-09-12', 'user1', 'This is flow 1 snapshot 2'); + +insert into FLOW_SNAPSHOT (flow_id, version, created, created_by, comments) + values ('1', 3, DATE'2017-09-11', 'user1', 'This is flow 1 snapshot 3'); + + +-- test data for signing keys + +insert into SIGNING_KEY (id, tenant_identity, key_value) + values ('1', 'unit_test_tenant_identity', '0123456789abcdef'); + +-- test data for extension bundles + +-- processors bundle, depends on service api bundle +insert into BUCKET_ITEM ( + id, + name, + description, + created, + modified, + item_type, + bucket_id +) values ( + 'eb1', + 'nifi-example-processors-nar', + 'Example processors bundle', + DATE'2018-11-02', + DATE'2018-11-02', + 'BUNDLE', + '3' +); + +insert into BUNDLE ( + id, + bucket_id, + bundle_type, + group_id, + artifact_id +) values ( + 'eb1', + '3', + 'NIFI_NAR', + 'org.apache.nifi', + 'nifi-example-processors-nar' +); + +insert into BUNDLE_VERSION ( + id, + bundle_id, + version, + created, + created_by, + description, + sha_256_hex, + sha_256_supplied, + content_size +) values ( + 'eb1-v1', + 'eb1', + '1.0.0', + DATE'2018-11-02', + 'user1', + 'First version of eb1', + '123456789', + '1', + 1024 +); + +insert into BUNDLE_VERSION_DEPENDENCY ( + id, + bundle_version_id, + group_id, + artifact_id, + version +) values ( + 'eb1-v1-dep1', + 'eb1-v1', + 'org.apache.nifi', + 'nifi-example-service-api-nar', + '2.0.0' +); + +-- service impl bundle, depends on service api bundle +insert into BUCKET_ITEM ( + id, + name, + description, + created, + modified, + item_type, + bucket_id +) values ( + 'eb2', + 'nifi-example-services-nar', + 'Example services bundle', + DATE'2018-11-03', + DATE'2018-11-03', + 'BUNDLE', + '3' +); + +insert into BUNDLE ( + id, + bucket_id, + bundle_type, + group_id, + artifact_id +) values ( + 'eb2', + '3', + 'NIFI_NAR', + 'com.foo', + 'nifi-example-services-nar' +); + +insert into BUNDLE_VERSION ( + id, + bundle_id, + version, + created, + created_by, + description, + sha_256_hex, + sha_256_supplied, + content_size +) values ( + 'eb2-v1', + 'eb2', + '1.0.0', + DATE'2018-11-03', + 'user1', + 'First version of eb2', + '123456789', + '1', + 1024 +); + +insert into BUNDLE_VERSION_DEPENDENCY ( + id, + bundle_version_id, + group_id, + artifact_id, + version +) values ( + 'eb2-v1-dep1', + 'eb2-v1', + 'org.apache.nifi', + 'nifi-example-service-api-nar', + '2.0.0' +); + +-- service api bundle +insert into BUCKET_ITEM ( + id, + name, + description, + created, + modified, + item_type, + bucket_id +) values ( + 'eb3', + 'nifi-example-service-api-nar', + 'Example service API bundle', + DATE'2018-11-04', + DATE'2017-11-04', + 'BUNDLE', + '3' +); + +insert into BUNDLE ( + id, + bucket_id, + bundle_type, + group_id, + artifact_id +) values ( + 'eb3', + '3', + 'NIFI_NAR', + 'org.apache.nifi', + 'nifi-example-service-api-nar' +); + +insert into BUNDLE_VERSION ( + id, + bundle_id, + version, + created, + created_by, + description, + sha_256_hex, + sha_256_supplied, + content_size +) values ( + 'eb3-v1', + 'eb3', + '2.0.0', + DATE'2018-11-04', + 'user1', + 'First version of eb3', + '123456789', + '1', + 1024 +); + +-- test data for extensions + +insert into EXTENSION ( + id, bundle_version_id, name, display_name, type, content, has_additional_details +) values ( + 'e1', 'eb1-v1', 'org.apache.nifi.ExampleProcessor', 'ExampleProcessor', 'PROCESSOR', '{ "name" : "org.apache.nifi.ExampleProcessor", "type" : "PROCESSOR" }', 0 +); + +insert into EXTENSION ( + id, bundle_version_id, name, display_name, type, content, has_additional_details +) values ( + 'e2', 'eb1-v1', 'org.apache.nifi.ExampleProcessorRestricted', 'ExampleProcessorRestricted', 'PROCESSOR', '{ "name" : "org.apache.nifi.ExampleProcessorRestricted", "type" : "PROCESSOR" }', 0 +); + +insert into EXTENSION ( + id, bundle_version_id, name, display_name, type, content, additional_details, has_additional_details +) values ( + 'e3', 'eb2-v1', 'org.apache.nifi.ExampleService', 'ExampleService', 'CONTROLLER_SERVICE', '{ "name" : "org.apache.nifi.ExampleService", "type" : "CONTROLLER_SERVICE" }', 'extra docs', 1 +); + +-- test data for extension restrictions + +insert into EXTENSION_RESTRICTION ( + id, extension_id, required_permission, explanation +) values ( + 'er1', 'e2', 'write filesystem', 'This writes to the filesystem' +); + +-- test data for extension provided service apis + +insert into EXTENSION_PROVIDED_SERVICE_API ( + id, extension_id, class_name, group_id, artifact_id, version +) values ( + 'epapi1', 'e3', 'org.apache.nifi.ExampleServiceAPI', 'org.apache.nifi', 'nifi-example-service-api-nar', '2.0.0' +); + +-- test data for extension tags + +insert into EXTENSION_TAG (extension_id, tag) values ('e1', 'example'); +insert into EXTENSION_TAG (extension_id, tag) values ('e1', 'processor'); + +insert into EXTENSION_TAG (extension_id, tag) values ('e2', 'example'); +insert into EXTENSION_TAG (extension_id, tag) values ('e2', 'processor'); +insert into EXTENSION_TAG (extension_id, tag) values ('e2', 'restricted'); + +insert into EXTENSION_TAG (extension_id, tag) values ('e3', 'example'); +insert into EXTENSION_TAG (extension_id, tag) values ('e3', 'service'); \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/extensions/ConsumeKafkaRecord_1_0.json b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/extensions/ConsumeKafkaRecord_1_0.json new file mode 100644 index 0000000000..026b143e62 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/extensions/ConsumeKafkaRecord_1_0.json @@ -0,0 +1,369 @@ +{ + "description": "Consumes messages from Apache Kafka specifically built against the Kafka 1.0 Consumer API. The complementary NiFi processor for sending messages is PublishKafkaRecord_1_0. Please note that, at this time, the Processor assumes that all records that are retrieved from a given partition have the same schema. If any of the Kafka messages are pulled but cannot be parsed or written with the configured Record Reader or Record Writer, the contents of the message will be written to a separate FlowFile, and that FlowFile will be transferred to the 'parse.failure' relationship. Otherwise, each FlowFile is sent to the 'success' relationship and may contain many individual messages within the single FlowFile. A 'record.count' attribute is added to indicate how many messages are contained in the FlowFile. No two Kafka messages will be placed into the same FlowFile if they have different schemas, or if they have different values for a message header that is included by the property.", + "dynamicProperty": [ + { + "description": "These properties will be added on the Kafka configuration after loading any provided configuration properties. In the event a dynamic property represents a property that was already set, its value will be ignored and WARN message logged. For the list of available Kafka properties please refer to: https://kafka.apache.org/documentation.html#configuration. ", + "expressionLanguageScope": "VARIABLE_REGISTRY", + "expressionLanguageSupported": false, + "name": "The name of a Kafka configuration property.", + "value": "The value of a given Kafka configuration property." + } + ], + "inputRequirement": "INPUT_FORBIDDEN", + "name": "org.apache.nifi.processors.kafka.pubsub.ConsumeKafkaRecord_1_0", + "property": [ + { + "defaultValue": "localhost:9092", + "description": "A comma-separated list of known Kafka Brokers in the format :", + "displayName": "Kafka Brokers", + "dynamic": false, + "dynamicallyModifiesClasspath": false, + "expressionLanguageScope": "VARIABLE_REGISTRY", + "expressionLanguageSupported": true, + "name": "bootstrap.servers", + "required": true, + "sensitive": false + }, + { + "defaultValue": "", + "description": "The name of the Kafka Topic(s) to pull from. More than one can be supplied if comma separated.", + "displayName": "Topic Name(s)", + "dynamic": false, + "dynamicallyModifiesClasspath": false, + "expressionLanguageScope": "VARIABLE_REGISTRY", + "expressionLanguageSupported": true, + "name": "topic", + "required": true, + "sensitive": false + }, + { + "allowableValue": [ + { + "description": "Topic is a full topic name or comma separated list of names", + "displayName": "names", + "value": "names" + }, + { + "description": "Topic is a regex using the Java Pattern syntax", + "displayName": "pattern", + "value": "pattern" + } + ], + "defaultValue": "names", + "description": "Specifies whether the Topic(s) provided are a comma separated list of names or a single regular expression", + "displayName": "Topic Name Format", + "dynamic": false, + "dynamicallyModifiesClasspath": false, + "expressionLanguageScope": "NONE", + "expressionLanguageSupported": false, + "name": "topic_type", + "required": true, + "sensitive": false + }, + { + "controllerServiceDefinition": { + "artifactId": "nifi-standard-services-api-nar", + "className": "org.apache.nifi.serialization.RecordReaderFactory", + "groupId": "org.apache.nifi", + "version": "1.10.0-SNAPSHOT" + }, + "defaultValue": "", + "description": "The Record Reader to use for incoming FlowFiles", + "displayName": "Record Reader", + "dynamic": false, + "dynamicallyModifiesClasspath": false, + "expressionLanguageScope": "NONE", + "expressionLanguageSupported": false, + "name": "record-reader", + "required": true, + "sensitive": false + }, + { + "controllerServiceDefinition": { + "artifactId": "nifi-standard-services-api-nar", + "className": "org.apache.nifi.serialization.RecordSetWriterFactory", + "groupId": "org.apache.nifi", + "version": "1.10.0-SNAPSHOT" + }, + "defaultValue": "", + "description": "The Record Writer to use in order to serialize the data before sending to Kafka", + "displayName": "Record Writer", + "dynamic": false, + "dynamicallyModifiesClasspath": false, + "expressionLanguageScope": "NONE", + "expressionLanguageSupported": false, + "name": "record-writer", + "required": true, + "sensitive": false + }, + { + "allowableValue": [ + { + "description": "", + "displayName": "true", + "value": "true" + }, + { + "description": "", + "displayName": "false", + "value": "false" + } + ], + "defaultValue": "true", + "description": "Specifies whether or not NiFi should honor transactional guarantees when communicating with Kafka. If false, the Processor will use an \"isolation level\" of read_uncomitted. This means that messages will be received as soon as they are written to Kafka but will be pulled, even if the producer cancels the transactions. If this value is true, NiFi will not receive any messages for which the producer's transaction was canceled, but this can result in some latency since the consumer must wait for the producer to finish its entire transaction instead of pulling as the messages become available.", + "displayName": "Honor Transactions", + "dynamic": false, + "dynamicallyModifiesClasspath": false, + "expressionLanguageScope": "NONE", + "expressionLanguageSupported": false, + "name": "honor-transactions", + "required": true, + "sensitive": false + }, + { + "allowableValue": [ + { + "description": "PLAINTEXT", + "displayName": "PLAINTEXT", + "value": "PLAINTEXT" + }, + { + "description": "SSL", + "displayName": "SSL", + "value": "SSL" + }, + { + "description": "SASL_PLAINTEXT", + "displayName": "SASL_PLAINTEXT", + "value": "SASL_PLAINTEXT" + }, + { + "description": "SASL_SSL", + "displayName": "SASL_SSL", + "value": "SASL_SSL" + } + ], + "defaultValue": "PLAINTEXT", + "description": "Protocol used to communicate with brokers. Corresponds to Kafka's 'security.protocol' property.", + "displayName": "Security Protocol", + "dynamic": false, + "dynamicallyModifiesClasspath": false, + "expressionLanguageScope": "NONE", + "expressionLanguageSupported": false, + "name": "security.protocol", + "required": true, + "sensitive": false + }, + { + "controllerServiceDefinition": { + "artifactId": "nifi-standard-services-api-nar", + "className": "org.apache.nifi.kerberos.KerberosCredentialsService", + "groupId": "org.apache.nifi", + "version": "1.10.0-SNAPSHOT" + }, + "defaultValue": "", + "description": "Specifies the Kerberos Credentials Controller Service that should be used for authenticating with Kerberos", + "displayName": "Kerberos Credentials Service", + "dynamic": false, + "dynamicallyModifiesClasspath": false, + "expressionLanguageScope": "NONE", + "expressionLanguageSupported": false, + "name": "kerberos-credentials-service", + "required": false, + "sensitive": false + }, + { + "defaultValue": "", + "description": "The Kerberos principal name that Kafka runs as. This can be defined either in Kafka's JAAS config or in Kafka's config. Corresponds to Kafka's 'security.protocol' property.It is ignored unless one of the SASL options of the are selected.", + "displayName": "Kerberos Service Name", + "dynamic": false, + "dynamicallyModifiesClasspath": false, + "expressionLanguageScope": "VARIABLE_REGISTRY", + "expressionLanguageSupported": true, + "name": "sasl.kerberos.service.name", + "required": false, + "sensitive": false + }, + { + "defaultValue": "", + "description": "The Kerberos principal that will be used to connect to brokers. If not set, it is expected to set a JAAS configuration file in the JVM properties defined in the bootstrap.conf file. This principal will be set into 'sasl.jaas.config' Kafka's property.", + "displayName": "Kerberos Principal", + "dynamic": false, + "dynamicallyModifiesClasspath": false, + "expressionLanguageScope": "VARIABLE_REGISTRY", + "expressionLanguageSupported": true, + "name": "sasl.kerberos.principal", + "required": false, + "sensitive": false + }, + { + "defaultValue": "", + "description": "The Kerberos keytab that will be used to connect to brokers. If not set, it is expected to set a JAAS configuration file in the JVM properties defined in the bootstrap.conf file. This principal will be set into 'sasl.jaas.config' Kafka's property.", + "displayName": "Kerberos Keytab", + "dynamic": false, + "dynamicallyModifiesClasspath": false, + "expressionLanguageScope": "VARIABLE_REGISTRY", + "expressionLanguageSupported": true, + "name": "sasl.kerberos.keytab", + "required": false, + "sensitive": false + }, + { + "controllerServiceDefinition": { + "artifactId": "nifi-standard-services-api-nar", + "className": "org.apache.nifi.ssl.SSLContextService", + "groupId": "org.apache.nifi", + "version": "1.10.0-SNAPSHOT" + }, + "defaultValue": "", + "description": "Specifies the SSL Context Service to use for communicating with Kafka.", + "displayName": "SSL Context Service", + "dynamic": false, + "dynamicallyModifiesClasspath": false, + "expressionLanguageScope": "NONE", + "expressionLanguageSupported": false, + "name": "ssl.context.service", + "required": false, + "sensitive": false + }, + { + "defaultValue": "", + "description": "A Group ID is used to identify consumers that are within the same consumer group. Corresponds to Kafka's 'group.id' property.", + "displayName": "Group ID", + "dynamic": false, + "dynamicallyModifiesClasspath": false, + "expressionLanguageScope": "VARIABLE_REGISTRY", + "expressionLanguageSupported": true, + "name": "group.id", + "required": true, + "sensitive": false + }, + { + "allowableValue": [ + { + "description": "Automatically reset the offset to the earliest offset", + "displayName": "earliest", + "value": "earliest" + }, + { + "description": "Automatically reset the offset to the latest offset", + "displayName": "latest", + "value": "latest" + }, + { + "description": "Throw exception to the consumer if no previous offset is found for the consumer's group", + "displayName": "none", + "value": "none" + } + ], + "defaultValue": "latest", + "description": "Allows you to manage the condition when there is no initial offset in Kafka or if the current offset does not exist any more on the server (e.g. because that data has been deleted). Corresponds to Kafka's 'auto.offset.reset' property.", + "displayName": "Offset Reset", + "dynamic": false, + "dynamicallyModifiesClasspath": false, + "expressionLanguageScope": "NONE", + "expressionLanguageSupported": false, + "name": "auto.offset.reset", + "required": true, + "sensitive": false + }, + { + "defaultValue": "UTF-8", + "description": "Any message header that is found on a Kafka message will be added to the outbound FlowFile as an attribute. This property indicates the Character Encoding to use for deserializing the headers.", + "displayName": "Message Header Encoding", + "dynamic": false, + "dynamicallyModifiesClasspath": false, + "expressionLanguageScope": "NONE", + "expressionLanguageSupported": false, + "name": "message-header-encoding", + "required": false, + "sensitive": false + }, + { + "defaultValue": "", + "description": "A Regular Expression that is matched against all message headers. Any message header whose name matches the regex will be added to the FlowFile as an Attribute. If not specified, no Header values will be added as FlowFile attributes. If two messages have a different value for the same header and that header is selected by the provided regex, then those two messages must be added to different FlowFiles. As a result, users should be cautious about using a regex like \".*\" if messages are expected to have header values that are unique per message, such as an identifier or timestamp, because it will prevent NiFi from bundling the messages together efficiently.", + "displayName": "Headers to Add as Attributes (Regex)", + "dynamic": false, + "dynamicallyModifiesClasspath": false, + "expressionLanguageScope": "NONE", + "expressionLanguageSupported": false, + "name": "header-name-regex", + "required": false, + "sensitive": false + }, + { + "defaultValue": "10000", + "description": "Specifies the maximum number of records Kafka should return in a single poll.", + "displayName": "Max Poll Records", + "dynamic": false, + "dynamicallyModifiesClasspath": false, + "expressionLanguageScope": "NONE", + "expressionLanguageSupported": false, + "name": "max.poll.records", + "required": false, + "sensitive": false + }, + { + "defaultValue": "1 secs", + "description": "Specifies the maximum amount of time allowed to pass before offsets must be committed. This value impacts how often offsets will be committed. Committing offsets less often increases throughput but also increases the window of potential data duplication in the event of a rebalance or JVM restart between commits. This value is also related to maximum poll records and the use of a message demarcator. When using a message demarcator we can have far more uncommitted messages than when we're not as there is much less for us to keep track of in memory.", + "displayName": "Max Uncommitted Time", + "dynamic": false, + "dynamicallyModifiesClasspath": false, + "expressionLanguageScope": "NONE", + "expressionLanguageSupported": false, + "name": "max-uncommit-offset-wait", + "required": false, + "sensitive": false + } + ], + "relationship": [ + { + "autoTerminated": false, + "description": "FlowFiles received from Kafka. Depending on demarcation strategy it is a flow file per message or a bundle of messages grouped by topic and partition.", + "name": "success" + }, + { + "autoTerminated": false, + "description": "If a message from Kafka cannot be parsed using the configured Record Reader, the contents of the message will be routed to this Relationship as its own individual FlowFile.", + "name": "parse.failure" + } + ], + "see": [ + "org.apache.nifi.processors.kafka.pubsub.ConsumeKafka_1_0", + "org.apache.nifi.processors.kafka.pubsub.PublishKafka_1_0", + "org.apache.nifi.processors.kafka.pubsub.PublishKafkaRecord_1_0" + ], + "tag": [ + "Kafka", + "Get", + "Record", + "csv", + "avro", + "json", + "Ingest", + "Ingress", + "Topic", + "PubSub", + "Consume", + "1.0" + ], + "type": "PROCESSOR", + "writesAttribute": [ + { + "description": "The number of records received", + "name": "record.count" + }, + { + "description": "The MIME Type that is provided by the configured Record Writer", + "name": "mime.type" + }, + { + "description": "The partition of the topic the records are from", + "name": "kafka.partition" + }, + { + "description": "The topic records are from", + "name": "kafka.topic" + } + ] +} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/nifi-example.ldif b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/nifi-example.ldif new file mode 100644 index 0000000000..2af953aec5 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/nifi-example.ldif @@ -0,0 +1,174 @@ +## --------------------------------------------------------------------------- +## Licensed to the Apache Software Foundation (ASF) under one or more +## contributor license agreements. See the NOTICE file distributed with +## this work for additional information regarding copyright ownership. +## The ASF licenses this file to You 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. +## --------------------------------------------------------------------------- + +version: 1 + +dn: o=nifi +objectclass: extensibleObject +objectclass: top +objectclass: domain +dc: nifi +o: nifi + +dn: ou=users,o=nifi +objectClass: organizationalUnit +objectClass: top +ou: users + +dn: ou=users-2,o=nifi +objectClass: organizationalUnit +objectClass: top +ou: users-2 + +dn: cn=User 1,ou=users,o=nifi +objectClass: organizationalPerson +objectClass: person +objectClass: inetOrgPerson +objectClass: top +cn: User 1 +sn: User1 +uid: user1 + +dn: cn=User 2,ou=users,o=nifi +objectClass: organizationalPerson +objectClass: person +objectClass: inetOrgPerson +objectClass: top +cn: User 2 +sn: User2 +uid: user2 + +dn: cn=User 3,ou=users,o=nifi +objectClass: organizationalPerson +objectClass: person +objectClass: inetOrgPerson +objectClass: top +cn: User 3 +sn: User3 +uid: user3 + +## since the embedded ldap does not support memberof, we are using description to simulate + +dn: cn=User 4,ou=users,o=nifi +objectClass: organizationalPerson +objectClass: person +objectClass: inetOrgPerson +objectClass: top +cn: User 4 +sn: User4 +description: cn=team1,ou=groups,o=nifi +uid: user4 + +dn: cn=User 5,ou=users,o=nifi +objectClass: organizationalPerson +objectClass: person +objectClass: inetOrgPerson +objectClass: top +cn: User 5 +sn: User5 +description: cn=team1,ou=groups,o=nifi +uid: user5 + +dn: cn=User 6,ou=users,o=nifi +objectClass: organizationalPerson +objectClass: person +objectClass: inetOrgPerson +objectClass: top +cn: User 6 +sn: User6 +description: cn=team2,ou=groups,o=nifi +uid: user6 + +dn: cn=User 7,ou=users,o=nifi +objectClass: organizationalPerson +objectClass: person +objectClass: inetOrgPerson +objectClass: top +cn: User 7 +sn: User7 +description: cn=team2,ou=groups,o=nifi +uid: user7 + +dn: cn=User 8,ou=users,o=nifi +objectClass: organizationalPerson +objectClass: person +objectClass: inetOrgPerson +objectClass: top +cn: User 8 +sn: User8 +description: cn=Team2,ou=groups,o=nifi +uid: user8 + +dn: cn=User 9,ou=users-2,o=nifi +objectClass: organizationalPerson +objectClass: person +objectClass: inetOrgPerson +objectClass: top +cn: User 9 +sn: User9 +description: team3 +uid: user9 + +dn: ou=groups,o=nifi +objectClass: organizationalUnit +objectClass: top +ou: groups + +dn: ou=groups-2,o=nifi +objectClass: organizationalUnit +objectClass: top +ou: groups + +dn: cn=admins,ou=groups,o=nifi +objectClass: groupOfNames +objectClass: top +cn: admins +member: cn=User 1,ou=users,o=nifi +member: cn=User 3,ou=users,o=nifi + +dn: cn=read-only,ou=groups,o=nifi +objectClass: groupOfNames +objectClass: top +cn: read-only +member: cn=User 2,ou=users,o=nifi + +dn: cn=team1,ou=groups,o=nifi +objectClass: groupOfNames +objectClass: top +cn: team1 +member: cn=User 1,ou=users,o=nifi + +dn: cn=team2,ou=groups,o=nifi +objectClass: groupOfNames +objectClass: top +cn: team2 +member: cn=User 1,ou=users,o=nifi + +dn: cn=team4,ou=groups,o=nifi +objectClass: groupOfNames +objectClass: top +cn: team4 +member: cn=User 1,ou=users,o=nifi +member: cn=user 2,ou=users,o=nifi + +## since the embedded ldap requires member to be fqdn, we are simulating using room and description + +dn: cn=team3,ou=groups-2,o=nifi +objectClass: room +objectClass: top +cn: team3 +description: user9 diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/hook/bad-script-provider.xml b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/hook/bad-script-provider.xml new file mode 100644 index 0000000000..c6dfc70b8f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/hook/bad-script-provider.xml @@ -0,0 +1,36 @@ + + + + + + org.apache.nifi.registry.provider.MockFlowPersistenceProvider + flow foo + flow bar + + + + org.apache.nifi.registry.provider.hook.ScriptEventHookProvider + + + + + + org.apache.nifi.registry.provider.MockBundlePersistenceProvider + extension foo + extension bar + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/providers-class-not-found.xml b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/providers-class-not-found.xml new file mode 100644 index 0000000000..e887bf02d3 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/providers-class-not-found.xml @@ -0,0 +1,30 @@ + + + + + + org.apache.nifi.registry.provider.FlowProviderXXX + foo + bar + + + + org.apache.nifi.registry.provider.MockBundlePersistenceProvider + extension foo + extension bar + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/providers-good.xml b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/providers-good.xml new file mode 100644 index 0000000000..615e90b1c3 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/providers-good.xml @@ -0,0 +1,30 @@ + + + + + + org.apache.nifi.registry.provider.MockFlowPersistenceProvider + flow foo + flow bar + + + + org.apache.nifi.registry.provider.MockBundlePersistenceProvider + extension foo + extension bar + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-ap-provider-ids.xml b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-ap-provider-ids.xml new file mode 100644 index 0000000000..dcbc36aeda --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-ap-provider-ids.xml @@ -0,0 +1,47 @@ + + + + + + file-user-group-provider + org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider + ./target/test-classes/security/users.xml + + + + file-access-policy-provider + org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider + file-user-group-provider + ./target/test-classes/security/authorizations1.xml + + + + + file-access-policy-provider + org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider + file-user-group-provider + ./target/test-classes/security/authorizations2.xml + + + + managed-authorizer + org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer + file-access-policy-provider + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-authorizer-ids.xml b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-authorizer-ids.xml new file mode 100644 index 0000000000..d31de9173a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-authorizer-ids.xml @@ -0,0 +1,46 @@ + + + + + + file-user-group-provider + org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider + ./target/test-classes/security/users.xml + + + + file-access-policy-provider + org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider + file-user-group-provider + ./target/test-classes/security/authorizations.xml + + + + managed-authorizer + org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer + file-access-policy-provider + + + + + managed-authorizer + org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer + file-access-policy-provider + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-composite.xml b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-composite.xml new file mode 100644 index 0000000000..1aa96f87ba --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-composite.xml @@ -0,0 +1,54 @@ + + + + + + file-user-group-provider-1 + org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider + ./target/test-classes/security/users.xml + + + + file-user-group-provider-2 + org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider + ./target/test-classes/security/read-only-users.xml + + + + composite-user-group-provider + org.apache.nifi.registry.security.authorization.CompositeUserGroupProvider + file-user-group-provider-1 + file-user-group-provider-2 + + file-user-group-provider-2 + + + + file-access-policy-provider + org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider + composite-user-group-provider + ./target/test-classes/security/authorizations.xml + + + + managed-authorizer + org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer + file-access-policy-provider + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-configurable-composite.xml b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-configurable-composite.xml new file mode 100644 index 0000000000..f019345b45 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-configurable-composite.xml @@ -0,0 +1,54 @@ + + + + + + file-user-group-provider-1 + org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider + ./target/test-classes/security/users.xml + + + + file-user-group-provider-2 + org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider + ./target/test-classes/security/read-only-users.xml + + + + composite-configurable-user-group-provider + org.apache.nifi.registry.security.authorization.CompositeConfigurableUserGroupProvider + file-user-group-provider-1 + + file-user-group-provider-1 + file-user-group-provider-2 + + + + file-access-policy-provider + org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider + composite-configurable-user-group-provider + ./target/test-classes/security/authorizations.xml + + + + managed-authorizer + org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer + file-access-policy-provider + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-ug-provider-ids.xml b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-ug-provider-ids.xml new file mode 100644 index 0000000000..a28a36dfbc --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-ug-provider-ids.xml @@ -0,0 +1,46 @@ + + + + + + file-user-group-provider + org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider + ./target/test-classes/security/users1.xml + + + + + file-user-group-provider + org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider + ./target/test-classes/security/users2.xml + + + + file-access-policy-provider + org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider + file-user-group-provider + ./target/test-classes/security/authorizations.xml + + + + managed-authorizer + org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer + file-access-policy-provider + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-good-file-providers.xml b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-good-file-providers.xml new file mode 100644 index 0000000000..98ad3ce130 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-good-file-providers.xml @@ -0,0 +1,39 @@ + + + + + + file-user-group-provider + org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider + ./target/test-classes/security/users.xml + + + + file-access-policy-provider + org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider + file-user-group-provider + ./target/test-classes/security/authorizations.xml + + + + managed-authorizer + org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer + file-access-policy-provider + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/json/no-version.snapshot b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/json/no-version.snapshot new file mode 100644 index 0000000000..ce1901f5c8 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/json/no-version.snapshot @@ -0,0 +1,5 @@ +{ + "header": { + }, + "content": {} +} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/json/non-integer-version.snapshot b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/json/non-integer-version.snapshot new file mode 100644 index 0000000000..33d4da35af --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/json/non-integer-version.snapshot @@ -0,0 +1,6 @@ +{ + "header": { + "dataModelVersion": "One" + }, + "content": {} +} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver1.snapshot b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver1.snapshot new file mode 100644 index 0000000000..7c1ab49b60 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver1.snapshot differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver2.snapshot b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver2.snapshot new file mode 100644 index 0000000000..7f4dfc558f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver2.snapshot @@ -0,0 +1,97 @@ +{ + "header": { + "dataModelVersion": 2 + }, + "content": { + "identifier": "a2c80883-171c-316d-ba25-24df2c352693", + "name": "Flow1", + "comments": "", + "position": { + "x": 1549.249149182042, + "y": 764.2426186568309 + }, + "processGroups": [], + "remoteProcessGroups": [], + "processors": [ + { + "identifier": "92fe4513-21c0-34f6-a916-2874f46ae864", + "name": "GenerateFlowFile", + "comments": "", + "position": { + "x": 488.99999411591034, + "y": 114.00000359389122 + }, + "bundle": { + "group": "org.apache.nifi", + "artifact": "nifi-standard-nar", + "version": "1.6.0-SNAPSHOT" + }, + "style": {}, + "type": "org.apache.nifi.processors.standard.GenerateFlowFile", + "properties": { + "character-set": "UTF-8", + "File Size": "0B", + "Batch Size": "1", + "Unique FlowFiles": "false", + "Data Format": "Text" + }, + "propertyDescriptors": { + "character-set": { + "name": "character-set", + "displayName": "Character Set", + "identifiesControllerService": false, + "sensitive": false + }, + "File Size": { + "name": "File Size", + "displayName": "File Size", + "identifiesControllerService": false, + "sensitive": false + }, + "generate-ff-custom-text": { + "name": "generate-ff-custom-text", + "displayName": "Custom Text", + "identifiesControllerService": false, + "sensitive": false + }, + "Batch Size": { + "name": "Batch Size", + "displayName": "Batch Size", + "identifiesControllerService": false, + "sensitive": false + }, + "Unique FlowFiles": { + "name": "Unique FlowFiles", + "displayName": "Unique FlowFiles", + "identifiesControllerService": false, + "sensitive": false + }, + "Data Format": { + "name": "Data Format", + "displayName": "Data Format", + "identifiesControllerService": false, + "sensitive": false + } + }, + "schedulingPeriod": "0 sec", + "schedulingStrategy": "TIMER_DRIVEN", + "executionNode": "ALL", + "penaltyDuration": "30 sec", + "yieldDuration": "1 sec", + "bulletinLevel": "WARN", + "runDurationMillis": 0, + "concurrentlySchedulableTaskCount": 1, + "componentType": "PROCESSOR", + "groupIdentifier": "a2c80883-171c-316d-ba25-24df2c352693" + } + ], + "inputPorts": [], + "outputPorts": [], + "connections": [], + "labels": [], + "funnels": [], + "controllerServices": [], + "variables": {}, + "componentType": "PROCESS_GROUP" + } +} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver3.snapshot b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver3.snapshot new file mode 100644 index 0000000000..3611014190 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver3.snapshot @@ -0,0 +1,28 @@ +{ + "header" : { + "dataModelVersion" : "3" + }, + "content" : { + "flowSnapshot" : { + "flowContents" : { + "componentType" : "PROCESS_GROUP", + "connections" : [ ], + "controllerServices" : [ ], + "funnels" : [ ], + "identifier" : "pg1", + "inputPorts" : [ ], + "labels" : [ ], + "name" : "My Process Group", + "outputPorts" : [ ], + "processGroups" : [ ], + "processors" : [ { + "componentType" : "PROCESSOR", + "identifier" : "processor1", + "name" : "My Processor 1" + } ], + "remoteProcessGroups" : [ ], + "variables" : { } + } + } + } +} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver9999.snapshot b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver9999.snapshot new file mode 100644 index 0000000000..743401b4d9 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver9999.snapshot @@ -0,0 +1,6 @@ +{ + "header": { + "dataModelVersion": 9999 + }, + "content": {} +} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-jetty/pom.xml b/nifi-registry/nifi-registry-core/nifi-registry-jetty/pom.xml new file mode 100644 index 0000000000..bf9e6adfc9 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-jetty/pom.xml @@ -0,0 +1,76 @@ + + + + 4.0.0 + + org.apache.nifi.registry + nifi-registry-core + 1.14.0-SNAPSHOT + + nifi-registry-jetty + jar + + + org.apache.nifi.registry + nifi-registry-properties + 1.14.0-SNAPSHOT + + + org.eclipse.jetty + jetty-server + + + org.eclipse.jetty + jetty-servlet + + + org.eclipse.jetty + jetty-webapp + + + org.eclipse.jetty + jetty-servlets + + + org.eclipse.jetty + jetty-annotations + + + org.apache.commons + commons-lang3 + + + org.eclipse.jetty + apache-jsp + compile + + + org.eclipse.jetty + apache-jstl + compile + + + org.codehaus.groovy + groovy-test + test + + + org.mockito + mockito-core + test + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/JettyServer.java b/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/JettyServer.java new file mode 100644 index 0000000000..04d08de7b0 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/JettyServer.java @@ -0,0 +1,566 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.jetty; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.jetty.headers.ContentSecurityPolicyFilter; +import org.apache.nifi.registry.jetty.headers.StrictTransportSecurityFilter; +import org.apache.nifi.registry.jetty.headers.XFrameOptionsFilter; +import org.apache.nifi.registry.jetty.headers.XSSProtectionFilter; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.apache.nifi.registry.security.crypto.CryptoKeyProvider; +import org.eclipse.jetty.annotations.AnnotationConfiguration; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.server.handler.HandlerCollection; +import org.eclipse.jetty.servlet.DefaultServlet; +import org.eclipse.jetty.servlet.FilterHolder; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.eclipse.jetty.webapp.Configuration; +import org.eclipse.jetty.webapp.JettyWebXmlConfiguration; +import org.eclipse.jetty.webapp.WebAppClassLoader; +import org.eclipse.jetty.webapp.WebAppContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.DispatcherType; +import javax.servlet.Filter; +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + + +public class JettyServer { + + private static final Logger logger = LoggerFactory.getLogger(JettyServer.class); + private static final String WEB_DEFAULTS_XML = "org/apache/nifi-registry/web/webdefault.xml"; + private static final int HEADER_BUFFER_SIZE = 16 * 1024; // 16kb + + private static final FileFilter WAR_FILTER = new FileFilter() { + @Override + public boolean accept(File pathname) { + final String nameToTest = pathname.getName().toLowerCase(); + return nameToTest.endsWith(".war") && pathname.isFile(); + } + }; + + private final NiFiRegistryProperties properties; + private final CryptoKeyProvider masterKeyProvider; + private final String docsLocation; + private final Server server; + + private WebAppContext webUiContext; + private WebAppContext webApiContext; + private WebAppContext webDocsContext; + + public JettyServer(final NiFiRegistryProperties properties, final CryptoKeyProvider cryptoKeyProvider, final String docsLocation) { + final QueuedThreadPool threadPool = new QueuedThreadPool(properties.getWebThreads()); + threadPool.setName("NiFi Registry Web Server"); + + this.properties = properties; + this.masterKeyProvider = cryptoKeyProvider; + this.docsLocation = docsLocation; + this.server = new Server(threadPool); + + // enable the annotation based configuration to ensure the jsp container is initialized properly + final Configuration.ClassList classlist = Configuration.ClassList.setServerDefault(server); + classlist.addBefore(JettyWebXmlConfiguration.class.getName(), AnnotationConfiguration.class.getName()); + + try { + configureConnectors(); + loadWars(); + } catch (final Throwable t) { + startUpFailure(t); + } + } + + /** + * Instantiates this object but does not perform any configuration. Used for unit testing. + */ + JettyServer(Server server, NiFiRegistryProperties properties) { + this.server = server; + this.properties = properties; + this.masterKeyProvider = null; + this.docsLocation = null; + } + + /** + * Returns a File object for the directory containing NIFI documentation. + *

+ * Formerly, if the docsDirectory did not exist NIFI would fail to start + * with an IllegalStateException and a rather unhelpful log message. + * NIFI-2184 updates the process such that if the docsDirectory does not + * exist an attempt will be made to create the directory. If that is + * successful NIFI will no longer fail and will start successfully barring + * any other errors. The side effect of the docsDirectory not being present + * is that the documentation links under the 'General' portion of the help + * page will not be accessible, but at least the process will be running. + * + * @param docsDirectory Name of documentation directory in installation directory. + * @return A File object to the documentation directory; else startUpFailure called. + */ + private File getDocsDir(final String docsDirectory) { + File docsDir; + try { + docsDir = Paths.get(docsDirectory).toRealPath().toFile(); + } catch (IOException ex) { + logger.info("Directory '" + docsDirectory + "' is missing. Some documentation will be unavailable."); + docsDir = new File(docsDirectory).getAbsoluteFile(); + final boolean made = docsDir.mkdirs(); + if (!made) { + logger.error("Failed to create 'docs' directory!"); + startUpFailure(new IOException(docsDir.getAbsolutePath() + " could not be created")); + } + } + return docsDir; + } + + private void configureConnectors() { + // create the http configuration + final HttpConfiguration httpConfiguration = new HttpConfiguration(); + httpConfiguration.setRequestHeaderSize(HEADER_BUFFER_SIZE); + httpConfiguration.setResponseHeaderSize(HEADER_BUFFER_SIZE); + httpConfiguration.setSendServerVersion(properties.shouldSendServerVersion()); + + if (properties.getPort() != null) { + final Integer port = properties.getPort(); + if (port < 0 || (int) Math.pow(2, 16) <= port) { + throw new IllegalStateException("Invalid HTTP port: " + port); + } + + logger.info("Configuring Jetty for HTTP on port: " + port); + + // create the connector + final ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(httpConfiguration)); + + // set host and port + if (StringUtils.isNotBlank(properties.getHttpHost())) { + http.setHost(properties.getHttpHost()); + } + http.setPort(port); + + // add this connector + server.addConnector(http); + } else if (properties.getSslPort() != null) { + final Integer port = properties.getSslPort(); + if (port < 0 || (int) Math.pow(2, 16) <= port) { + throw new IllegalStateException("Invalid HTTPs port: " + port); + } + + if (StringUtils.isBlank(properties.getKeyStorePath())) { + throw new IllegalStateException(NiFiRegistryProperties.SECURITY_KEYSTORE + + " must be provided to configure Jetty for HTTPs"); + } + + logger.info("Configuring Jetty for HTTPs on port: " + port); + + // add some secure config + final HttpConfiguration httpsConfiguration = new HttpConfiguration(httpConfiguration); + httpsConfiguration.setSecureScheme("https"); + httpsConfiguration.setSecurePort(properties.getSslPort()); + httpsConfiguration.addCustomizer(new SecureRequestCustomizer()); + + // build the connector + final ServerConnector https = new ServerConnector(server, + new SslConnectionFactory(createSslContextFactory(), "http/1.1"), + new HttpConnectionFactory(httpsConfiguration)); + + // set host and port + if (StringUtils.isNotBlank(properties.getHttpsHost())) { + https.setHost(properties.getHttpsHost()); + } + https.setPort(port); + + // add this connector + server.addConnector(https); + } + } + + private SslContextFactory createSslContextFactory() { + final SslContextFactory.Server contextFactory = new SslContextFactory.Server(); + + // if needClientAuth is false then set want to true so we can optionally use certs + if (properties.getNeedClientAuth()) { + logger.info("Setting Jetty's SSLContextFactory needClientAuth to true"); + contextFactory.setNeedClientAuth(true); + } else { + logger.info("Setting Jetty's SSLContextFactory wantClientAuth to true"); + contextFactory.setWantClientAuth(true); + } + + /* below code sets JSSE system properties when values are provided */ + // keystore properties + if (StringUtils.isNotBlank(properties.getKeyStorePath())) { + contextFactory.setKeyStorePath(properties.getKeyStorePath()); + } + if (StringUtils.isNotBlank(properties.getKeyStoreType())) { + contextFactory.setKeyStoreType(properties.getKeyStoreType()); + } + + + final String keystorePassword = properties.getKeyStorePassword(); + final String keyPassword = properties.getKeyPassword(); + + if (StringUtils.isEmpty(keystorePassword)) { + throw new IllegalArgumentException("The keystore password cannot be null or empty"); + } else { + // if no key password was provided, then assume the key password is the same as the keystore password. + final String defaultKeyPassword = (StringUtils.isBlank(keyPassword)) ? keystorePassword : keyPassword; + contextFactory.setKeyStorePassword(keystorePassword); + contextFactory.setKeyManagerPassword(defaultKeyPassword); + } + + // truststore properties + if (StringUtils.isNotBlank(properties.getTrustStorePath())) { + contextFactory.setTrustStorePath(properties.getTrustStorePath()); + } + if (StringUtils.isNotBlank(properties.getTrustStoreType())) { + contextFactory.setTrustStoreType(properties.getTrustStoreType()); + } + if (StringUtils.isNotBlank(properties.getTrustStorePassword())) { + contextFactory.setTrustStorePassword(properties.getTrustStorePassword()); + } + + return contextFactory; + } + + private void loadWars() throws IOException { + final File warDirectory = properties.getWarLibDirectory(); + final File[] wars = warDirectory.listFiles(WAR_FILTER); + + if (wars == null) { + throw new RuntimeException("Unable to access war lib directory: " + warDirectory); + } + + File webUiWar = null; + File webApiWar = null; + File webDocsWar = null; + for (final File war : wars) { + if (war.getName().startsWith("nifi-registry-web-ui")) { + webUiWar = war; + } else if (war.getName().startsWith("nifi-registry-web-api")) { + webApiWar = war; + } else if (war.getName().startsWith("nifi-registry-web-docs")) { + webDocsWar = war; + } + } + + if (webUiWar == null) { + throw new IllegalStateException("Unable to locate NiFi Registry Web UI"); + } else if (webApiWar == null) { + throw new IllegalStateException("Unable to locate NiFi Registry Web API"); + } else if (webDocsWar == null) { + throw new IllegalStateException("Unable to locate NiFi Registry Web Docs"); + } + + webUiContext = loadWar(webUiWar, "/nifi-registry"); + webUiContext.getInitParams().put("oidc-supported", String.valueOf(properties.isOidcEnabled())); + + webApiContext = loadWar(webApiWar, "/nifi-registry-api", getWebApiAdditionalClasspath()); + logger.info("Adding {} object to ServletContext with key 'nifi-registry.properties'", properties.getClass().getSimpleName()); + webApiContext.setAttribute("nifi-registry.properties", properties); + logger.info("Adding {} object to ServletContext with key 'nifi-registry.key'", masterKeyProvider.getClass().getSimpleName()); + webApiContext.setAttribute("nifi-registry.key", masterKeyProvider); + + // there is an issue scanning the asm repackaged jar so narrow down what we are scanning + webApiContext.setAttribute("org.eclipse.jetty.server.webapp.WebInfIncludeJarPattern", ".*/spring-[^/]*\\.jar$"); + + final String docsContextPath = "/nifi-registry-docs"; + webDocsContext = loadWar(webDocsWar, docsContextPath); + addDocsServlets(webDocsContext); + + final HandlerCollection handlers = new HandlerCollection(); + handlers.addHandler(webUiContext); + handlers.addHandler(webApiContext); + handlers.addHandler(webDocsContext); + server.setHandler(handlers); + } + + private WebAppContext loadWar(final File warFile, final String contextPath) + throws IOException { + return loadWar(warFile, contextPath, new URL[0]); + } + + private WebAppContext loadWar(final File warFile, final String contextPath, final URL[] additionalResources) + throws IOException { + final WebAppContext webappContext = new WebAppContext(warFile.getPath(), contextPath); + webappContext.setContextPath(contextPath); + webappContext.setDisplayName(contextPath); + + // remove slf4j server class to allow WAR files to have slf4j dependencies in WEB-INF/lib + List serverClasses = new ArrayList<>(Arrays.asList(webappContext.getServerClasses())); + serverClasses.remove("org.slf4j."); + webappContext.setServerClasses(serverClasses.toArray(new String[0])); + webappContext.setDefaultsDescriptor(WEB_DEFAULTS_XML); + + // get the temp directory for this webapp + final File webWorkingDirectory = properties.getWebWorkingDirectory(); + final File tempDir = new File(webWorkingDirectory, warFile.getName()); + if (tempDir.exists() && !tempDir.isDirectory()) { + throw new RuntimeException(tempDir.getAbsolutePath() + " is not a directory"); + } else if (!tempDir.exists()) { + final boolean made = tempDir.mkdirs(); + if (!made) { + throw new RuntimeException(tempDir.getAbsolutePath() + " could not be created"); + } + } + if (!(tempDir.canRead() && tempDir.canWrite())) { + throw new RuntimeException(tempDir.getAbsolutePath() + " directory does not have read/write privilege"); + } + + // configure the temp dir + webappContext.setTempDirectory(tempDir); + + // configure the max form size (3x the default) + webappContext.setMaxFormContentSize(600000); + + // add HTTP security headers to all responses + final String ALL_PATHS = "/*"; + ArrayList> filters = new ArrayList<>(Arrays.asList(XFrameOptionsFilter.class, ContentSecurityPolicyFilter.class, XSSProtectionFilter.class)); + if(properties.isHTTPSConfigured()) { + filters.add(StrictTransportSecurityFilter.class); + } + + filters.forEach( (filter) -> addFilters(filter, ALL_PATHS, webappContext)); + + // start out assuming the system ClassLoader will be the parent, but if additional resources were specified then + // inject a new ClassLoader in between the system and webapp ClassLoaders that contains the additional resources + ClassLoader parentClassLoader = ClassLoader.getSystemClassLoader(); + if (additionalResources != null && additionalResources.length > 0) { + URLClassLoader additionalClassLoader = new URLClassLoader(additionalResources, ClassLoader.getSystemClassLoader()); + parentClassLoader = additionalClassLoader; + } + + webappContext.setClassLoader(new WebAppClassLoader(parentClassLoader, webappContext)); + + logger.info("Loading WAR: " + warFile.getAbsolutePath() + " with context path set to " + contextPath); + return webappContext; + } + + private void addFilters(Class clazz, String path, WebAppContext webappContext) { + FilterHolder holder = new FilterHolder(clazz); + holder.setName(clazz.getSimpleName()); + webappContext.addFilter(holder, path, EnumSet.allOf(DispatcherType.class)); + } + + private URL[] getWebApiAdditionalClasspath() { + final String dbDriverDir = properties.getDatabaseDriverDirectory(); + + if (StringUtils.isBlank(dbDriverDir)) { + logger.info("No database driver directory was specified"); + return new URL[0]; + } + + final File dirFile = new File(dbDriverDir); + + if (!dirFile.exists()) { + logger.warn("Skipping database driver directory that does not exist: " + dbDriverDir); + return new URL[0]; + } + + if (!dirFile.canRead()) { + logger.warn("Skipping database driver directory that can not be read: " + dbDriverDir); + return new URL[0]; + } + + final List resources = new LinkedList<>(); + try { + resources.add(dirFile.toURI().toURL()); + } catch (final MalformedURLException mfe) { + logger.warn("Unable to add {} to classpath due to {}", new Object[]{ dirFile.getAbsolutePath(), mfe.getMessage()}, mfe); + } + + if (dirFile.isDirectory()) { + final File[] files = dirFile.listFiles(); + if (files != null) { + for (final File resource : files) { + if (resource.isDirectory()) { + logger.warn("Recursive directories are not supported, skipping " + resource.getAbsolutePath()); + } else { + try { + resources.add(resource.toURI().toURL()); + } catch (final MalformedURLException mfe) { + logger.warn("Unable to add {} to classpath due to {}", new Object[]{ resource.getAbsolutePath(), mfe.getMessage()}, mfe); + } + } + } + } + } + + if (!resources.isEmpty()) { + logger.info("Added additional resources to nifi-registry-api classpath: ["); + for (URL resource : resources) { + logger.info(" " + resource.toString()); + } + logger.info("]"); + } + + return resources.toArray(new URL[resources.size()]); + } + + private void addDocsServlets(WebAppContext docsContext) { + try { + // Load the nifi-registry/docs directory + final File docsDir = getDocsDir(docsLocation); + + // Create the servlet which will serve the static resources + ServletHolder defaultHolder = new ServletHolder("default", DefaultServlet.class); + defaultHolder.setInitParameter("dirAllowed", "false"); + + ServletHolder docs = new ServletHolder("docs", DefaultServlet.class); + docs.setInitParameter("resourceBase", docsDir.getPath()); + docs.setInitParameter("dirAllowed", "false"); + + docsContext.addServlet(docs, "/html/*"); + docsContext.addServlet(defaultHolder, "/"); + + // load the rest documentation + final File webApiDocsDir = new File(webApiContext.getTempDirectory(), "webapp/docs"); + if (!webApiDocsDir.exists()) { + final boolean made = webApiDocsDir.mkdirs(); + if (!made) { + throw new RuntimeException(webApiDocsDir.getAbsolutePath() + " could not be created"); + } + } + + ServletHolder apiDocs = new ServletHolder("apiDocs", DefaultServlet.class); + apiDocs.setInitParameter("resourceBase", webApiDocsDir.getPath()); + apiDocs.setInitParameter("dirAllowed", "false"); + + docsContext.addServlet(apiDocs, "/rest-api/*"); + + logger.info("Loading documents web app with context path set to " + docsContext.getContextPath()); + + } catch (Exception ex) { + logger.error("Unhandled Exception in createDocsWebApp: " + ex.getMessage()); + startUpFailure(ex); + } + } + + public void start() { + try { + // start the server + server.start(); + + // ensure everything started successfully + for (Handler handler : server.getChildHandlers()) { + // see if the handler is a web app + if (handler instanceof WebAppContext) { + WebAppContext context = (WebAppContext) handler; + + // see if this webapp had any exceptions that would + // cause it to be unavailable + if (context.getUnavailableException() != null) { + startUpFailure(context.getUnavailableException()); + } + } + } + + dumpUrls(); + } catch (final Throwable t) { + startUpFailure(t); + } + } + + private void startUpFailure(Throwable t) { + System.err.println("Failed to start web server: " + t.getMessage()); + System.err.println("Shutting down..."); + logger.warn("Failed to start web server... shutting down.", t); + System.exit(1); + } + + private void dumpUrls() throws SocketException { + final List urls = new ArrayList<>(); + + for (Connector connector : server.getConnectors()) { + if (connector instanceof ServerConnector) { + final ServerConnector serverConnector = (ServerConnector) connector; + + Set hosts = new HashSet<>(); + + // determine the hosts + if (StringUtils.isNotBlank(serverConnector.getHost())) { + hosts.add(serverConnector.getHost()); + } else { + Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); + if (networkInterfaces != null) { + for (NetworkInterface networkInterface : Collections.list(networkInterfaces)) { + for (InetAddress inetAddress : Collections.list(networkInterface.getInetAddresses())) { + hosts.add(inetAddress.getHostAddress()); + } + } + } + } + + // ensure some hosts were found + if (!hosts.isEmpty()) { + String scheme = "http"; + if (properties.getSslPort() != null && serverConnector.getPort() == properties.getSslPort()) { + scheme = "https"; + } + + // dump each url + for (String host : hosts) { + urls.add(String.format("%s://%s:%s", scheme, host, serverConnector.getPort())); + } + } + } + } + + if (urls.isEmpty()) { + logger.warn("NiFi Registry has started, but the UI is not available on any hosts. Please verify the host properties."); + } else { + // log the ui location + logger.info("NiFi Registry has started. The UI is available at the following URLs:"); + for (final String url : urls) { + logger.info(String.format("%s/nifi-registry", url)); + } + } + } + + public void stop() { + try { + server.stop(); + } catch (Exception ex) { + logger.warn("Failed to stop web server", ex); + } + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/ContentSecurityPolicyFilter.java b/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/ContentSecurityPolicyFilter.java new file mode 100644 index 0000000000..758e939599 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/ContentSecurityPolicyFilter.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.jetty.headers; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * A filter to apply the Content Security Policy header. + * + */ +public class ContentSecurityPolicyFilter implements Filter { + private static final String HEADER = "Content-Security-Policy"; + private static final String POLICY = "frame-ancestors 'self'"; + + private static final Logger logger = LoggerFactory.getLogger(ContentSecurityPolicyFilter.class); + + @Override + public void doFilter(final ServletRequest req, final ServletResponse resp, final FilterChain filterChain) + throws IOException, ServletException { + + final HttpServletResponse response = (HttpServletResponse) resp; + response.setHeader(HEADER, POLICY); + + filterChain.doFilter(req, resp); + } + + @Override + public void init(final FilterConfig config) { + } + + @Override + public void destroy() { + } +} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/StrictTransportSecurityFilter.java b/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/StrictTransportSecurityFilter.java new file mode 100644 index 0000000000..7f0f913885 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/StrictTransportSecurityFilter.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.jetty.headers; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * A filter to apply the HTTP Strict Transport Security (HSTS) HTTP header. This forces the browser to use HTTPS for + * all + */ +public class StrictTransportSecurityFilter implements Filter { + private static final String HEADER = "Strict-Transport-Security"; + private static final String POLICY = "max-age=31540000"; + + private static final Logger logger = LoggerFactory.getLogger(StrictTransportSecurityFilter.class); + + @Override + public void doFilter(final ServletRequest req, final ServletResponse resp, final FilterChain filterChain) + throws IOException, ServletException { + + final HttpServletResponse response = (HttpServletResponse) resp; + response.setHeader(HEADER, POLICY); + + filterChain.doFilter(req, resp); + } + + @Override + public void init(final FilterConfig config) { + } + + @Override + public void destroy() { + } +} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/XFrameOptionsFilter.java b/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/XFrameOptionsFilter.java new file mode 100644 index 0000000000..fad5bbcb05 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/XFrameOptionsFilter.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.jetty.headers; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * A filter to apply the X-Frame-Options header. + * + */ +public class XFrameOptionsFilter implements Filter { + private static final String HEADER = "X-Frame-Options"; + private static final String POLICY = "SAMEORIGIN"; + + private static final Logger logger = LoggerFactory.getLogger(XFrameOptionsFilter.class); + + @Override + public void doFilter(final ServletRequest req, final ServletResponse resp, final FilterChain filterChain) + throws IOException, ServletException { + + final HttpServletResponse response = (HttpServletResponse) resp; + response.setHeader(HEADER, POLICY); + + filterChain.doFilter(req, resp); + } + + @Override + public void init(final FilterConfig config) { + } + + @Override + public void destroy() { + } +} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/XSSProtectionFilter.java b/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/XSSProtectionFilter.java new file mode 100644 index 0000000000..62792f1b4f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/XSSProtectionFilter.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.jetty.headers; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * A filter to apply the Cross Site Scripting (XSS) HTTP header. Protects against reflected cross-site scripting attacks. + * The browser will prevent rendering of the page if an attack is detected. + */ + +public class XSSProtectionFilter implements Filter { + private static final String HEADER = "X-XSS-Protection"; + private static final String POLICY = "1; mode=block"; + + private static final Logger logger = LoggerFactory.getLogger(XSSProtectionFilter.class); + + @Override + public void doFilter(final ServletRequest req, final ServletResponse resp, final FilterChain filterChain) + throws IOException, ServletException { + + final HttpServletResponse response = (HttpServletResponse) resp; + response.setHeader(HEADER, POLICY); + + filterChain.doFilter(req, resp); + } + + @Override + public void init(final FilterConfig config) { + } + + @Override + public void destroy() { + } +} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/main/resources/org/apache/nifi-registry/web/webdefault.xml b/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/main/resources/org/apache/nifi-registry/web/webdefault.xml new file mode 100644 index 0000000000..f686689977 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/main/resources/org/apache/nifi-registry/web/webdefault.xml @@ -0,0 +1,556 @@ + + + + + + + + + + + + + + + + + + + + + + + + Default web.xml file. + This file is applied to a Web application before it's own WEB_INF/web.xml file + + + + + + + + org.eclipse.jetty.servlet.listener.ELContextCleaner + + + + + + + + org.eclipse.jetty.servlet.listener.IntrospectorCleaner + + + + + + + + + + + + + + + + + + + default + org.eclipse.jetty.servlet.DefaultServlet + + aliases + false + + + acceptRanges + true + + + dirAllowed + false + + + welcomeServlets + true + + + redirectWelcome + false + + + maxCacheSize + 256000000 + + + maxCachedFileSize + 200000000 + + + maxCachedFiles + 2048 + + + gzip + true + + + etags + false + + + useFileMappedBuffer + true + + + + 0 + + + + default + / + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + jsp + org.apache.jasper.servlet.JspServlet + + logVerbosityLevel + DEBUG + + + fork + false + + + keepgenerated + true + + + development + false + + + xpoweredBy + false + + + compilerTargetVM + 1.7 + + + compilerSourceVM + 1.7 + + + 0 + + + + jsp + *.jsp + *.jspf + *.jspx + *.xsp + *.JSP + *.JSPF + *.JSPX + *.XSP + + + + + + 30 + + + + + + + + + + + + + index.html + index.htm + index.jsp + + + + + + ar + ISO-8859-6 + + + be + ISO-8859-5 + + + bg + ISO-8859-5 + + + ca + ISO-8859-1 + + + cs + ISO-8859-2 + + + da + ISO-8859-1 + + + de + ISO-8859-1 + + + el + ISO-8859-7 + + + en + ISO-8859-1 + + + es + ISO-8859-1 + + + et + ISO-8859-1 + + + fi + ISO-8859-1 + + + fr + ISO-8859-1 + + + hr + ISO-8859-2 + + + hu + ISO-8859-2 + + + is + ISO-8859-1 + + + it + ISO-8859-1 + + + iw + ISO-8859-8 + + + ja + Shift_JIS + + + ko + EUC-KR + + + lt + ISO-8859-2 + + + lv + ISO-8859-2 + + + mk + ISO-8859-5 + + + nl + ISO-8859-1 + + + no + ISO-8859-1 + + + pl + ISO-8859-2 + + + pt + ISO-8859-1 + + + ro + ISO-8859-2 + + + ru + ISO-8859-5 + + + sh + ISO-8859-5 + + + sk + ISO-8859-2 + + + sl + ISO-8859-2 + + + sq + ISO-8859-2 + + + sr + ISO-8859-5 + + + sv + ISO-8859-1 + + + tr + ISO-8859-9 + + + uk + ISO-8859-5 + + + zh + GB2312 + + + zh_TW + Big5 + + + + + + Disable TRACE + / + TRACE + + + + + + Enable everything but TRACE + / + TRACE + + + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/test/groovy/org/apache/nifi/registry/jetty/JettyServerGroovyTest.groovy b/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/test/groovy/org/apache/nifi/registry/jetty/JettyServerGroovyTest.groovy new file mode 100644 index 0000000000..a96e5c1872 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/test/groovy/org/apache/nifi/registry/jetty/JettyServerGroovyTest.groovy @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.jetty + +import org.apache.nifi.registry.properties.NiFiRegistryProperties +import org.eclipse.jetty.util.ssl.SslContextFactory +import org.junit.Rule +import org.junit.Test +import org.junit.rules.ExpectedException +import org.junit.runner.RunWith +import org.mockito.junit.MockitoJUnitRunner +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.eclipse.jetty.server.Server + +@RunWith(MockitoJUnitRunner.class) +class JettyServerGroovyTest extends GroovyTestCase { + + private static final Logger logger = LoggerFactory.getLogger(JettyServerGroovyTest.class) + + private static final keyPassword = "keyPassword" + private static final keystorePassword = "keystorePassword" + private static final truststorePassword = "truststorePassword" + private static final matchingPassword = "thePassword" + + @Test + void testCreateSslContextFactoryWithKeystoreAndKeypassword() throws Exception { + + // Arrange + NiFiRegistryProperties properties = new NiFiRegistryProperties() + properties.setProperty(NiFiRegistryProperties.SECURITY_TRUSTSTORE, "src/test/resources/truststore.jks") + properties.setProperty(NiFiRegistryProperties.SECURITY_TRUSTSTORE_PASSWD, truststorePassword) + properties.setProperty(NiFiRegistryProperties.SECURITY_TRUSTSTORE_TYPE, "JKS") + properties.setProperty(NiFiRegistryProperties.SECURITY_KEYSTORE, "src/test/resources/keystoreDifferentPasswords.jks") + properties.setProperty(NiFiRegistryProperties.SECURITY_KEY_PASSWD, keyPassword) + properties.setProperty(NiFiRegistryProperties.SECURITY_KEYSTORE_PASSWD, keystorePassword) + properties.setProperty(NiFiRegistryProperties.SECURITY_KEYSTORE_TYPE, "JKS") + + Server internalServer = new Server() + JettyServer testServer = new JettyServer(internalServer, properties) + + // Act + SslContextFactory sslContextFactory = testServer.createSslContextFactory() + sslContextFactory.start() + + // Assert + assertNotNull(sslContextFactory) + assertNotNull(sslContextFactory.getSslContext()) + } + + @Test + void testCreateSslContextFactoryWithOnlyKeystorePassword() throws Exception { + + // Arrange + NiFiRegistryProperties properties = new NiFiRegistryProperties() + properties.setProperty(NiFiRegistryProperties.SECURITY_TRUSTSTORE, "src/test/resources/truststore.jks") + properties.setProperty(NiFiRegistryProperties.SECURITY_TRUSTSTORE_PASSWD, truststorePassword) + properties.setProperty(NiFiRegistryProperties.SECURITY_TRUSTSTORE_TYPE, "JKS") + properties.setProperty(NiFiRegistryProperties.SECURITY_KEYSTORE, "src/test/resources/keystoreSamePassword.jks") + properties.setProperty(NiFiRegistryProperties.SECURITY_KEYSTORE_PASSWD, matchingPassword) + properties.setProperty(NiFiRegistryProperties.SECURITY_KEYSTORE_TYPE, "JKS") + + Server internalServer = new Server() + JettyServer testServer = new JettyServer(internalServer, properties) + + // Act + SslContextFactory sslContextFactory = testServer.createSslContextFactory() + sslContextFactory.start() + + // Assert + assertNotNull(sslContextFactory) + assertNotNull(sslContextFactory.getSslContext()) + } + + @Test + void testCreateSslContextFactoryWithMatchingPasswordsDefined() throws Exception { + + // Arrange + NiFiRegistryProperties properties = new NiFiRegistryProperties() + properties.setProperty(NiFiRegistryProperties.SECURITY_TRUSTSTORE, "src/test/resources/truststore.jks") + properties.setProperty(NiFiRegistryProperties.SECURITY_TRUSTSTORE_PASSWD, truststorePassword) + properties.setProperty(NiFiRegistryProperties.SECURITY_TRUSTSTORE_TYPE, "JKS") + properties.setProperty(NiFiRegistryProperties.SECURITY_KEYSTORE, "src/test/resources/keystoreSamePassword.jks") + properties.setProperty(NiFiRegistryProperties.SECURITY_KEY_PASSWD, matchingPassword) + properties.setProperty(NiFiRegistryProperties.SECURITY_KEYSTORE_PASSWD, matchingPassword) + properties.setProperty(NiFiRegistryProperties.SECURITY_KEYSTORE_TYPE, "JKS") + + Server internalServer = new Server() + JettyServer testServer = new JettyServer(internalServer, properties) + + // Act + SslContextFactory sslContextFactory = testServer.createSslContextFactory() + sslContextFactory.start() + + // Assert + assertNotNull(sslContextFactory) + assertNotNull(sslContextFactory.getSslContext()) + } + + @Rule public ExpectedException exception = ExpectedException.none() + + @Test + void testCreateSslContextFactoryWithNoKeystorePasswordFails() throws Exception { + + // Arrange + exception.expect(IllegalArgumentException.class) + exception.expectMessage("The keystore password cannot be null or empty") + + NiFiRegistryProperties properties = new NiFiRegistryProperties() + properties.setProperty(NiFiRegistryProperties.SECURITY_TRUSTSTORE, "src/test/resources/truststore.jks") + properties.setProperty(NiFiRegistryProperties.SECURITY_TRUSTSTORE_PASSWD, truststorePassword) + properties.setProperty(NiFiRegistryProperties.SECURITY_TRUSTSTORE_TYPE, "JKS") + properties.setProperty(NiFiRegistryProperties.SECURITY_KEYSTORE, "src/test/resources/keystoreSamePassword.jks") + properties.setProperty(NiFiRegistryProperties.SECURITY_KEYSTORE_TYPE, "JKS") + + Server internalServer = new Server() + JettyServer testServer = new JettyServer(internalServer, properties) + + // Act but expect exception + SslContextFactory sslContextFactory = testServer.createSslContextFactory() + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/test/resources/keystoreDifferentPasswords.jks b/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/test/resources/keystoreDifferentPasswords.jks new file mode 100644 index 0000000000..98c8903ad9 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/test/resources/keystoreDifferentPasswords.jks differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/test/resources/keystoreSamePassword.jks b/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/test/resources/keystoreSamePassword.jks new file mode 100644 index 0000000000..aeedd7f952 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/test/resources/keystoreSamePassword.jks differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/test/resources/truststore.jks b/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/test/resources/truststore.jks new file mode 100644 index 0000000000..47c8e45c3c Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-jetty/src/test/resources/truststore.jks differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/pom.xml b/nifi-registry/nifi-registry-core/nifi-registry-properties/pom.xml new file mode 100644 index 0000000000..32c55615fc --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/pom.xml @@ -0,0 +1,76 @@ + + + + 4.0.0 + + org.apache.nifi.registry + nifi-registry-core + 1.14.0-SNAPSHOT + + nifi-registry-properties + jar + + + + + org.codehaus.gmavenplus + gmavenplus-plugin + 1.5 + + + + addTestSources + testCompile + + + + + + + + + + org.apache.commons + commons-lang3 + + + org.bouncycastle + bcprov-jdk15on + ${bouncycastle.version} + + + org.codehaus.groovy + groovy-test + test + + + cglib + cglib-nodep + 2.2.2 + test + + + org.slf4j + slf4j-simple + 1.7.12 + test + + + org.mockito + mockito-core + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProvider.java new file mode 100644 index 0000000000..b7d1d2e23f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProvider.java @@ -0,0 +1,265 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.properties; + +import org.apache.commons.lang3.StringUtils; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.util.encoders.Base64; +import org.bouncycastle.util.encoders.DecoderException; +import org.bouncycastle.util.encoders.EncoderException; +import org.bouncycastle.util.encoders.Hex; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SecureRandom; +import java.security.Security; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class AESSensitivePropertyProvider implements SensitivePropertyProvider { + private static final Logger logger = LoggerFactory.getLogger(AESSensitivePropertyProvider.class); + + private static final String IMPLEMENTATION_NAME = "AES Sensitive Property Provider"; + private static final String IMPLEMENTATION_KEY = "aes/gcm/"; + private static final String ALGORITHM = "AES/GCM/NoPadding"; + private static final String PROVIDER = "BC"; + private static final String DELIMITER = "||"; // "|" is not a valid Base64 character, so ensured not to be present in cipher text + private static final int IV_LENGTH = 12; + private static final int MIN_CIPHER_TEXT_LENGTH = IV_LENGTH * 4 / 3 + DELIMITER.length() + 1; + + private Cipher cipher; + private final SecretKey key; + + public AESSensitivePropertyProvider(String keyHex) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException { + byte[] key = validateKey(keyHex); + + try { + Security.addProvider(new BouncyCastleProvider()); + cipher = Cipher.getInstance(ALGORITHM, PROVIDER); + // Only store the key if the cipher was initialized successfully + this.key = new SecretKeySpec(key, "AES"); + } catch (NoSuchAlgorithmException | NoSuchProviderException | NoSuchPaddingException e) { + logger.error("Encountered an error initializing the {}: {}", IMPLEMENTATION_NAME, e.getMessage()); + throw new SensitivePropertyProtectionException("Error initializing the protection cipher", e); + } + } + + private byte[] validateKey(String keyHex) { + if (keyHex == null || StringUtils.isBlank(keyHex)) { + throw new SensitivePropertyProtectionException("The key cannot be empty"); + } + keyHex = formatHexKey(keyHex); + if (!isHexKeyValid(keyHex)) { + throw new SensitivePropertyProtectionException("The key must be a valid hexadecimal key"); + } + byte[] key = Hex.decode(keyHex); + final List validKeyLengths = getValidKeyLengths(); + if (!validKeyLengths.contains(key.length * 8)) { + List validKeyLengthsAsStrings = validKeyLengths.stream().map(i -> Integer.toString(i)).collect(Collectors.toList()); + throw new SensitivePropertyProtectionException("The key (" + key.length * 8 + " bits) must be a valid length: " + StringUtils.join(validKeyLengthsAsStrings, ", ")); + } + return key; + } + + public AESSensitivePropertyProvider(byte[] key) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException { + this(key == null ? "" : Hex.toHexString(key)); + } + + private static String formatHexKey(String input) { + if (input == null || StringUtils.isBlank(input)) { + return ""; + } + return input.replaceAll("[^0-9a-fA-F]", "").toLowerCase(); + } + + private static boolean isHexKeyValid(String key) { + if (key == null || StringUtils.isBlank(key)) { + return false; + } + // Key length is in "nibbles" (i.e. one hex char = 4 bits) + return getValidKeyLengths().contains(key.length() * 4) && key.matches("^[0-9a-fA-F]*$"); + } + + private static List getValidKeyLengths() { + List validLengths = new ArrayList<>(); + validLengths.add(128); + + try { + if (Cipher.getMaxAllowedKeyLength("AES") > 128) { + validLengths.add(192); + validLengths.add(256); + } else { + logger.warn("JCE Unlimited Strength Cryptography Jurisdiction policies are not available, so the max key length is 128 bits"); + } + } catch (NoSuchAlgorithmException e) { + logger.warn("Encountered an error determining the max key length", e); + } + + return validLengths; + } + + /** + * Returns the name of the underlying implementation. + * + * @return the name of this sensitive property provider + */ + @Override + public String getName() { + return IMPLEMENTATION_NAME; + } + + /** + * Returns the key used to identify the provider implementation in {@code nifi.properties}. + * + * @return the key to persist in the sibling property + */ + @Override + public String getIdentifierKey() { + return IMPLEMENTATION_KEY + getKeySize(Hex.toHexString(key.getEncoded())); + } + + private int getKeySize(String key) { + if (StringUtils.isBlank(key)) { + return 0; + } else { + // A key in hexadecimal format has one char per nibble (4 bits) + return formatHexKey(key).length() * 4; + } + } + + /** + * Returns the encrypted cipher text. + * + * @param unprotectedValue the sensitive value + * @return the value to persist in the {@code nifi.properties} file + * @throws SensitivePropertyProtectionException if there is an exception encrypting the value + */ + @Override + public String protect(String unprotectedValue) throws SensitivePropertyProtectionException { + if (unprotectedValue == null || unprotectedValue.trim().length() == 0) { + throw new IllegalArgumentException("Cannot encrypt an empty value"); + } + + // Generate IV + byte[] iv = generateIV(); + if (iv.length < IV_LENGTH) { + throw new IllegalArgumentException("The IV (" + iv.length + " bytes) must be at least " + IV_LENGTH + " bytes"); + } + + try { + // Initialize cipher for encryption + cipher.init(Cipher.ENCRYPT_MODE, this.key, new IvParameterSpec(iv)); + + byte[] plainBytes = unprotectedValue.getBytes(StandardCharsets.UTF_8); + byte[] cipherBytes = cipher.doFinal(plainBytes); + logger.info(getName() + " encrypted a sensitive value successfully"); + return base64Encode(iv) + DELIMITER + base64Encode(cipherBytes); + // return Base64.toBase64String(iv) + DELIMITER + Base64.toBase64String(cipherBytes); + } catch (BadPaddingException | IllegalBlockSizeException | EncoderException | InvalidAlgorithmParameterException | InvalidKeyException e) { + final String msg = "Error encrypting a protected value"; + logger.error(msg, e); + throw new SensitivePropertyProtectionException(msg, e); + } + } + + private String base64Encode(byte[] input) { + return Base64.toBase64String(input).replaceAll("=", ""); + } + + /** + * Generates a new random IV of 12 bytes using {@link SecureRandom}. + * + * @return the IV + */ + private byte[] generateIV() { + byte[] iv = new byte[IV_LENGTH]; + new SecureRandom().nextBytes(iv); + return iv; + } + + /** + * Returns the decrypted plaintext. + * + * @param protectedValue the cipher text read from the {@code nifi.properties} file + * @return the raw value to be used by the application + * @throws SensitivePropertyProtectionException if there is an error decrypting the cipher text + */ + @Override + public String unprotect(String protectedValue) throws SensitivePropertyProtectionException { + if (protectedValue == null || protectedValue.trim().length() < MIN_CIPHER_TEXT_LENGTH) { + throw new IllegalArgumentException("Cannot decrypt a cipher text shorter than " + MIN_CIPHER_TEXT_LENGTH + " chars"); + } + + if (!protectedValue.contains(DELIMITER)) { + throw new IllegalArgumentException("The cipher text does not contain the delimiter " + DELIMITER + " -- it should be of the form Base64(IV) || Base64(cipherText)"); + } + + protectedValue = protectedValue.trim(); + + final String IV_B64 = protectedValue.substring(0, protectedValue.indexOf(DELIMITER)); + byte[] iv = Base64.decode(IV_B64); + if (iv.length < IV_LENGTH) { + throw new IllegalArgumentException("The IV (" + iv.length + " bytes) must be at least " + IV_LENGTH + " bytes"); + } + + String CIPHERTEXT_B64 = protectedValue.substring(protectedValue.indexOf(DELIMITER) + 2); + + // Restore the = padding if necessary to reconstitute the GCM MAC check + if (CIPHERTEXT_B64.length() % 4 != 0) { + final int paddedLength = CIPHERTEXT_B64.length() + 4 - (CIPHERTEXT_B64.length() % 4); + CIPHERTEXT_B64 = StringUtils.rightPad(CIPHERTEXT_B64, paddedLength, '='); + } + + try { + byte[] cipherBytes = Base64.decode(CIPHERTEXT_B64); + + cipher.init(Cipher.DECRYPT_MODE, this.key, new IvParameterSpec(iv)); + byte[] plainBytes = cipher.doFinal(cipherBytes); + logger.debug(getName() + " decrypted a sensitive value successfully"); + return new String(plainBytes, StandardCharsets.UTF_8); + } catch (BadPaddingException | IllegalBlockSizeException | DecoderException | InvalidAlgorithmParameterException | InvalidKeyException e) { + final String msg = "Error decrypting a protected value"; + logger.error(msg, e); + throw new SensitivePropertyProtectionException(msg, e); + } + } + + public static int getIvLength() { + return IV_LENGTH; + } + + public static int getMinCipherTextLength() { + return MIN_CIPHER_TEXT_LENGTH; + } + + public static String getDelimiter() { + return DELIMITER; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactory.java new file mode 100644 index 0000000000..5c24a73c63 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactory.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.properties; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.crypto.NoSuchPaddingException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; + +public class AESSensitivePropertyProviderFactory implements SensitivePropertyProviderFactory { + private static final Logger logger = LoggerFactory.getLogger(AESSensitivePropertyProviderFactory.class); + + private String keyHex; + + public AESSensitivePropertyProviderFactory(String keyHex) { + this.keyHex = keyHex; + } + + public SensitivePropertyProvider getProvider() throws SensitivePropertyProtectionException { + try { + if (keyHex != null && !StringUtils.isBlank(keyHex)) { + return new AESSensitivePropertyProvider(keyHex); + } else { + throw new SensitivePropertyProtectionException("The provider factory cannot generate providers without a key"); + } + } catch (NoSuchAlgorithmException | NoSuchProviderException | NoSuchPaddingException e) { + String msg = "Error creating AES Sensitive Property Provider"; + logger.warn(msg, e); + throw new SensitivePropertyProtectionException(msg, e); + } + } + + @Override + public String toString() { + return "SensitivePropertyProviderFactory for creating AESSensitivePropertyProviders"; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/MultipleSensitivePropertyProtectionException.java b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/MultipleSensitivePropertyProtectionException.java new file mode 100644 index 0000000000..df4047feac --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/MultipleSensitivePropertyProtectionException.java @@ -0,0 +1,129 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.properties; + +import org.apache.commons.lang3.StringUtils; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +public class MultipleSensitivePropertyProtectionException extends SensitivePropertyProtectionException { + + private Set failedKeys; + + /** + * Constructs a new throwable with {@code null} as its detail message. + * The cause is not initialized, and may subsequently be initialized by a + * call to {@link #initCause}. + *

+ *

The {@link #fillInStackTrace()} method is called to initialize + * the stack trace data in the newly created throwable. + */ + public MultipleSensitivePropertyProtectionException() { + } + + /** + * Constructs a new throwable with the specified detail message. The + * cause is not initialized, and may subsequently be initialized by + * a call to {@link #initCause}. + *

+ *

The {@link #fillInStackTrace()} method is called to initialize + * the stack trace data in the newly created throwable. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + */ + public MultipleSensitivePropertyProtectionException(String message) { + super(message); + } + + /** + * Constructs a new throwable with the specified detail message and + * cause.

Note that the detail message associated with + * {@code cause} is not automatically incorporated in + * this throwable's detail message. + *

+ *

The {@link #fillInStackTrace()} method is called to initialize + * the stack trace data in the newly created throwable. + * + * @param message the detail message (which is saved for later retrieval + * by the {@link #getMessage()} method). + * @param cause the cause (which is saved for later retrieval by the + * {@link #getCause()} method). (A {@code null} value is + * permitted, and indicates that the cause is nonexistent or + * unknown.) + * @since 1.4 + */ + public MultipleSensitivePropertyProtectionException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new throwable with the specified cause and a detail + * message of {@code (cause==null ? null : cause.toString())} (which + * typically contains the class and detail message of {@code cause}). + * This constructor is useful for throwables that are little more than + * wrappers for other throwables (for example, PrivilegedActionException). + *

+ *

The {@link #fillInStackTrace()} method is called to initialize + * the stack trace data in the newly created throwable. + * + * @param cause the cause (which is saved for later retrieval by the + * {@link #getCause()} method). (A {@code null} value is + * permitted, and indicates that the cause is nonexistent or + * unknown.) + * @since 1.4 + */ + public MultipleSensitivePropertyProtectionException(Throwable cause) { + super(cause); + } + + /** + * Constructs a new exception with the provided message and a unique set of the keys that caused the error. + * + * @param message the message + * @param failedKeys any failed keys + */ + public MultipleSensitivePropertyProtectionException(String message, Collection failedKeys) { + this(message, failedKeys, null); + } + + /** + * Constructs a new exception with the provided message and a unique set of the keys that caused the error. + * + * @param message the message + * @param failedKeys any failed keys + * @param cause the cause (which is saved for later retrieval by the + * {@link #getCause()} method). (A {@code null} value is + * permitted, and indicates that the cause is nonexistent or + * unknown.) + */ + public MultipleSensitivePropertyProtectionException(String message, Collection failedKeys, Throwable cause) { + super(message, cause); + this.failedKeys = new HashSet<>(failedKeys); + } + + public Set getFailedKeys() { + return this.failedKeys; + } + + @Override + public String toString() { + return "SensitivePropertyProtectionException for [" + StringUtils.join(this.failedKeys, ", ") + "]: " + getLocalizedMessage(); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java new file mode 100644 index 0000000000..48b90e5318 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java @@ -0,0 +1,461 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.properties; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.stream.Collectors; + +public class NiFiRegistryProperties extends Properties { + + private static final Logger logger = LoggerFactory.getLogger(NiFiRegistryProperties.class); + + // Keys + public static final String WEB_WAR_DIR = "nifi.registry.web.war.directory"; + public static final String WEB_HTTP_PORT = "nifi.registry.web.http.port"; + public static final String WEB_HTTP_HOST = "nifi.registry.web.http.host"; + public static final String WEB_HTTPS_PORT = "nifi.registry.web.https.port"; + public static final String WEB_HTTPS_HOST = "nifi.registry.web.https.host"; + public static final String WEB_WORKING_DIR = "nifi.registry.web.jetty.working.directory"; + public static final String WEB_THREADS = "nifi.registry.web.jetty.threads"; + public static final String WEB_SHOULD_SEND_SERVER_VERSION = "nifi.registry.web.should.send.server.version"; + + public static final String SECURITY_KEYSTORE = "nifi.registry.security.keystore"; + public static final String SECURITY_KEYSTORE_TYPE = "nifi.registry.security.keystoreType"; + public static final String SECURITY_KEYSTORE_PASSWD = "nifi.registry.security.keystorePasswd"; + public static final String SECURITY_KEY_PASSWD = "nifi.registry.security.keyPasswd"; + public static final String SECURITY_TRUSTSTORE = "nifi.registry.security.truststore"; + public static final String SECURITY_TRUSTSTORE_TYPE = "nifi.registry.security.truststoreType"; + public static final String SECURITY_TRUSTSTORE_PASSWD = "nifi.registry.security.truststorePasswd"; + public static final String SECURITY_NEED_CLIENT_AUTH = "nifi.registry.security.needClientAuth"; + public static final String SECURITY_AUTHORIZERS_CONFIGURATION_FILE = "nifi.registry.security.authorizers.configuration.file"; + public static final String SECURITY_AUTHORIZER = "nifi.registry.security.authorizer"; + public static final String SECURITY_IDENTITY_PROVIDERS_CONFIGURATION_FILE = "nifi.registry.security.identity.providers.configuration.file"; + public static final String SECURITY_IDENTITY_PROVIDER = "nifi.registry.security.identity.provider"; + public static final String SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX = "nifi.registry.security.identity.mapping.pattern."; + public static final String SECURITY_IDENTITY_MAPPING_VALUE_PREFIX = "nifi.registry.security.identity.mapping.value."; + public static final String SECURITY_IDENTITY_MAPPING_TRANSFORM_PREFIX = "nifi.registry.security.identity.mapping.transform."; + public static final String SECURITY_GROUP_MAPPING_PATTERN_PREFIX = "nifi.registry.security.group.mapping.pattern."; + public static final String SECURITY_GROUP_MAPPING_VALUE_PREFIX = "nifi.registry.security.group.mapping.value."; + public static final String SECURITY_GROUP_MAPPING_TRANSFORM_PREFIX = "nifi.registry.security.group.mapping.transform."; + + public static final String EXTENSION_DIR_PREFIX = "nifi.registry.extension.dir."; + + public static final String PROVIDERS_CONFIGURATION_FILE = "nifi.registry.providers.configuration.file"; + public static final String REGISTRY_ALIAS_CONFIGURATION_FILE = "nifi.registry.registry.alias.configuration.file"; + + public static final String EXTENSIONS_WORKING_DIR = "nifi.registry.extensions.working.directory"; + + // Original DB properties + public static final String DATABASE_DIRECTORY = "nifi.registry.db.directory"; + public static final String DATABASE_URL_APPEND = "nifi.registry.db.url.append"; + + // New style DB properties + public static final String DATABASE_URL = "nifi.registry.db.url"; + public static final String DATABASE_DRIVER_CLASS_NAME = "nifi.registry.db.driver.class"; + public static final String DATABASE_DRIVER_DIR = "nifi.registry.db.driver.directory"; + public static final String DATABASE_USERNAME = "nifi.registry.db.username"; + public static final String DATABASE_PASSWORD = "nifi.registry.db.password"; + public static final String DATABASE_MAX_CONNECTIONS = "nifi.registry.db.maxConnections"; + public static final String DATABASE_SQL_DEBUG = "nifi.registry.db.sql.debug"; + + // Kerberos properties + public static final String KERBEROS_KRB5_FILE = "nifi.registry.kerberos.krb5.file"; + public static final String KERBEROS_SPNEGO_PRINCIPAL = "nifi.registry.kerberos.spnego.principal"; + public static final String KERBEROS_SPNEGO_KEYTAB_LOCATION = "nifi.registry.kerberos.spnego.keytab.location"; + public static final String KERBEROS_SPNEGO_AUTHENTICATION_EXPIRATION = "nifi.registry.kerberos.spnego.authentication.expiration"; + public static final String KERBEROS_SERVICE_PRINCIPAL = "nifi.registry.kerberos.service.principal"; + public static final String KERBEROS_SERVICE_KEYTAB_LOCATION = "nifi.registry.kerberos.service.keytab.location"; + + // OIDC properties + public static final String SECURITY_USER_OIDC_DISCOVERY_URL = "nifi.registry.security.user.oidc.discovery.url"; + public static final String SECURITY_USER_OIDC_CONNECT_TIMEOUT = "nifi.registry.security.user.oidc.connect.timeout"; + public static final String SECURITY_USER_OIDC_READ_TIMEOUT = "nifi.registry.security.user.oidc.read.timeout"; + public static final String SECURITY_USER_OIDC_CLIENT_ID = "nifi.registry.security.user.oidc.client.id"; + public static final String SECURITY_USER_OIDC_CLIENT_SECRET = "nifi.registry.security.user.oidc.client.secret"; + public static final String SECURITY_USER_OIDC_PREFERRED_JWSALGORITHM = "nifi.registry.security.user.oidc.preferred.jwsalgorithm"; + public static final String SECURITY_USER_OIDC_ADDITIONAL_SCOPES = "nifi.registry.security.user.oidc.additional.scopes"; + public static final String SECURITY_USER_OIDC_CLAIM_IDENTIFYING_USER = "nifi.registry.security.user.oidc.claim.identifying.user"; + + // Revision Management Properties + public static final String REVISIONS_ENABLED = "nifi.registry.revisions.enabled"; + + // Defaults + public static final String DEFAULT_WEB_WORKING_DIR = "./work/jetty"; + public static final String DEFAULT_WAR_DIR = "./lib"; + public static final String DEFAULT_PROVIDERS_CONFIGURATION_FILE = "./conf/providers.xml"; + public static final String DEFAULT_REGISTRY_ALIAS_CONFIGURATION_FILE = "./conf/registry-aliases.xml"; + public static final String DEFAULT_SECURITY_AUTHORIZERS_CONFIGURATION_FILE = "./conf/authorizers.xml"; + public static final String DEFAULT_SECURITY_IDENTITY_PROVIDER_CONFIGURATION_FILE = "./conf/identity-providers.xml"; + public static final String DEFAULT_AUTHENTICATION_EXPIRATION = "12 hours"; + public static final String DEFAULT_EXTENSIONS_WORKING_DIR = "./work/extensions"; + public static final String DEFAULT_WEB_SHOULD_SEND_SERVER_VERSION = "true"; + public static final String DEFAULT_SECURITY_USER_OIDC_CONNECT_TIMEOUT = "5 secs"; + public static final String DEFAULT_SECURITY_USER_OIDC_READ_TIMEOUT = "5 secs"; + + public NiFiRegistryProperties() { + super(); + } + + public NiFiRegistryProperties(Map props) { + this.putAll(props); + } + + public int getWebThreads() { + int webThreads = 200; + try { + webThreads = Integer.parseInt(getProperty(WEB_THREADS)); + } catch (final NumberFormatException nfe) { + logger.warn(String.format("%s must be an integer value. Defaulting to %s", WEB_THREADS, webThreads)); + } + return webThreads; + } + + public Integer getPort() { + return getPropertyAsInteger(WEB_HTTP_PORT); + } + + public String getHttpHost() { + return getProperty(WEB_HTTP_HOST); + } + + public Integer getSslPort() { + return getPropertyAsInteger(WEB_HTTPS_PORT); + } + + public boolean isHTTPSConfigured() { + return getSslPort() != null; + } + + public boolean shouldSendServerVersion() { + return Boolean.parseBoolean(getProperty(WEB_SHOULD_SEND_SERVER_VERSION, DEFAULT_WEB_SHOULD_SEND_SERVER_VERSION)); + } + + public String getHttpsHost() { + return getProperty(WEB_HTTPS_HOST); + } + + public boolean getNeedClientAuth() { + boolean needClientAuth = true; + String rawNeedClientAuth = getProperty(SECURITY_NEED_CLIENT_AUTH); + if ("false".equalsIgnoreCase(rawNeedClientAuth)) { + needClientAuth = false; + } + return needClientAuth; + } + + public String getKeyStorePath() { + return getProperty(SECURITY_KEYSTORE); + } + + public String getKeyStoreType() { + return getProperty(SECURITY_KEYSTORE_TYPE); + } + + public String getKeyStorePassword() { + return getProperty(SECURITY_KEYSTORE_PASSWD); + } + + public String getKeyPassword() { + return getProperty(SECURITY_KEY_PASSWD); + } + + public String getTrustStorePath() { + return getProperty(SECURITY_TRUSTSTORE); + } + + public String getTrustStoreType() { + return getProperty(SECURITY_TRUSTSTORE_TYPE); + } + + public String getTrustStorePassword() { + return getProperty(SECURITY_TRUSTSTORE_PASSWD); + } + + public File getWarLibDirectory() { + return new File(getProperty(WEB_WAR_DIR, DEFAULT_WAR_DIR)); + } + + public File getWebWorkingDirectory() { + return new File(getProperty(WEB_WORKING_DIR, DEFAULT_WEB_WORKING_DIR)); + } + + public File getExtensionsWorkingDirectory() { + return new File(getProperty(EXTENSIONS_WORKING_DIR, DEFAULT_EXTENSIONS_WORKING_DIR)); + } + + public File getProvidersConfigurationFile() { + return getPropertyAsFile(PROVIDERS_CONFIGURATION_FILE, DEFAULT_PROVIDERS_CONFIGURATION_FILE); + } + + public File getRegistryAliasConfigurationFile() { + return getPropertyAsFile(REGISTRY_ALIAS_CONFIGURATION_FILE, DEFAULT_REGISTRY_ALIAS_CONFIGURATION_FILE); + } + + public String getLegacyDatabaseDirectory() { + return getProperty(DATABASE_DIRECTORY); + } + + public String getLegacyDatabaseUrlAppend() { + return getProperty(DATABASE_URL_APPEND); + } + + public String getDatabaseUrl() { + return getProperty(DATABASE_URL); + } + + public String getDatabaseDriverClassName() { + return getProperty(DATABASE_DRIVER_CLASS_NAME); + } + + public String getDatabaseDriverDirectory() { + return getProperty(DATABASE_DRIVER_DIR); + } + + public String getDatabaseUsername() { + return getProperty(DATABASE_USERNAME); + } + + public String getDatabasePassword() { + return getProperty(DATABASE_PASSWORD); + } + + public Integer getDatabaseMaxConnections() { + return getPropertyAsInteger(DATABASE_MAX_CONNECTIONS); + } + + public boolean getDatabaseSqlDebug() { + final String value = getProperty(DATABASE_SQL_DEBUG); + + if (StringUtils.isBlank(value)) { + return false; + } + + return "true".equalsIgnoreCase(value.trim()); + } + + public File getAuthorizersConfigurationFile() { + return getPropertyAsFile(SECURITY_AUTHORIZERS_CONFIGURATION_FILE, DEFAULT_SECURITY_AUTHORIZERS_CONFIGURATION_FILE); + } + + public File getIdentityProviderConfigurationFile() { + return getPropertyAsFile(SECURITY_IDENTITY_PROVIDERS_CONFIGURATION_FILE, DEFAULT_SECURITY_IDENTITY_PROVIDER_CONFIGURATION_FILE); + } + + public File getKerberosConfigurationFile() { + return getPropertyAsFile(KERBEROS_KRB5_FILE); + } + + public String getKerberosSpnegoAuthenticationExpiration() { + return getProperty(KERBEROS_SPNEGO_AUTHENTICATION_EXPIRATION, DEFAULT_AUTHENTICATION_EXPIRATION); + } + + public String getKerberosSpnegoPrincipal() { + return getPropertyAsTrimmedString(KERBEROS_SPNEGO_PRINCIPAL); + } + + public String getKerberosSpnegoKeytabLocation() { + return getPropertyAsTrimmedString(KERBEROS_SPNEGO_KEYTAB_LOCATION); + } + + public boolean isKerberosSpnegoSupportEnabled() { + return !StringUtils.isBlank(getKerberosSpnegoPrincipal()) && !StringUtils.isBlank(getKerberosSpnegoKeytabLocation()); + } + + public String getKerberosServicePrincipal() { + return getPropertyAsTrimmedString(KERBEROS_SERVICE_PRINCIPAL); + } + + public String getKerberosServiceKeytabLocation() { + return getPropertyAsTrimmedString(KERBEROS_SERVICE_KEYTAB_LOCATION); + } + + public Set getExtensionsDirs() { + final Set extensionDirs = new HashSet<>(); + stringPropertyNames().stream().filter(key -> key.startsWith(EXTENSION_DIR_PREFIX)).forEach(key -> extensionDirs.add(getProperty(key))); + return extensionDirs; + } + + public boolean areRevisionsEnabled() { + return Boolean.parseBoolean(getPropertyAsTrimmedString(REVISIONS_ENABLED)); + } + + /** + * Retrieves all known property keys. + * + * @return all known property keys + */ + public Set getPropertyKeys() { + Set propertyNames = new HashSet<>(); + Enumeration e = this.propertyNames(); + for (; e.hasMoreElements(); ){ + propertyNames.add((String) e.nextElement()); + } + + return propertyNames; + } + + // Helper functions for common ways of interpreting property values + + private String getPropertyAsTrimmedString(String key) { + final String value = getProperty(key); + if (!StringUtils.isBlank(value)) { + return value.trim(); + } else { + return null; + } + } + + private Integer getPropertyAsInteger(String key) { + final String value = getProperty(key); + if (StringUtils.isBlank(value)) { + return null; + } + try { + return Integer.parseInt(value); + } catch (final NumberFormatException nfe) { + throw new IllegalStateException(String.format("%s must be an integer value.", key)); + } + } + + private File getPropertyAsFile(String key) { + final String filePath = getProperty(key); + if (filePath != null && filePath.trim().length() > 0) { + return new File(filePath.trim()); + } else { + return null; + } + } + + private File getPropertyAsFile(String propertyKey, String defaultFileLocation) { + final String value = getProperty(propertyKey); + if (StringUtils.isBlank(value)) { + return new File(defaultFileLocation); + } else { + return new File(value); + } + } + + /** + * Returns true if the login identity provider has been configured. + * + * @return true if the login identity provider has been configured + */ + public boolean isLoginIdentityProviderEnabled() { + return !StringUtils.isBlank(getProperty(NiFiRegistryProperties.SECURITY_IDENTITY_PROVIDER)); + } + + /** + * Returns whether an OpenId Connect (OIDC) URL is set. + * + * @return whether an OpenId Connect URL is set + */ + public boolean isOidcEnabled() { + return !StringUtils.isBlank(getOidcDiscoveryUrl()); + } + + /** + * Returns the OpenId Connect (OIDC) URL. Null otherwise. + * + * @return OIDC discovery url + */ + public String getOidcDiscoveryUrl() { + return getProperty(SECURITY_USER_OIDC_DISCOVERY_URL); + } + + /** + * Returns the OpenId Connect connect timeout. Non null. + * + * @return OIDC connect timeout + */ + public String getOidcConnectTimeout() { + return getProperty(SECURITY_USER_OIDC_CONNECT_TIMEOUT, DEFAULT_SECURITY_USER_OIDC_CONNECT_TIMEOUT); + } + + /** + * Returns the OpenId Connect read timeout. Non null. + * + * @return OIDC read timeout + */ + public String getOidcReadTimeout() { + return getProperty(SECURITY_USER_OIDC_READ_TIMEOUT, DEFAULT_SECURITY_USER_OIDC_READ_TIMEOUT); + } + + /** + * Returns the OpenId Connect client id. + * + * @return OIDC client id + */ + public String getOidcClientId() { + return getProperty(SECURITY_USER_OIDC_CLIENT_ID); + } + + /** + * Returns the OpenId Connect client secret. + * + * @return OIDC client secret + */ + public String getOidcClientSecret() { + return getProperty(SECURITY_USER_OIDC_CLIENT_SECRET); + } + + /** + * Returns the preferred json web signature algorithm. May be null/blank. + * + * @return OIDC preferred json web signature algorithm + */ + public String getOidcPreferredJwsAlgorithm() { + return getProperty(SECURITY_USER_OIDC_PREFERRED_JWSALGORITHM); + } + + /** + * Returns additional scopes to be sent when requesting the access token from the IDP. + * + * @return List of additional scopes to be sent + */ + public List getOidcAdditionalScopes() { + String rawProperty = getProperty(SECURITY_USER_OIDC_ADDITIONAL_SCOPES, ""); + if (rawProperty.isEmpty()) { + return new ArrayList<>(); + } + List additionalScopes = Arrays.asList(rawProperty.split(",")); + return additionalScopes.stream().map(String::trim).collect(Collectors.toList()); + } + + /** + * Returns the claim to be used to identify a user. + * Claim must be requested by adding the scope for it. + * Default is 'email'. + * + * @return The claim to be used to identify the user. + */ + public String getOidcClaimIdentifyingUser() { + return getProperty(SECURITY_USER_OIDC_CLAIM_IDENTIFYING_USER, "email").trim(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoader.java b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoader.java new file mode 100644 index 0000000000..5ceffd1754 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoader.java @@ -0,0 +1,148 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.properties; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.crypto.Cipher; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.security.NoSuchAlgorithmException; + +public class NiFiRegistryPropertiesLoader { + + private static final Logger logger = LoggerFactory.getLogger(NiFiRegistryPropertiesLoader.class); + + private static final String RELATIVE_PATH = "conf/nifi-registry.properties"; + + private String keyHex; + + // Future enhancement: allow for external registration of new providers + private static SensitivePropertyProviderFactory sensitivePropertyProviderFactory; + + /** + * Returns an instance of the loader configured with the key. + *

+ *

+ * NOTE: This method is used reflectively by the process which starts NiFi + * so changes to it must be made in conjunction with that mechanism.

+ * + * @param keyHex the key used to encrypt any sensitive properties + * @return the configured loader + */ + public static NiFiRegistryPropertiesLoader withKey(String keyHex) { + NiFiRegistryPropertiesLoader loader = new NiFiRegistryPropertiesLoader(); + loader.setKeyHex(keyHex); + return loader; + } + + /** + * Sets the hexadecimal key used to unprotect properties encrypted with + * {@link AESSensitivePropertyProvider}. If the key has already been set, + * calling this method will throw a {@link RuntimeException}. + * + * @param keyHex the key in hexadecimal format + */ + public void setKeyHex(String keyHex) { + if (this.keyHex == null || this.keyHex.trim().isEmpty()) { + this.keyHex = keyHex; + } else { + throw new RuntimeException("Cannot overwrite an existing key"); + } + } + + private static String getDefaultProviderKey() { + try { + return "aes/gcm/" + (Cipher.getMaxAllowedKeyLength("AES") > 128 ? "256" : "128"); + } catch (NoSuchAlgorithmException e) { + return "aes/gcm/128"; + } + } + + private void initializeSensitivePropertyProviderFactory() { + sensitivePropertyProviderFactory = new AESSensitivePropertyProviderFactory(keyHex); + } + + private SensitivePropertyProvider getSensitivePropertyProvider() { + initializeSensitivePropertyProviderFactory(); + return sensitivePropertyProviderFactory.getProvider(); + } + + /** + * Returns a {@link ProtectedNiFiRegistryProperties} instance loaded from the + * serialized form in the file. Responsible for actually reading from disk + * and deserializing the properties. Returns a protected instance to allow + * for decryption operations. + * + * @param file the file containing serialized properties + * @return the ProtectedNiFiProperties instance + */ + ProtectedNiFiRegistryProperties readProtectedPropertiesFromDisk(File file) { + if (file == null || !file.exists() || !file.canRead()) { + String path = (file == null ? "missing file" : file.getAbsolutePath()); + logger.error("Cannot read from '{}' -- file is missing or not readable", path); + throw new IllegalArgumentException("NiFi Registry properties file missing or unreadable"); + } + + final NiFiRegistryProperties rawProperties = new NiFiRegistryProperties(); + try (final FileReader reader = new FileReader(file)) { + rawProperties.load(reader); + logger.info("Loaded {} properties from {}", rawProperties.size(), file.getAbsolutePath()); + ProtectedNiFiRegistryProperties protectedNiFiRegistryProperties = new ProtectedNiFiRegistryProperties(rawProperties); + return protectedNiFiRegistryProperties; + } catch (final IOException ioe) { + logger.error("Cannot load properties file due to " + ioe.getLocalizedMessage()); + throw new RuntimeException("Cannot load properties file due to " + ioe.getLocalizedMessage(), ioe); + } + } + + /** + * Returns an instance of {@link NiFiRegistryProperties} loaded from the provided + * {@link File}. If any properties are protected, will attempt to use the appropriate + * {@link SensitivePropertyProvider} to unprotect them transparently. + * + * @param file the File containing the serialized properties + * @return the NiFiProperties instance + */ + public NiFiRegistryProperties load(File file) { + ProtectedNiFiRegistryProperties protectedNiFiRegistryProperties = readProtectedPropertiesFromDisk(file); + if (protectedNiFiRegistryProperties.hasProtectedKeys()) { + protectedNiFiRegistryProperties.addSensitivePropertyProvider(getSensitivePropertyProvider()); + } + + return protectedNiFiRegistryProperties.getUnprotectedProperties(); + } + + /** + * Returns an instance of {@link NiFiRegistryProperties}. The path must not be empty. + * + * @param path the path of the serialized properties file + * @return the NiFiRegistryProperties instance + * @see NiFiRegistryPropertiesLoader#load(File) + */ + public NiFiRegistryProperties load(String path) { + if (path != null && !path.trim().isEmpty()) { + return load(new File(path)); + } else { + logger.error("Cannot read from '{}' -- path is null or empty", path); + throw new IllegalArgumentException("NiFi Registry properties file path empty or null"); + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/ProtectedNiFiRegistryProperties.java b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/ProtectedNiFiRegistryProperties.java new file mode 100644 index 0000000000..5debc4a1be --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/ProtectedNiFiRegistryProperties.java @@ -0,0 +1,528 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.properties; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static java.util.Arrays.asList; + +/** + * Wrapper class of {@link NiFiRegistryProperties} for intermediate phase when + * {@link NiFiRegistryPropertiesLoader} loads the raw properties file and performs + * unprotection activities before returning an instance of {@link NiFiRegistryProperties}. + */ +class ProtectedNiFiRegistryProperties { + private static final Logger logger = LoggerFactory.getLogger(ProtectedNiFiRegistryProperties.class); + + private NiFiRegistryProperties properties; + + private Map localProviderCache = new HashMap<>(); + + // Additional "sensitive" property key + public static final String ADDITIONAL_SENSITIVE_PROPERTIES_KEY = "nifi.registry.sensitive.props.additional.keys"; + + // Default list of "sensitive" property keys + public static final List DEFAULT_SENSITIVE_PROPERTIES = new ArrayList<>(asList( + NiFiRegistryProperties.SECURITY_KEY_PASSWD, + NiFiRegistryProperties.SECURITY_KEYSTORE_PASSWD, + NiFiRegistryProperties.SECURITY_TRUSTSTORE_PASSWD)); + + public ProtectedNiFiRegistryProperties() { + this(null); + } + + /** + * Creates an instance containing the provided {@link NiFiRegistryProperties}. + * + * @param props the NiFiProperties to contain + */ + public ProtectedNiFiRegistryProperties(NiFiRegistryProperties props) { + if (props == null) { + props = new NiFiRegistryProperties(); + } + this.properties = props; + logger.debug("Loaded {} properties (including {} protection schemes) into ProtectedNiFiProperties", + getPropertyKeysIncludingProtectionSchemes().size(), getProtectedPropertyKeys().size()); + } + + /** + * Retrieves the property value for the given property key. + * + * @param key the key of property value to lookup + * @return value of property at given key or null if not found + */ + // @Override + public String getProperty(String key) { + return getInternalNiFiProperties().getProperty(key); + } + + /** + * Returns the internal representation of the {@link NiFiRegistryProperties} -- protected + * or not as determined by the current state. No guarantee is made to the + * protection state of these properties. If the internal reference is null, a new + * {@link NiFiRegistryProperties} instance is created. + * + * @return the internal properties + */ + NiFiRegistryProperties getInternalNiFiProperties() { + if (this.properties == null) { + this.properties = new NiFiRegistryProperties(); + } + + return this.properties; + } + + /** + * Returns the number of properties in the NiFiRegistryProperties, + * excluding protection scheme properties. + * + *

+ * Example: + *

+ * key: E(value, key) + * key.protected: aes/gcm/256 + * key2: value2 + *

+ * would return size 2 + * + * @return the count of real properties + */ + int size() { + return getPropertyKeysExcludingProtectionSchemes().size(); + } + + /** + * Returns the complete set of property keys in the NiFiRegistryProperties, + * including any protection keys (i.e. 'x.y.z.protected'). + * + * @return the set of property keys + */ + Set getPropertyKeysIncludingProtectionSchemes() { + return getInternalNiFiProperties().getPropertyKeys(); + } + + /** + * Returns the set of property keys in the NiFiRegistryProperties, + * excluding any protection keys (i.e. 'x.y.z.protected'). + * + * @return the set of property keys + */ + Set getPropertyKeysExcludingProtectionSchemes() { + Set filteredKeys = getPropertyKeysIncludingProtectionSchemes(); + filteredKeys.removeIf(p -> p.endsWith(".protected")); + return filteredKeys; + } + + /** + * Splits a single string containing multiple property keys into a List. + * + * Delimited by ',' or ';' and ignores leading and trailing whitespace around delimiter. + * + * @param multipleProperties a single String containing multiple properties, i.e. + * "nifi.registry.property.1; nifi.registry.property.2, nifi.registry.property.3" + * @return a List containing the split and trimmed properties + */ + private static List splitMultipleProperties(String multipleProperties) { + if (multipleProperties == null || multipleProperties.trim().isEmpty()) { + return new ArrayList<>(0); + } else { + List properties = new ArrayList<>(asList(multipleProperties.split("\\s*[,;]\\s*"))); + for (int i = 0; i < properties.size(); i++) { + properties.set(i, properties.get(i).trim()); + } + return properties; + } + } + + /** + * Returns a list of the keys identifying "sensitive" properties. + * + * There is a default list, and additional keys can be provided in the + * {@code nifi.registry.sensitive.props.additional.keys} property in {@code nifi-registry.properties}. + * + * @return the list of sensitive property keys + */ + public List getSensitivePropertyKeys() { + String additionalPropertiesString = getProperty(ADDITIONAL_SENSITIVE_PROPERTIES_KEY); + if (additionalPropertiesString == null || additionalPropertiesString.trim().isEmpty()) { + return DEFAULT_SENSITIVE_PROPERTIES; + } else { + List additionalProperties = splitMultipleProperties(additionalPropertiesString); + /* Remove this key if it was accidentally provided as a sensitive key + * because we cannot protect it and read from it + */ + if (additionalProperties.contains(ADDITIONAL_SENSITIVE_PROPERTIES_KEY)) { + logger.warn("The key '{}' contains itself. This is poor practice and should be removed", ADDITIONAL_SENSITIVE_PROPERTIES_KEY); + additionalProperties.remove(ADDITIONAL_SENSITIVE_PROPERTIES_KEY); + } + additionalProperties.addAll(DEFAULT_SENSITIVE_PROPERTIES); + return additionalProperties; + } + } + + /** + * Returns a list of the keys identifying "sensitive" properties. There is a default list, + * and additional keys can be provided in the {@code nifi.sensitive.props.additional.keys} property in {@code nifi.properties}. + * + * @return the list of sensitive property keys + */ + public List getPopulatedSensitivePropertyKeys() { + List allSensitiveKeys = getSensitivePropertyKeys(); + return allSensitiveKeys.stream().filter(k -> StringUtils.isNotBlank(getProperty(k))).collect(Collectors.toList()); + } + + /** + * Returns true if any sensitive keys are protected. + * + * @return true if any key is protected; false otherwise + */ + public boolean hasProtectedKeys() { + List sensitiveKeys = getSensitivePropertyKeys(); + for (String k : sensitiveKeys) { + if (isPropertyProtected(k)) { + return true; + } + } + return false; + } + + /** + * Returns a Map of the keys identifying "sensitive" properties that are currently protected and the "protection" key for each. + * + * This may or may not include all properties marked as sensitive. + * + * @return the Map of protected property keys and the protection identifier for each + */ + public Map getProtectedPropertyKeys() { + List sensitiveKeys = getSensitivePropertyKeys(); + + Map traditionalProtectedProperties = new HashMap<>(); + for (String key : sensitiveKeys) { + String protection = getProperty(getProtectionKey(key)); + if (StringUtils.isNotBlank(protection) && StringUtils.isNotBlank(getProperty(key))) { + traditionalProtectedProperties.put(key, protection); + } + } + + return traditionalProtectedProperties; + } + + /** + * Returns the unique set of all protection schemes currently in use for this instance. + * + * @return the set of protection schemes + */ + public Set getProtectionSchemes() { + return new HashSet<>(getProtectedPropertyKeys().values()); + } + + /** + * Returns a percentage of the total number of populated properties marked as sensitive that are currently protected. + * + * @return the percent of sensitive properties marked as protected + */ + public int getPercentOfSensitivePropertiesProtected() { + return (int) Math.round(getProtectedPropertyKeys().size() / ((double) getPopulatedSensitivePropertyKeys().size()) * 100); + } + + /** + * Returns true if the property identified by this key is considered sensitive in this instance of {@code NiFiProperties}. + * Some properties are sensitive by default, while others can be specified by + * {@link ProtectedNiFiRegistryProperties#ADDITIONAL_SENSITIVE_PROPERTIES_KEY}. + * + * @param key the key + * @return true if it is sensitive + * @see ProtectedNiFiRegistryProperties#getSensitivePropertyKeys() + */ + public boolean isPropertySensitive(String key) { + // If the explicit check for ADDITIONAL_SENSITIVE_PROPERTIES_KEY is not here, this could loop infinitely + return key != null && !key.equals(ADDITIONAL_SENSITIVE_PROPERTIES_KEY) && getSensitivePropertyKeys().contains(key.trim()); + } + + /** + * Returns true if the property identified by this key is considered protected in this instance of {@code NiFiProperties}. + * The property value is protected if the key is sensitive and the sibling key of key.protected is present. + * + * @param key the key + * @return true if it is currently marked as protected + * @see ProtectedNiFiRegistryProperties#getSensitivePropertyKeys() + */ + public boolean isPropertyProtected(String key) { + return key != null && isPropertySensitive(key) && !StringUtils.isBlank(getProperty(getProtectionKey(key))); + } + + /** + * Returns the sibling property key which specifies the protection scheme for this key. + *

+ * Example: + *

+ * nifi.registry.sensitive.key=ABCXYZ + * nifi.registry.sensitive.key.protected=aes/gcm/256 + *

+ * nifi.registry.sensitive.key -> nifi.sensitive.key.protected + * + * @param key the key identifying the sensitive property + * @return the key identifying the protection scheme for the sensitive property + */ + public static String getProtectionKey(String key) { + if (key == null || key.isEmpty()) { + throw new IllegalArgumentException("Cannot find protection key for null key"); + } + + return key + ".protected"; + } + + /** + * Returns the unprotected {@link NiFiRegistryProperties} instance. If none of the + * properties loaded are marked as protected, it will simply pass through the + * internal instance. If any are protected, it will drop the protection scheme keys + * and translate each protected value (encrypted, HSM-retrieved, etc.) into the raw + * value and store it under the original key. + *

+ * If any property fails to unprotect, it will save that key and continue. After + * attempting all properties, it will throw an exception containing all failed + * properties. This is necessary because the order is not enforced, so all failed + * properties should be gathered together. + * + * @return the NiFiRegistryProperties instance with all raw values + * @throws SensitivePropertyProtectionException if there is a problem unprotecting one or more keys + */ + public NiFiRegistryProperties getUnprotectedProperties() throws SensitivePropertyProtectionException { + if (hasProtectedKeys()) { + logger.debug("There are {} protected properties of {} sensitive properties ({}%)", + getProtectedPropertyKeys().size(), + getPopulatedSensitivePropertyKeys().size(), + getPercentOfSensitivePropertiesProtected()); + + NiFiRegistryProperties unprotectedProperties = new NiFiRegistryProperties(); + + Set failedKeys = new HashSet<>(); + + for (String key : getPropertyKeysExcludingProtectionSchemes()) { + /* Three kinds of keys + * 1. protection schemes -- skip + * 2. protected keys -- unprotect and copy + * 3. normal keys -- copy over + */ + if (key.endsWith(".protected")) { + // Do nothing + } else if (isPropertyProtected(key)) { + try { + unprotectedProperties.setProperty(key, unprotectValue(key, getProperty(key))); + } catch (SensitivePropertyProtectionException e) { + logger.warn("Failed to unprotect '{}'", key, e); + failedKeys.add(key); + } + } else { + unprotectedProperties.setProperty(key, getProperty(key)); + } + } + + if (!failedKeys.isEmpty()) { + if (failedKeys.size() > 1) { + logger.warn("Combining {} failed keys [{}] into single exception", failedKeys.size(), StringUtils.join(failedKeys, ", ")); + throw new MultipleSensitivePropertyProtectionException("Failed to unprotect keys", failedKeys); + } else { + throw new SensitivePropertyProtectionException("Failed to unprotect key " + failedKeys.iterator().next()); + } + } + + return unprotectedProperties; + } else { + logger.debug("No protected properties"); + return getInternalNiFiProperties(); + } + } + + /** + * Registers a new {@link SensitivePropertyProvider}. This method will throw a {@link UnsupportedOperationException} if a provider is already registered for the protection scheme. + * + * @param sensitivePropertyProvider the provider + */ + void addSensitivePropertyProvider(SensitivePropertyProvider sensitivePropertyProvider) { + if (sensitivePropertyProvider == null) { + throw new IllegalArgumentException("Cannot add null SensitivePropertyProvider"); + } + + if (getSensitivePropertyProviders().containsKey(sensitivePropertyProvider.getIdentifierKey())) { + throw new UnsupportedOperationException("Cannot overwrite existing sensitive property provider registered for " + sensitivePropertyProvider.getIdentifierKey()); + } + + getSensitivePropertyProviders().put(sensitivePropertyProvider.getIdentifierKey(), sensitivePropertyProvider); + } + + private String getDefaultProtectionScheme() { + if (!getSensitivePropertyProviders().isEmpty()) { + List schemes = new ArrayList<>(getSensitivePropertyProviders().keySet()); + Collections.sort(schemes); + return schemes.get(0); + } else { + throw new IllegalStateException("No registered protection schemes"); + } + } + + /** + * Returns a new instance of {@link NiFiRegistryProperties} with all populated sensitive values protected by the default protection scheme. + * + * Plain non-sensitive values are copied directly. + * + * @return the protected properties in a {@link NiFiRegistryProperties} object + * @throws IllegalStateException if no protection schemes are registered + */ + NiFiRegistryProperties protectPlainProperties() { + try { + return protectPlainProperties(getDefaultProtectionScheme()); + } catch (IllegalStateException e) { + final String msg = "Cannot protect properties with default scheme if no protection schemes are registered"; + logger.warn(msg); + throw new IllegalStateException(msg, e); + } + } + + /** + * Returns a new instance of {@link NiFiRegistryProperties} with all populated sensitive values protected by the provided protection scheme. + * + * Plain non-sensitive values are copied directly. + * + * @param protectionScheme the identifier key of the {@link SensitivePropertyProvider} to use + * @return the protected properties in a {@link NiFiRegistryProperties} object + */ + NiFiRegistryProperties protectPlainProperties(String protectionScheme) { + SensitivePropertyProvider spp = getSensitivePropertyProvider(protectionScheme); + + NiFiRegistryProperties protectedProperties = new NiFiRegistryProperties(); + + // Copy over the plain keys + Set plainKeys = getPropertyKeysExcludingProtectionSchemes(); + plainKeys.removeAll(getSensitivePropertyKeys()); + for (String key : plainKeys) { + protectedProperties.setProperty(key, getInternalNiFiProperties().getProperty(key)); + } + + // Add the protected keys and the protection schemes + for (String key : getSensitivePropertyKeys()) { + final String plainValue = getProperty(key); + if (plainValue != null && !plainValue.trim().isEmpty()) { + final String protectedValue = spp.protect(plainValue); + protectedProperties.setProperty(key, protectedValue); + protectedProperties.setProperty(getProtectionKey(key), protectionScheme); + } + } + + return protectedProperties; + } + + /** + * Returns the number of properties that are marked as protected in the provided {@link NiFiRegistryProperties} instance + * without requiring external creation of a {@link ProtectedNiFiRegistryProperties} instance. + * + * @param plainProperties the instance to count protected properties + * @return the number of protected properties + */ + public static int countProtectedProperties(NiFiRegistryProperties plainProperties) { + return new ProtectedNiFiRegistryProperties(plainProperties).getProtectedPropertyKeys().size(); + } + + /** + * Returns the number of properties that are marked as sensitive in the provided {@link NiFiRegistryProperties} instance + * without requiring external creation of a {@link ProtectedNiFiRegistryProperties} instance. + * + * @param plainProperties the instance to count sensitive properties + * @return the number of sensitive properties + */ + public static int countSensitiveProperties(NiFiRegistryProperties plainProperties) { + return new ProtectedNiFiRegistryProperties(plainProperties).getSensitivePropertyKeys().size(); + } + + @Override + public String toString() { + final Set providers = getSensitivePropertyProviders().keySet(); + return new StringBuilder("ProtectedNiFiProperties instance with ") + .append(getPropertyKeysIncludingProtectionSchemes().size()) + .append(" properties (") + .append(getProtectedPropertyKeys().size()) + .append(" protected) and ") + .append(providers.size()) + .append(" sensitive property providers: ") + .append(StringUtils.join(providers, ", ")) + .toString(); + } + + /** + * Returns the local provider cache (null-safe) as a Map of protection schemes -> implementations. + * + * @return the map + */ + private Map getSensitivePropertyProviders() { + if (localProviderCache == null) { + localProviderCache = new HashMap<>(); + } + + return localProviderCache; + } + + private SensitivePropertyProvider getSensitivePropertyProvider(String protectionScheme) { + if (isProviderAvailable(protectionScheme)) { + return getSensitivePropertyProviders().get(protectionScheme); + } else { + throw new SensitivePropertyProtectionException("No provider available for " + protectionScheme); + } + } + + private boolean isProviderAvailable(String protectionScheme) { + return getSensitivePropertyProviders().containsKey(protectionScheme); + } + + /** + * If the value is protected, unprotects it and returns it. If not, returns the original value. + * + * @param key the retrieved property key + * @param retrievedValue the retrieved property value + * @return the unprotected value + */ + private String unprotectValue(String key, String retrievedValue) { + // Checks if the key is sensitive and marked as protected + if (isPropertyProtected(key)) { + final String protectionScheme = getProperty(getProtectionKey(key)); + + // No provider registered for this scheme, so just return the value + if (!isProviderAvailable(protectionScheme)) { + logger.warn("No provider available for {} so passing the protected {} value back", protectionScheme, key); + return retrievedValue; + } + + try { + SensitivePropertyProvider sensitivePropertyProvider = getSensitivePropertyProvider(protectionScheme); + return sensitivePropertyProvider.unprotect(retrievedValue); + } catch (SensitivePropertyProtectionException e) { + throw new SensitivePropertyProtectionException("Error unprotecting value for " + key, e.getCause()); + } + } + return retrievedValue; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProtectionException.java b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProtectionException.java new file mode 100644 index 0000000000..2ffa9022f8 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProtectionException.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.properties; + +public class SensitivePropertyProtectionException extends RuntimeException { + /** + * Constructs a new throwable with {@code null} as its detail message. + * The cause is not initialized, and may subsequently be initialized by a + * call to {@link #initCause}. + *

+ *

The {@link #fillInStackTrace()} method is called to initialize + * the stack trace data in the newly created throwable. + */ + public SensitivePropertyProtectionException() { + } + + /** + * Constructs a new throwable with the specified detail message. The + * cause is not initialized, and may subsequently be initialized by + * a call to {@link #initCause}. + *

+ *

The {@link #fillInStackTrace()} method is called to initialize + * the stack trace data in the newly created throwable. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + */ + public SensitivePropertyProtectionException(String message) { + super(message); + } + + /** + * Constructs a new throwable with the specified detail message and + * cause.

Note that the detail message associated with + * {@code cause} is not automatically incorporated in + * this throwable's detail message. + *

+ *

The {@link #fillInStackTrace()} method is called to initialize + * the stack trace data in the newly created throwable. + * + * @param message the detail message (which is saved for later retrieval + * by the {@link #getMessage()} method). + * @param cause the cause (which is saved for later retrieval by the + * {@link #getCause()} method). (A {@code null} value is + * permitted, and indicates that the cause is nonexistent or + * unknown.) + */ + public SensitivePropertyProtectionException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new throwable with the specified cause and a detail + * message of {@code (cause==null ? null : cause.toString())} (which + * typically contains the class and detail message of {@code cause}). + * This constructor is useful for throwables that are little more than + * wrappers for other throwables (for example, PrivilegedActionException). + *

+ *

The {@link #fillInStackTrace()} method is called to initialize + * the stack trace data in the newly created throwable. + * + * @param cause the cause (which is saved for later retrieval by the + * {@link #getCause()} method). (A {@code null} value is + * permitted, and indicates that the cause is nonexistent or + * unknown.) + */ + public SensitivePropertyProtectionException(Throwable cause) { + super(cause); + } + + @Override + public String toString() { + return "SensitivePropertyProtectionException: " + getLocalizedMessage(); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProvider.java new file mode 100644 index 0000000000..c0dd43c6b9 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProvider.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.properties; + +public interface SensitivePropertyProvider { + + /** + * Returns the name of the underlying implementation. + * + * @return the name of this sensitive property provider + */ + String getName(); + + /** + * Returns the key used to identify the provider implementation in {@code nifi.properties}. + * + * @return the key to persist in the sibling property + */ + String getIdentifierKey(); + + /** + * Returns the "protected" form of this value. This is a form which can safely be persisted in the {@code nifi.properties} file without compromising the value. + * An encryption-based provider would return a cipher text, while a remote-lookup provider could return a unique ID to retrieve the secured value. + * + * @param unprotectedValue the sensitive value + * @return the value to persist in the {@code nifi.properties} file + */ + String protect(String unprotectedValue) throws SensitivePropertyProtectionException; + + /** + * Returns the "unprotected" form of this value. This is the raw sensitive value which is used by the application logic. + * An encryption-based provider would decrypt a cipher text and return the plaintext, while a remote-lookup provider could retrieve the secured value. + * + * @param protectedValue the protected value read from the {@code nifi.properties} file + * @return the raw value to be used by the application + */ + String unprotect(String protectedValue) throws SensitivePropertyProtectionException; +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProviderFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProviderFactory.java new file mode 100644 index 0000000000..c9d4313e81 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProviderFactory.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.properties; + +public interface SensitivePropertyProviderFactory { + + SensitivePropertyProvider getProvider(); + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMapping.java b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMapping.java new file mode 100644 index 0000000000..cedec8b549 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMapping.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.properties.util; + +import java.util.regex.Pattern; + +import static org.apache.nifi.registry.properties.util.IdentityMapping.Transform.NONE; + +/** + * Holder to pass around the key, pattern, and replacement from an identity mapping in NiFiProperties. + */ +public class IdentityMapping { + + public enum Transform { + NONE, + UPPER, + LOWER + } + + private final String key; + private final Pattern pattern; + private final String replacementValue; + private final Transform transform; + + public IdentityMapping(String key, Pattern pattern, String replacementValue) { + this(key, pattern, replacementValue, NONE); + } + + public IdentityMapping(String key, Pattern pattern, String replacementValue, Transform transform) { + this.key = key; + this.pattern = pattern; + this.replacementValue = replacementValue; + this.transform = transform; + } + + public String getKey() { + return key; + } + + public Pattern getPattern() { + return pattern; + } + + public String getReplacementValue() { + return replacementValue; + } + + public Transform getTransform() { + return transform; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMappingUtil.java b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMappingUtil.java new file mode 100644 index 0000000000..d7c2709183 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMappingUtil.java @@ -0,0 +1,210 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.properties.util; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.properties.util.IdentityMapping.Transform; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.apache.nifi.registry.properties.NiFiRegistryProperties.SECURITY_GROUP_MAPPING_PATTERN_PREFIX; +import static org.apache.nifi.registry.properties.NiFiRegistryProperties.SECURITY_GROUP_MAPPING_TRANSFORM_PREFIX; +import static org.apache.nifi.registry.properties.NiFiRegistryProperties.SECURITY_GROUP_MAPPING_VALUE_PREFIX; +import static org.apache.nifi.registry.properties.NiFiRegistryProperties.SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX; +import static org.apache.nifi.registry.properties.NiFiRegistryProperties.SECURITY_IDENTITY_MAPPING_TRANSFORM_PREFIX; +import static org.apache.nifi.registry.properties.NiFiRegistryProperties.SECURITY_IDENTITY_MAPPING_VALUE_PREFIX; + +public class IdentityMappingUtil { + + private static final Logger LOGGER = LoggerFactory.getLogger(IdentityMappingUtil.class); + private static final Pattern backReferencePattern = Pattern.compile("\\$(\\d+)"); + + /** + * Builds the identity mappings from NiFiRegistryProperties. + * + * @param properties the NiFiRegistryProperties instance + * @return a list of identity mappings + */ + public static List getIdentityMappings(final NiFiRegistryProperties properties) { + return getMappings( + properties, + SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX, + SECURITY_IDENTITY_MAPPING_VALUE_PREFIX, + SECURITY_IDENTITY_MAPPING_TRANSFORM_PREFIX, + () -> "Identity"); + } + + /** + * Buils the group mappings from NiFiProperties. + * + * @param properties the NiFiProperties instance + * @return a list of group mappings + */ + public static List getGroupMappings(final NiFiRegistryProperties properties) { + return getMappings( + properties, + SECURITY_GROUP_MAPPING_PATTERN_PREFIX, + SECURITY_GROUP_MAPPING_VALUE_PREFIX, + SECURITY_GROUP_MAPPING_TRANSFORM_PREFIX, + () -> "Group"); + } + + /** + * Builds the identity mappings from NiFiRegistryProperties. + * + * @param properties the NiFiRegistryProperties instance + * @return a list of identity mappings + */ + private static List getMappings(final NiFiRegistryProperties properties, final String patternPrefix, + final String valuePrefix, final String transformPrefix, final Supplier getSubject) { + final List mappings = new ArrayList<>(); + + // go through each property + for (String propertyName : properties.getPropertyKeys()) { + if (StringUtils.startsWith(propertyName, patternPrefix)) { + final String key = StringUtils.substringAfter(propertyName, patternPrefix); + final String identityPattern = properties.getProperty(propertyName); + + if (StringUtils.isBlank(identityPattern)) { + LOGGER.warn("{} Mapping property {} was found, but was empty", new Object[] {getSubject.get(), propertyName}); + continue; + } + + final String identityValueProperty = valuePrefix + key; + final String identityValue = properties.getProperty(identityValueProperty); + + if (StringUtils.isBlank(identityValue)) { + LOGGER.warn("{} Mapping property {} was found, but corresponding value {} was not found", + new Object[]{propertyName, identityValueProperty}); + continue; + } + + final String identityTransformProperty = transformPrefix + key; + String rawIdentityTransform = properties.getProperty(identityTransformProperty); + + if (StringUtils.isBlank(rawIdentityTransform)) { + LOGGER.debug("{} Mapping property {} was found, but no transform was present. Using NONE.", new Object[] {getSubject.get(), propertyName}); + rawIdentityTransform = IdentityMapping.Transform.NONE.name(); + } + + final Transform identityTransform; + try { + identityTransform = Transform.valueOf(rawIdentityTransform); + } catch (final IllegalArgumentException iae) { + LOGGER.warn("{} Mapping property {} was found, but corresponding transform {} was not valid. Allowed values {}", + new Object[] {getSubject.get(), propertyName, rawIdentityTransform, StringUtils.join(Transform.values(), ", ")}); + continue; + } + + final IdentityMapping identityMapping = new IdentityMapping(key, Pattern.compile(identityPattern), identityValue, identityTransform); + mappings.add(identityMapping); + + LOGGER.debug("Found {} Mapping with key = {}, pattern = {}, value = {}, transform = {}", + new Object[] {getSubject.get(), key, identityPattern, identityValue, rawIdentityTransform}); + } + } + + // sort the list by the key so users can control the ordering in nifi-registry.properties + Collections.sort(mappings, new Comparator() { + @Override + public int compare(IdentityMapping m1, IdentityMapping m2) { + return m1.getKey().compareTo(m2.getKey()); + } + }); + + return mappings; + } + + /** + * Checks the given identity against each provided mapping and performs the mapping using the first one that matches. + * If none match then the identity is returned as is. + * + * @param identity the identity to map + * @param mappings the mappings + * @return the mapped identity, or the same identity if no mappings matched + */ + public static String mapIdentity(final String identity, List mappings) { + for (IdentityMapping mapping : mappings) { + Matcher m = mapping.getPattern().matcher(identity); + if (m.matches()) { + final String pattern = mapping.getPattern().pattern(); + final String replacementValue = escapeLiteralBackReferences(mapping.getReplacementValue(), m.groupCount()); + final String replacement = identity.replaceAll(pattern, replacementValue); + + if (Transform.UPPER.equals(mapping.getTransform())) { + return replacement.toUpperCase(); + } else if (Transform.LOWER.equals(mapping.getTransform())) { + return replacement.toLowerCase(); + } else { + return replacement; + } + } + } + + return identity; + } + + // If we find a back reference that is not valid, then we will treat it as a literal string. For example, if we have 3 capturing + // groups and the Replacement Value has the value is "I owe $8 to him", then we want to treat the $8 as a literal "$8", rather + // than attempting to use it as a back reference. + private static String escapeLiteralBackReferences(final String unescaped, final int numCapturingGroups) { + if (numCapturingGroups == 0) { + return unescaped; + } + + String value = unescaped; + final Matcher backRefMatcher = backReferencePattern.matcher(value); + while (backRefMatcher.find()) { + final String backRefNum = backRefMatcher.group(1); + if (backRefNum.startsWith("0")) { + continue; + } + final int originalBackRefIndex = Integer.parseInt(backRefNum); + int backRefIndex = originalBackRefIndex; + + // if we have a replacement value like $123, and we have less than 123 capturing groups, then + // we want to truncate the 3 and use capturing group 12; if we have less than 12 capturing groups, + // then we want to truncate the 2 and use capturing group 1; if we don't have a capturing group then + // we want to truncate the 1 and get 0. + while (backRefIndex > numCapturingGroups && backRefIndex >= 10) { + backRefIndex /= 10; + } + + if (backRefIndex > numCapturingGroups) { + final StringBuilder sb = new StringBuilder(value.length() + 1); + final int groupStart = backRefMatcher.start(1); + + sb.append(value.substring(0, groupStart - 1)); + sb.append("\\"); + sb.append(value.substring(groupStart - 1)); + value = sb.toString(); + } + } + + return value; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/BootstrapFileCryptoKeyProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/BootstrapFileCryptoKeyProvider.java new file mode 100644 index 0000000000..191b5e28bf --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/BootstrapFileCryptoKeyProvider.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.crypto; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +/** + * An implementation of {@link CryptoKeyProvider} that loads the key from disk every time it is needed. + * + * The persistence-backing of the key is in the bootstrap.conf file, which must be provided to the + * constructor of this class. + * + * As key access for sensitive value decryption is only used a few times during server initialization, + * this implementation trades efficiency for security by only keeping the key in memory with an + * in-scope reference for a brief period of time (assuming callers do not maintain an in-scope reference). + * + * @see CryptoKeyProvider + */ +public class BootstrapFileCryptoKeyProvider implements CryptoKeyProvider { + + private static final Logger logger = LoggerFactory.getLogger(BootstrapFileCryptoKeyProvider.class); + + private final String bootstrapFile; + + /** + * Construct a new instance backed by the contents of a bootstrap.conf file. + * + * @param bootstrapFilePath The path to the bootstrap.conf file for this instance of NiFi Registry. + * Must not be null. + */ + public BootstrapFileCryptoKeyProvider(final String bootstrapFilePath) { + if (bootstrapFilePath == null) { + throw new IllegalArgumentException(BootstrapFileCryptoKeyProvider.class.getSimpleName() + " cannot be initialized with null bootstrap file path."); + } + this.bootstrapFile = bootstrapFilePath; + } + + /** + * @return The bootstrap file path that backs this provider instance. + */ + public String getBootstrapFile() { + return bootstrapFile; + } + + @Override + public String getKey() throws MissingCryptoKeyException { + try { + return CryptoKeyLoader.extractKeyFromBootstrapFile(this.bootstrapFile); + } catch (IOException ioe) { + final String errMsg = "Loading the master crypto key from bootstrap file '" + bootstrapFile + "' failed due to IOException."; + logger.warn(errMsg); + throw new MissingCryptoKeyException(errMsg, ioe); + } + + } + + @Override + public String toString() { + return "BootstrapFileCryptoKeyProvider{" + + "bootstrapFile='" + bootstrapFile + '\'' + + '}'; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyLoader.java b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyLoader.java new file mode 100644 index 0000000000..d828773f34 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyLoader.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.crypto; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Optional; +import java.util.stream.Stream; + +public class CryptoKeyLoader { + + private static final Logger logger = LoggerFactory.getLogger(CryptoKeyLoader.class); + + private static final String BOOTSTRAP_KEY_PREFIX = "nifi.registry.bootstrap.sensitive.key="; + + /** + * Returns the key (if any) used to encrypt sensitive properties. + * The key extracted from the bootstrap.conf file at the specified location. + * + * @param bootstrapPath the path to the bootstrap file + * @return the key in hexadecimal format, or {@link CryptoKeyProvider#EMPTY_KEY} if the key is null or empty + * @throws IOException if the file is not readable + */ + public static String extractKeyFromBootstrapFile(String bootstrapPath) throws IOException { + File bootstrapFile; + if (StringUtils.isBlank(bootstrapPath)) { + logger.error("Cannot read from bootstrap.conf file to extract encryption key; location not specified"); + throw new IOException("Cannot read from bootstrap.conf without file location"); + } else { + bootstrapFile = new File(bootstrapPath); + } + + String keyValue; + if (bootstrapFile.exists() && bootstrapFile.canRead()) { + try (Stream stream = Files.lines(Paths.get(bootstrapFile.getAbsolutePath()))) { + Optional keyLine = stream.filter(l -> l.startsWith(BOOTSTRAP_KEY_PREFIX)).findFirst(); + if (keyLine.isPresent()) { + keyValue = keyLine.get().split("=", 2)[1]; + keyValue = checkHexKey(keyValue); + } else { + keyValue = CryptoKeyProvider.EMPTY_KEY; + } + } catch (IOException e) { + logger.error("Cannot read from bootstrap.conf file at {} to extract encryption key", bootstrapFile.getAbsolutePath()); + throw new IOException("Cannot read from bootstrap.conf", e); + } + } else { + logger.error("Cannot read from bootstrap.conf file at {} to extract encryption key -- file is missing or permissions are incorrect", bootstrapFile.getAbsolutePath()); + throw new IOException("Cannot read from bootstrap.conf"); + } + + if (CryptoKeyProvider.EMPTY_KEY.equals(keyValue)) { + logger.info("No encryption key present in the bootstrap.conf file at {}", bootstrapFile.getAbsolutePath()); + } + + return keyValue; + } + + private static String checkHexKey(String input) { + if (input == null || input.trim().isEmpty()) { + logger.debug("Checking the hex key value that was loaded determined the key is empty."); + return CryptoKeyProvider.EMPTY_KEY; + } + return input; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyProvider.java new file mode 100644 index 0000000000..bab8d7c7f4 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyProvider.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.crypto; + +/** + * A simple interface that wraps a key that can be used for encryption and decryption. + * This allows for more flexibility with the lifecycle of keys and how other classes + * can declare dependencies for keys, by depending on a CryptoKeyProvider that will provided + * at runtime. + */ +public interface CryptoKeyProvider { + + /** + * A string literal that indicates the contents of a key are empty. + * Can also be used in contexts that a null key is undesirable. + */ + String EMPTY_KEY = ""; + + /** + * @return The crypto key known to this CryptoKeyProvider instance in hexadecimal format, or + * {@link #EMPTY_KEY} if the key is empty. + * @throws MissingCryptoKeyException if the key cannot be provided or determined for any reason. + * If the key is known to be empty, {@link #EMPTY_KEY} will be returned and a + * CryptoKeyMissingException will not be thrown + */ + String getKey() throws MissingCryptoKeyException; + + /** + * @return A boolean indicating if the key value held by this CryptoKeyProvider is empty, + * such as 'null' or empty string. + */ + default boolean isEmpty() { + String key; + try { + key = getKey(); + } catch (MissingCryptoKeyException e) { + return true; + } + return EMPTY_KEY.equals(key); + } + + /** + * A string representation of this CryptoKeyProvider instance. + *

+ *

+ * Note: Implementations of this interface should take care not to leak sensitive + * key material in any strings they emmit, including in the toString implementation. + * + * @return A string representation of this CryptoKeyProvider instance. + */ + @Override + public String toString(); + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/MissingCryptoKeyException.java b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/MissingCryptoKeyException.java new file mode 100644 index 0000000000..dbc3752c99 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/MissingCryptoKeyException.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.crypto; + +/** + * An exception type used by a {@link CryptoKeyProvider} when a request for the key + * cannot be fulfilled for any reason. + * + * @see CryptoKeyProvider + */ +public class MissingCryptoKeyException extends Exception { + + public MissingCryptoKeyException() { + super(); + } + + public MissingCryptoKeyException(String message) { + super(message); + } + + public MissingCryptoKeyException(String message, Throwable cause) { + super(message, cause); + } + + public MissingCryptoKeyException(Throwable cause) { + super(cause); + } + + protected MissingCryptoKeyException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactoryTest.groovy b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactoryTest.groovy new file mode 100644 index 0000000000..0d1d5e29be --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactoryTest.groovy @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.properties + +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.junit.* +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import javax.crypto.Cipher +import java.security.Security + +@RunWith(JUnit4.class) +class AESSensitivePropertyProviderFactoryTest extends GroovyTestCase { + private static final Logger logger = LoggerFactory.getLogger(AESSensitivePropertyProviderFactoryTest.class) + + private static final String KEY_HEX_128 = "0123456789ABCDEFFEDCBA9876543210" + private static final String KEY_HEX_256 = KEY_HEX_128 * 2 + + @BeforeClass + public static void setUpOnce() throws Exception { + Security.addProvider(new BouncyCastleProvider()) + + logger.metaClass.methodMissing = { String name, args -> + logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}") + } + } + + @Before + public void setUp() throws Exception { + } + + @After + public void tearDown() throws Exception { + } + + @Test + public void testShouldGetProviderWithKey() throws Exception { + // Arrange + SensitivePropertyProviderFactory factory = new AESSensitivePropertyProviderFactory(KEY_HEX_128) + + // Act + SensitivePropertyProvider provider = factory.getProvider() + + // Assert + assert provider instanceof AESSensitivePropertyProvider + assert provider.@key + assert provider.@cipher + } + + @Test + public void testShouldGetProviderWith256BitKey() throws Exception { + // Arrange + Assume.assumeTrue("JCE unlimited strength crypto policy must be installed for this test", Cipher.getMaxAllowedKeyLength("AES") > 128) + SensitivePropertyProviderFactory factory = new AESSensitivePropertyProviderFactory(KEY_HEX_256) + + // Act + SensitivePropertyProvider provider = factory.getProvider() + + // Assert + assert provider instanceof AESSensitivePropertyProvider + assert provider.@key + assert provider.@cipher + } +} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderTest.groovy b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderTest.groovy new file mode 100644 index 0000000000..bad659feeb --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderTest.groovy @@ -0,0 +1,471 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.properties + +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.util.encoders.Hex +import org.junit.* +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec +import java.nio.charset.StandardCharsets +import java.security.SecureRandom +import java.security.Security + +@RunWith(JUnit4.class) +class AESSensitivePropertyProviderTest extends GroovyTestCase { + private static final Logger logger = LoggerFactory.getLogger(AESSensitivePropertyProviderTest.class) + + private static final String KEY_128_HEX = "0123456789ABCDEFFEDCBA9876543210" + private static final String KEY_256_HEX = KEY_128_HEX * 2 + private static final int IV_LENGTH = AESSensitivePropertyProvider.getIvLength() + + private static final List KEY_SIZES = getAvailableKeySizes() + + private static final SecureRandom secureRandom = new SecureRandom() + + private static final Base64.Encoder encoder = Base64.encoder + private static final Base64.Decoder decoder = Base64.decoder + + @BeforeClass + static void setUpOnce() throws Exception { + Security.addProvider(new BouncyCastleProvider()) + + logger.metaClass.methodMissing = { String name, args -> + logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}") + } + } + + @Before + void setUp() throws Exception { + + } + + @After + void tearDown() throws Exception { + + } + + private static Cipher getCipher(boolean encrypt = true, int keySize = 256, byte[] iv = [0x00] * IV_LENGTH) { + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding") + String key = getKeyOfSize(keySize) + cipher.init((encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE) as int, new SecretKeySpec(Hex.decode(key), "AES"), new IvParameterSpec(iv)) + logger.setup("Initialized a cipher in ${encrypt ? "encrypt" : "decrypt"} mode with a key of length ${keySize} bits") + cipher + } + + private static String getKeyOfSize(int keySize = 256) { + switch (keySize) { + case 128: + return KEY_128_HEX + case 192: + case 256: + if (Cipher.getMaxAllowedKeyLength("AES") < keySize) { + throw new IllegalArgumentException("The JCE unlimited strength cryptographic jurisdiction policies are not installed, so the max key size is 128 bits") + } + return KEY_256_HEX[0.. getAvailableKeySizes() { + if (Cipher.getMaxAllowedKeyLength("AES") > 128) { + [128, 192, 256] + } else { + [128] + } + } + + private static String manipulateString(String input, int start = 0, int end = input?.length()) { + if ((input[start..end] as List).unique().size() == 1) { + throw new IllegalArgumentException("Can't manipulate a String where the entire range is identical [${input[start..end]}]") + } + List shuffled = input[start..end] as List + Collections.shuffle(shuffled) + String reconstituted = input[0.. CIPHER_TEXTS = KEY_SIZES.collectEntries { int keySize -> + SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize))) + logger.info("Initialized ${spp.name} with key size ${keySize}") + [(keySize): spp.protect(PLAINTEXT)] + } + CIPHER_TEXTS.each { ks, ct -> logger.info("Encrypted for ${ks} length key: ${ct}") } + + // Assert + + // The IV generation is part of #protect, so the expected cipher text values must be generated after #protect has run + Map decryptionCiphers = CIPHER_TEXTS.collectEntries { int keySize, String cipherText -> + // The 12 byte IV is the first 16 Base64-encoded characters of the "complete" cipher text + byte[] iv = decoder.decode(cipherText[0..<16]) + [(keySize): getCipher(false, keySize, iv)] + } + Map plaintexts = decryptionCiphers.collectEntries { Map.Entry e -> + String cipherTextWithoutIVAndDelimiter = CIPHER_TEXTS[e.key][18..-1] + String plaintext = new String(e.value.doFinal(decoder.decode(cipherTextWithoutIVAndDelimiter)), StandardCharsets.UTF_8) + [(e.key): plaintext] + } + CIPHER_TEXTS.each { key, ct -> logger.expected("Cipher text for ${key} length key: ${ct}") } + + assert plaintexts.every { int ks, String pt -> pt == PLAINTEXT } + } + + @Test + void testShouldHandleProtectEmptyValue() throws Exception { + final List EMPTY_PLAINTEXTS = ["", " ", null] + + // Act + KEY_SIZES.collectEntries { int keySize -> + SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize))) + logger.info("Initialized ${spp.name} with key size ${keySize}") + EMPTY_PLAINTEXTS.each { String emptyPlaintext -> + def msg = shouldFail(IllegalArgumentException) { + spp.protect(emptyPlaintext) + } + logger.expected("${msg} for keySize ${keySize} and plaintext [${emptyPlaintext}]") + + // Assert + assert msg == "Cannot encrypt an empty value" + } + } + } + + @Test + void testShouldUnprotectValue() throws Exception { + // Arrange + final String PLAINTEXT = "This is a plaintext value" + + Map encryptionCiphers = KEY_SIZES.collectEntries { int keySize -> + byte[] iv = new byte[IV_LENGTH] + secureRandom.nextBytes(iv) + [(keySize): getCipher(true, keySize, iv)] + } + + Map CIPHER_TEXTS = encryptionCiphers.collectEntries { Map.Entry e -> + String iv = encoder.encodeToString(e.value.getIV()) + String cipherText = encoder.encodeToString(e.value.doFinal(PLAINTEXT.getBytes(StandardCharsets.UTF_8))) + [(e.key): "${iv}||${cipherText}"] + } + CIPHER_TEXTS.each { key, ct -> logger.expected("Cipher text for ${key} length key: ${ct}") } + + // Act + Map plaintexts = CIPHER_TEXTS.collectEntries { int keySize, String cipherText -> + SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize))) + logger.info("Initialized ${spp.name} with key size ${keySize}") + [(keySize): spp.unprotect(cipherText)] + } + plaintexts.each { ks, pt -> logger.info("Decrypted for ${ks} length key: ${pt}") } + + // Assert + assert plaintexts.every { int ks, String pt -> pt == PLAINTEXT } + } + + /** + * Tests inputs where the entire String is empty/blank space/{@code null}. + * + * @throws Exception + */ + @Test + void testShouldHandleUnprotectEmptyValue() throws Exception { + // Arrange + final List EMPTY_CIPHER_TEXTS = ["", " ", null] + + // Act + KEY_SIZES.each { int keySize -> + SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize))) + logger.info("Initialized ${spp.name} with key size ${keySize}") + EMPTY_CIPHER_TEXTS.each { String emptyCipherText -> + def msg = shouldFail(IllegalArgumentException) { + spp.unprotect(emptyCipherText) + } + logger.expected("${msg} for keySize ${keySize} and cipher text [${emptyCipherText}]") + + // Assert + assert msg == "Cannot decrypt a cipher text shorter than ${AESSensitivePropertyProvider.minCipherTextLength} chars".toString() + } + } + } + + @Test + void testShouldUnprotectValueWithWhitespace() throws Exception { + // Arrange + final String PLAINTEXT = "This is a plaintext value" + + Map encryptionCiphers = KEY_SIZES.collectEntries { int keySize -> + byte[] iv = new byte[IV_LENGTH] + secureRandom.nextBytes(iv) + [(keySize): getCipher(true, keySize, iv)] + } + + Map CIPHER_TEXTS = encryptionCiphers.collectEntries { Map.Entry e -> + String iv = encoder.encodeToString(e.value.getIV()) + String cipherText = encoder.encodeToString(e.value.doFinal(PLAINTEXT.getBytes(StandardCharsets.UTF_8))) + [(e.key): "${iv}||${cipherText}"] + } + CIPHER_TEXTS.each { key, ct -> logger.expected("Cipher text for ${key} length key: ${ct}") } + + // Act + Map plaintexts = CIPHER_TEXTS.collectEntries { int keySize, String cipherText -> + SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize))) + logger.info("Initialized ${spp.name} with key size ${keySize}") + [(keySize): spp.unprotect("\t" + cipherText + "\n")] + } + plaintexts.each { ks, pt -> logger.info("Decrypted for ${ks} length key: ${pt}") } + + // Assert + assert plaintexts.every { int ks, String pt -> pt == PLAINTEXT } + } + + @Test + void testShouldHandleUnprotectMalformedValue() throws Exception { + // Arrange + final String PLAINTEXT = "This is a plaintext value" + + // Act + KEY_SIZES.each { int keySize -> + SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize))) + logger.info("Initialized ${spp.name} with key size ${keySize}") + String cipherText = spp.protect(PLAINTEXT) + // Swap two characters in the cipher text + final String MALFORMED_CIPHER_TEXT = manipulateString(cipherText, 25, 28) + logger.info("Manipulated ${cipherText} to\n${MALFORMED_CIPHER_TEXT.padLeft(163)}") + + def msg = shouldFail(SensitivePropertyProtectionException) { + spp.unprotect(MALFORMED_CIPHER_TEXT) + } + logger.expected("${msg} for keySize ${keySize} and cipher text [${MALFORMED_CIPHER_TEXT}]") + + // Assert + assert msg == "Error decrypting a protected value" + } + } + + @Test + void testShouldHandleUnprotectMissingIV() throws Exception { + // Arrange + final String PLAINTEXT = "This is a plaintext value" + + // Act + KEY_SIZES.each { int keySize -> + SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize))) + logger.info("Initialized ${spp.name} with key size ${keySize}") + String cipherText = spp.protect(PLAINTEXT) + + // Remove the IV from the "complete" cipher text + final String MISSING_IV_CIPHER_TEXT = cipherText[18..-1] + logger.info("Manipulated ${cipherText} to\n${MISSING_IV_CIPHER_TEXT.padLeft(172)}") + + def msg = shouldFail(IllegalArgumentException) { + spp.unprotect(MISSING_IV_CIPHER_TEXT) + } + logger.expected("${msg} for keySize ${keySize} and cipher text [${MISSING_IV_CIPHER_TEXT}]") + + // Remove the IV from the "complete" cipher text but keep the delimiter + final String MISSING_IV_CIPHER_TEXT_WITH_DELIMITER = cipherText[16..-1] + logger.info("Manipulated ${cipherText} to\n${MISSING_IV_CIPHER_TEXT_WITH_DELIMITER.padLeft(172)}") + + def msgWithDelimiter = shouldFail(IllegalArgumentException) { + spp.unprotect(MISSING_IV_CIPHER_TEXT_WITH_DELIMITER) + } + logger.expected("${msgWithDelimiter} for keySize ${keySize} and cipher text [${MISSING_IV_CIPHER_TEXT_WITH_DELIMITER}]") + + // Assert + assert msg == "The cipher text does not contain the delimiter || -- it should be of the form Base64(IV) || Base64(cipherText)" + + // Assert + assert msgWithDelimiter == "The IV (0 bytes) must be at least 12 bytes" + } + } + + /** + * Tests inputs which have a valid IV and delimiter but no "cipher text". + * + * @throws Exception + */ + @Test + void testShouldHandleUnprotectEmptyCipherText() throws Exception { + // Arrange + final String IV_AND_DELIMITER = "${encoder.encodeToString("Bad IV value".getBytes(StandardCharsets.UTF_8))}||" + logger.info("IV and delimiter: ${IV_AND_DELIMITER}") + + final List EMPTY_CIPHER_TEXTS = ["", " ", "\n"].collect { "${IV_AND_DELIMITER}${it}" } + + // Act + KEY_SIZES.each { int keySize -> + SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize))) + logger.info("Initialized ${spp.name} with key size ${keySize}") + EMPTY_CIPHER_TEXTS.each { String emptyCipherText -> + def msg = shouldFail(IllegalArgumentException) { + spp.unprotect(emptyCipherText) + } + logger.expected("${msg} for keySize ${keySize} and cipher text [${emptyCipherText}]") + + // Assert + assert msg == "Cannot decrypt a cipher text shorter than ${AESSensitivePropertyProvider.minCipherTextLength} chars".toString() + } + } + } + + @Test + void testShouldHandleUnprotectMalformedIV() throws Exception { + // Arrange + final String PLAINTEXT = "This is a plaintext value" + + // Act + KEY_SIZES.each { int keySize -> + SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize))) + logger.info("Initialized ${spp.name} with key size ${keySize}") + String cipherText = spp.protect(PLAINTEXT) + // Swap two characters in the IV + final String MALFORMED_IV_CIPHER_TEXT = manipulateString(cipherText, 8, 11) + logger.info("Manipulated ${cipherText} to\n${MALFORMED_IV_CIPHER_TEXT.padLeft(163)}") + + def msg = shouldFail(SensitivePropertyProtectionException) { + spp.unprotect(MALFORMED_IV_CIPHER_TEXT) + } + logger.expected("${msg} for keySize ${keySize} and cipher text [${MALFORMED_IV_CIPHER_TEXT}]") + + // Assert + assert msg == "Error decrypting a protected value" + } + } + + @Test + void testShouldGetIdentifierKeyWithDifferentMaxKeyLengths() throws Exception { + // Arrange + def keys = getAvailableKeySizes().collectEntries { int keySize -> + [(keySize): getKeyOfSize(keySize)] + } + logger.info("Keys: ${keys}") + + // Act + keys.each { int size, String key -> + String identifierKey = new AESSensitivePropertyProvider(key).getIdentifierKey() + logger.info("Identifier key: ${identifierKey} for size ${size}") + + // Assert + assert identifierKey =~ /aes\/gcm\/${size}/ + } + } + + @Test + void testShouldNotAllowEmptyKey() throws Exception { + // Arrange + final String INVALID_KEY = "" + + // Act + def msg = shouldFail(SensitivePropertyProtectionException) { + AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(INVALID_KEY) + } + + // Assert + assert msg == "The key cannot be empty" + } + + @Test + void testShouldNotAllowIncorrectlySizedKey() throws Exception { + // Arrange + final String INVALID_KEY = "Z" * 31 + + // Act + def msg = shouldFail(SensitivePropertyProtectionException) { + AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(INVALID_KEY) + } + + // Assert + assert msg == "The key must be a valid hexadecimal key" + } + + @Test + void testShouldNotAllowInvalidKey() throws Exception { + // Arrange + final String INVALID_KEY = "Z" * 32 + + // Act + def msg = shouldFail(SensitivePropertyProtectionException) { + AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(INVALID_KEY) + } + + // Assert + assert msg == "The key must be a valid hexadecimal key" + } + + /** + * This test is to ensure internal consistency and allow for encrypting value for various property files + */ + @Test + void testShouldEncryptArbitraryValues() { + // Arrange + def values = ["thisIsABadPassword", "thisIsABadSensitiveKeyPassword", "thisIsABadKeystorePassword", "thisIsABadKeyPassword", "thisIsABadTruststorePassword", "This is an encrypted banner message", "nififtw!"] + + String key = "2C576A9585DB862F5ECBEE5B4FFFCCA1" //getKeyOfSize(128) + // key = "0" * 64 + + SensitivePropertyProvider spp = new AESSensitivePropertyProvider(key) + + // Act + def encryptedValues = values.collect { String v -> + def encryptedValue = spp.protect(v) + logger.info("${v} -> ${encryptedValue}") + def (String iv, String cipherText) = encryptedValue.tokenize("||") + logger.info("Normal Base64 encoding would be ${encoder.encodeToString(decoder.decode(iv))}||${encoder.encodeToString(decoder.decode(cipherText))}") + encryptedValue + } + + // Assert + assert values == encryptedValues.collect { spp.unprotect(it) } + } + + /** + * This test is to ensure external compatibility in case someone encodes the encrypted value with Base64 and does not remove the padding + */ + @Test + void testShouldDecryptPaddedValueWith256BitKey() { + // Arrange + Assume.assumeTrue("JCE unlimited strength crypto policy must be installed for this test", Cipher.getMaxAllowedKeyLength("AES") > 128) + + final String EXPECTED_VALUE = getKeyOfSize(256) // "thisIsABadKeyPassword" + String cipherText = "aYDkDKys1ENr3gp+||sTBPpMlIvHcOLTGZlfWct8r9RY8BuDlDkoaYmGJ/9m9af9tZIVzcnDwvYQAaIKxRGF7vI2yrY7Xd6x9GTDnWGiGiRXlaP458BBMMgfzH2O8" + String unpaddedCipherText = cipherText.replaceAll("=", "") + + String key = "AAAABBBBCCCCDDDDEEEEFFFF00001111" * 2 // getKeyOfSize(256) + + SensitivePropertyProvider spp = new AESSensitivePropertyProvider(key) + + // Act + String rawValue = spp.unprotect(cipherText) + logger.info("Decrypted ${cipherText} to ${rawValue}") + String rawUnpaddedValue = spp.unprotect(unpaddedCipherText) + logger.info("Decrypted ${unpaddedCipherText} to ${rawUnpaddedValue}") + + // Assert + assert rawValue == EXPECTED_VALUE + assert rawUnpaddedValue == EXPECTED_VALUE + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/NiFiRegistryPropertiesGroovyTest.groovy b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/NiFiRegistryPropertiesGroovyTest.groovy new file mode 100644 index 0000000000..c7a0a4d388 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/NiFiRegistryPropertiesGroovyTest.groovy @@ -0,0 +1,148 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.properties + +import org.junit.* +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import static org.mockito.Mockito.mock +import static org.mockito.Mockito.when + +@RunWith(JUnit4.class) +class NiFiRegistryPropertiesGroovyTest extends GroovyTestCase { + private static final Logger logger = LoggerFactory.getLogger(NiFiRegistryPropertiesGroovyTest.class) + + @BeforeClass + static void setUpOnce() throws Exception { + logger.metaClass.methodMissing = { String name, args -> + logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}") + } + } + + @Before + void setUp() throws Exception { + } + + @After + void tearDown() throws Exception { + } + + @AfterClass + static void tearDownOnce() { + } + + private static NiFiRegistryProperties loadFromFile(String propertiesFilePath) { + String filePath + try { + filePath = NiFiRegistryPropertiesGroovyTest.class.getResource(propertiesFilePath).toURI().getPath() + } catch (URISyntaxException ex) { + throw new RuntimeException("Cannot load properties file due to " + + ex.getLocalizedMessage(), ex) + } + + NiFiRegistryProperties properties = new NiFiRegistryProperties() + FileReader reader = new FileReader(filePath) + + try { + properties.load(reader) + logger.info("Loaded {} properties from {}", properties.size(), filePath) + + return properties + } catch (final Exception ex) { + logger.error("Cannot load properties file due to " + ex.getLocalizedMessage()) + throw new RuntimeException("Cannot load properties file due to " + + ex.getLocalizedMessage(), ex) + } + } + + @Test + void testConstructorShouldCreateNewInstance() throws Exception { + // Arrange + + // Act + NiFiRegistryProperties NiFiRegistryProperties = new NiFiRegistryProperties() + logger.info("NiFiRegistryProperties has ${NiFiRegistryProperties.size()} properties: ${NiFiRegistryProperties.getPropertyKeys()}") + + // Assert + assert NiFiRegistryProperties.size() == 0 + assert NiFiRegistryProperties.getPropertyKeys() == [] as Set + } + + @Test + void testConstructorShouldAcceptDefaultProperties() throws Exception { + // Arrange + Properties rawProperties = new Properties() + rawProperties.setProperty("key", "value") + logger.info("rawProperties has ${rawProperties.size()} properties: ${rawProperties.stringPropertyNames()}") + assert rawProperties.size() == 1 + + // Act + NiFiRegistryProperties NiFiRegistryProperties = new NiFiRegistryProperties(rawProperties) + logger.info("NiFiRegistryProperties has ${NiFiRegistryProperties.size()} properties: ${NiFiRegistryProperties.getPropertyKeys()}") + + // Assert + assert NiFiRegistryProperties.size() == 1 + assert NiFiRegistryProperties.getPropertyKeys() == ["key"] as Set + } + + @Test + void testShouldAllowMultipleInstances() throws Exception { + // Arrange + + // Act + NiFiRegistryProperties properties = new NiFiRegistryProperties() + properties.setProperty("key", "value") + logger.info("niFiProperties has ${properties.size()} properties: ${properties.getPropertyKeys()}") + NiFiRegistryProperties emptyProperties = new NiFiRegistryProperties() + logger.info("emptyProperties has ${emptyProperties.size()} properties: ${emptyProperties.getPropertyKeys()}") + + // Assert + assert properties.size() == 1 + assert properties.getPropertyKeys() == ["key"] as Set + + assert emptyProperties.size() == 0 + assert emptyProperties.getPropertyKeys() == [] as Set + } + + @Test + void testAdditionalOidcScopesAreTrimmed() { + final String scope = "abc" + final String scopeLeadingWhitespace = " def" + final String scopeTrailingWhitespace = "ghi " + final String scopeLeadingTrailingWhitespace = " jkl " + + String additionalScopes = String.join(",", scope, scopeLeadingWhitespace, + scopeTrailingWhitespace, scopeLeadingTrailingWhitespace) + + NiFiRegistryProperties properties = mock(NiFiRegistryProperties.class) + when(properties.getProperty(NiFiRegistryProperties.SECURITY_USER_OIDC_ADDITIONAL_SCOPES, "")) + .thenReturn(additionalScopes) + when(properties.getOidcAdditionalScopes()).thenCallRealMethod() + + List scopes = properties.getOidcAdditionalScopes() + + assertTrue(scopes.contains(scope)); + assertFalse(scopes.contains(scopeLeadingWhitespace)); + assertTrue(scopes.contains(scopeLeadingWhitespace.trim())); + assertFalse(scopes.contains(scopeTrailingWhitespace)); + assertTrue(scopes.contains(scopeTrailingWhitespace.trim())); + assertFalse(scopes.contains(scopeLeadingTrailingWhitespace)); + assertTrue(scopes.contains(scopeLeadingTrailingWhitespace.trim())); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoaderGroovyTest.groovy b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoaderGroovyTest.groovy new file mode 100644 index 0000000000..58c8087d5b --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoaderGroovyTest.groovy @@ -0,0 +1,264 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.properties + +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.junit.After +import org.junit.AfterClass +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import javax.crypto.Cipher +import java.security.Security + +@RunWith(JUnit4.class) +class NiFiRegistryPropertiesLoaderGroovyTest extends GroovyTestCase { + + private static final Logger logger = LoggerFactory.getLogger(NiFiRegistryPropertiesLoaderGroovyTest.class) + + private static final String KEYSTORE_PASSWORD_KEY = NiFiRegistryProperties.SECURITY_KEYSTORE_PASSWD + private static final String KEY_PASSWORD_KEY = NiFiRegistryProperties.SECURITY_KEY_PASSWD + private static final String TRUSTSTORE_PASSWORD_KEY = NiFiRegistryProperties.SECURITY_TRUSTSTORE_PASSWD + + private static final String KEY_HEX_128 = "0123456789ABCDEFFEDCBA9876543210" + private static final String KEY_HEX_256 = KEY_HEX_128 * 2 + private static final String KEY_HEX = Cipher.getMaxAllowedKeyLength("AES") < 256 ? KEY_HEX_128 : KEY_HEX_256 + + private static final String PASSWORD_KEY_HEX_128 = "2C576A9585DB862F5ECBEE5B4FFFCCA1" + + @BeforeClass + public static void setUpOnce() throws Exception { + Security.addProvider(new BouncyCastleProvider()) + + logger.metaClass.methodMissing = { String name, args -> + logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}") + } + } + + @Before + public void setUp() throws Exception { + } + + @After + public void tearDown() throws Exception { + // Clear the sensitive property providers between runs + NiFiRegistryPropertiesLoader.@sensitivePropertyProviderFactory = null + } + + @AfterClass + public static void tearDownOnce() { + } + + @Test + public void testConstructorShouldCreateNewInstance() throws Exception { + // Arrange + + // Act + NiFiRegistryPropertiesLoader propertiesLoader = new NiFiRegistryPropertiesLoader() + + // Assert + assert !propertiesLoader.@keyHex + } + + @Test + public void testShouldCreateInstanceWithKey() throws Exception { + // Arrange + + // Act + NiFiRegistryPropertiesLoader propertiesLoader = NiFiRegistryPropertiesLoader.withKey(KEY_HEX) + + // Assert + assert propertiesLoader.@keyHex == KEY_HEX + } + + @Test + public void testConstructorShouldCreateMultipleInstances() throws Exception { + // Arrange + NiFiRegistryPropertiesLoader propertiesLoader1 = NiFiRegistryPropertiesLoader.withKey(KEY_HEX) + + // Act + NiFiRegistryPropertiesLoader propertiesLoader2 = new NiFiRegistryPropertiesLoader() + + // Assert + assert propertiesLoader1.@keyHex == KEY_HEX + assert !propertiesLoader2.@keyHex + } + + @Test + public void testShouldGetDefaultProviderKey() throws Exception { + // Arrange + final String expectedProviderKey = "aes/gcm/${Cipher.getMaxAllowedKeyLength("AES") > 128 ? 256 : 128}" + logger.info("Expected provider key: ${expectedProviderKey}") + + // Act + String defaultKey = NiFiRegistryPropertiesLoader.getDefaultProviderKey() + logger.info("Default key: ${defaultKey}") + // Assert + assert defaultKey == expectedProviderKey + } + + @Test + public void testShouldInitializeSensitivePropertyProviderFactory() throws Exception { + // Arrange + NiFiRegistryPropertiesLoader propertiesLoader = new NiFiRegistryPropertiesLoader() + + // Act + propertiesLoader.initializeSensitivePropertyProviderFactory() + + // Assert + assert propertiesLoader.@sensitivePropertyProviderFactory + } + + @Test + public void testShouldLoadUnprotectedPropertiesFromFile() throws Exception { + // Arrange + File unprotectedFile = new File("src/test/resources/conf/nifi-registry.properties") + NiFiRegistryPropertiesLoader propertiesLoader = new NiFiRegistryPropertiesLoader() + + // Act + NiFiRegistryProperties properties = propertiesLoader.load(unprotectedFile) + + // Assert + assert properties.size() > 0 + + // Ensure it is not a ProtectedNiFiProperties + assert properties instanceof NiFiRegistryProperties + } + + @Test + public void testShouldNotLoadUnprotectedPropertiesFromNullFile() throws Exception { + // Arrange + NiFiRegistryPropertiesLoader propertiesLoader = new NiFiRegistryPropertiesLoader() + + // Act + def msg = shouldFail(IllegalArgumentException) { + NiFiRegistryProperties properties = propertiesLoader.load(null as File) + } + logger.info(msg) + + // Assert + assert msg == "NiFi Registry properties file missing or unreadable" + } + + @Test + public void testShouldNotLoadUnprotectedPropertiesFromMissingFile() throws Exception { + // Arrange + File missingFile = new File("src/test/resources/conf/nifi-registry.missing.properties") + assert !missingFile.exists() + + NiFiRegistryPropertiesLoader propertiesLoader = new NiFiRegistryPropertiesLoader() + + // Act + def msg = shouldFail(IllegalArgumentException) { + NiFiRegistryProperties properties = propertiesLoader.load(missingFile) + } + logger.info(msg) + + // Assert + assert msg == "NiFi Registry properties file missing or unreadable" + } + + @Test + public void testShouldLoadUnprotectedPropertiesFromPath() throws Exception { + // Arrange + File unprotectedFile = new File("src/test/resources/conf/nifi-registry.properties") + NiFiRegistryPropertiesLoader propertiesLoader = new NiFiRegistryPropertiesLoader() + + // Act + NiFiRegistryProperties properties = propertiesLoader.load(unprotectedFile.path) + + // Assert + assert properties.size() > 0 + + // Ensure it is not a ProtectedNiFiProperties + assert properties instanceof NiFiRegistryProperties + } + + @Test + public void testShouldLoadUnprotectedPropertiesFromProtectedFile() throws Exception { + // Arrange + File protectedFile = new File("src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_128.properties") + NiFiRegistryPropertiesLoader propertiesLoader = NiFiRegistryPropertiesLoader.withKey(KEY_HEX_128) + + final def EXPECTED_PLAIN_VALUES = [ + (KEYSTORE_PASSWORD_KEY): "thisIsABadPassword", + (KEY_PASSWORD_KEY): "thisIsABadPassword", + ] + + // This method is covered in tests above, so safe to use here to retrieve protected properties + ProtectedNiFiRegistryProperties protectedNiFiProperties = propertiesLoader.readProtectedPropertiesFromDisk(protectedFile) + int totalKeysCount = protectedNiFiProperties.getPropertyKeysIncludingProtectionSchemes().size() + int protectedKeysCount = protectedNiFiProperties.getProtectedPropertyKeys().size() + logger.info("Read ${totalKeysCount} total properties (${protectedKeysCount} protected) from ${protectedFile.canonicalPath}") + + // Act + NiFiRegistryProperties properties = propertiesLoader.load(protectedFile) + + // Assert + assert properties.size() == totalKeysCount - protectedKeysCount + + // Ensure that any key marked as protected above is different in this instance + protectedNiFiProperties.getProtectedPropertyKeys().keySet().each { String key -> + String plainValue = properties.getProperty(key) + String protectedValue = protectedNiFiProperties.getProperty(key) + + logger.info("Checking that [${protectedValue}] -> [${plainValue}] == [${EXPECTED_PLAIN_VALUES[key]}]") + + assert plainValue == EXPECTED_PLAIN_VALUES[key] + assert plainValue != protectedValue + assert plainValue.length() <= protectedValue.length() + } + + // Ensure it is not a ProtectedNiFiProperties + assert properties instanceof NiFiRegistryProperties + } + + @Test + public void testShouldUpdateKeyInFactory() throws Exception { + // Arrange + File originalKeyFile = new File("src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_128.properties") + File passwordKeyFile = new File("src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_128_password.properties") + NiFiRegistryPropertiesLoader propertiesLoader = NiFiRegistryPropertiesLoader.withKey(KEY_HEX_128) + + NiFiRegistryProperties properties = propertiesLoader.load(originalKeyFile) + logger.info("Read ${properties.size()} total properties from ${originalKeyFile.canonicalPath}") + + // Act + NiFiRegistryPropertiesLoader passwordNiFiRegistryPropertiesLoader = NiFiRegistryPropertiesLoader.withKey(PASSWORD_KEY_HEX_128) + + NiFiRegistryProperties passwordProperties = passwordNiFiRegistryPropertiesLoader.load(passwordKeyFile) + logger.info("Read ${passwordProperties.size()} total properties from ${passwordKeyFile.canonicalPath}") + + // Assert + assert properties.size() == passwordProperties.size() + + + def readPropertiesAndValues = properties.getPropertyKeys().collectEntries { + [(it): properties.getProperty(it)] + } + def readPasswordPropertiesAndValues = passwordProperties.getPropertyKeys().collectEntries { + [(it): passwordProperties.getProperty(it)] + } + + assert readPropertiesAndValues == readPasswordPropertiesAndValues + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/ProtectedNiFiPropertiesGroovyTest.groovy b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/ProtectedNiFiPropertiesGroovyTest.groovy new file mode 100644 index 0000000000..86c7fb4021 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/ProtectedNiFiPropertiesGroovyTest.groovy @@ -0,0 +1,739 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.properties + +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.junit.After +import org.junit.AfterClass +import org.junit.Assume +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import javax.crypto.Cipher +import java.security.Security + +@RunWith(JUnit4.class) +class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase { + private static final Logger logger = LoggerFactory.getLogger(ProtectedNiFiPropertiesGroovyTest.class) + + private static final String KEYSTORE_PASSWORD_KEY = NiFiRegistryProperties.SECURITY_KEYSTORE_PASSWD + private static final String KEY_PASSWORD_KEY = NiFiRegistryProperties.SECURITY_KEY_PASSWD + private static final String TRUSTSTORE_PASSWORD_KEY = NiFiRegistryProperties.SECURITY_TRUSTSTORE_PASSWD + + private static final def DEFAULT_SENSITIVE_PROPERTIES = [ + KEYSTORE_PASSWORD_KEY, + KEY_PASSWORD_KEY, + TRUSTSTORE_PASSWORD_KEY + ] + + private static final String KEY_HEX_128 = "0123456789ABCDEFFEDCBA9876543210" + private static final String KEY_HEX_256 = KEY_HEX_128 * 2 + private static final String KEY_HEX = Cipher.getMaxAllowedKeyLength("AES") < 256 ? KEY_HEX_128 : KEY_HEX_256 + + @BeforeClass + static void setUpOnce() throws Exception { + Security.addProvider(new BouncyCastleProvider()) + + logger.metaClass.methodMissing = { String name, args -> + logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}") + } + } + + @Before + void setUp() throws Exception { + } + + @After + void tearDown() throws Exception { + } + + @AfterClass + static void tearDownOnce() { + } + + private static ProtectedNiFiRegistryProperties loadFromResourceFile(String propertiesFilePath) { + return loadFromResourceFile(propertiesFilePath, KEY_HEX) + } + + private static ProtectedNiFiRegistryProperties loadFromResourceFile(String propertiesFilePath, String keyHex) { + File file = fileForResource(propertiesFilePath) + + if (file == null || !file.exists() || !file.canRead()) { + String path = (file == null ? "missing file" : file.getAbsolutePath()) + logger.error("Cannot read from '{}' -- file is missing or not readable", path) + throw new IllegalArgumentException("NiFi Registry properties file missing or unreadable") + } + + NiFiRegistryProperties properties = new NiFiRegistryProperties() + FileReader reader = new FileReader(file) + + try { + properties.load(reader) + logger.info("Loaded {} properties from {}", properties.size(), file.getAbsolutePath()) + + ProtectedNiFiRegistryProperties protectedNiFiProperties = new ProtectedNiFiRegistryProperties(properties) + + // If it has protected keys, inject the SPP + if (protectedNiFiProperties.hasProtectedKeys()) { + protectedNiFiProperties.addSensitivePropertyProvider(new AESSensitivePropertyProvider(keyHex)) + } + + return protectedNiFiProperties + } catch (final Exception ex) { + logger.error("Cannot load properties file due to " + ex.getLocalizedMessage()) + throw new RuntimeException("Cannot load properties file due to " + + ex.getLocalizedMessage(), ex) + } + } + + private static File fileForResource(String resourcePath) { + String filePath + try { + URL resourceURL = ProtectedNiFiPropertiesGroovyTest.class.getResource(resourcePath) + if (!resourceURL) { + throw new RuntimeException("File ${resourcePath} not found in class resources, cannot load.") + } + filePath = resourceURL.toURI().getPath() + } catch (URISyntaxException ex) { + throw new RuntimeException("Cannot load resource file due to " + + ex.getLocalizedMessage(), ex) + } + File file = new File(filePath) + return file + } + + @Test + void testShouldDetectIfPropertyIsSensitive() throws Exception { + // Arrange + final String INSENSITIVE_PROPERTY_KEY = "nifi.registry.web.http.port" + final String SENSITIVE_PROPERTY_KEY = "nifi.registry.security.keystorePasswd" + + ProtectedNiFiRegistryProperties properties = loadFromResourceFile("/conf/nifi-registry.properties") + + // Act + boolean bannerIsSensitive = properties.isPropertySensitive(INSENSITIVE_PROPERTY_KEY) + logger.info("${INSENSITIVE_PROPERTY_KEY} is ${bannerIsSensitive ? "SENSITIVE" : "NOT SENSITIVE"}") + boolean passwordIsSensitive = properties.isPropertySensitive(SENSITIVE_PROPERTY_KEY) + logger.info("${SENSITIVE_PROPERTY_KEY} is ${passwordIsSensitive ? "SENSITIVE" : "NOT SENSITIVE"}") + + // Assert + assert !bannerIsSensitive + assert passwordIsSensitive + } + + @Test + void testShouldGetDefaultSensitiveProperties() throws Exception { + // Arrange + logger.info("${DEFAULT_SENSITIVE_PROPERTIES.size()} default sensitive properties: ${DEFAULT_SENSITIVE_PROPERTIES.join(", ")}") + + ProtectedNiFiRegistryProperties properties = loadFromResourceFile("/conf/nifi-registry.properties") + + // Act + List defaultSensitiveProperties = properties.getSensitivePropertyKeys() + logger.info("${defaultSensitiveProperties.size()} default sensitive properties: ${defaultSensitiveProperties.join(", ")}") + + // Assert + assert defaultSensitiveProperties.size() == DEFAULT_SENSITIVE_PROPERTIES.size() + assert defaultSensitiveProperties.containsAll(DEFAULT_SENSITIVE_PROPERTIES) + } + + @Test + void testShouldGetAdditionalSensitiveProperties() throws Exception { + // Arrange + def completeSensitiveProperties = DEFAULT_SENSITIVE_PROPERTIES + ["nifi.registry.web.http.port", "nifi.registry.web.http.host"] + logger.info("${completeSensitiveProperties.size()} total sensitive properties: ${completeSensitiveProperties.join(", ")}") + + ProtectedNiFiRegistryProperties properties = loadFromResourceFile("/conf/nifi-registry.with_additional_sensitive_keys.properties") + + // Act + List retrievedSensitiveProperties = properties.getSensitivePropertyKeys() + logger.info("${retrievedSensitiveProperties.size()} retrieved sensitive properties: ${retrievedSensitiveProperties.join(", ")}") + + // Assert + assert retrievedSensitiveProperties.size() == completeSensitiveProperties.size() + assert retrievedSensitiveProperties.containsAll(completeSensitiveProperties) + } + + @Test + void testGetAdditionalSensitivePropertiesShouldNotIncludeSelf() throws Exception { + // Arrange + def completeSensitiveProperties = DEFAULT_SENSITIVE_PROPERTIES + ["nifi.registry.web.http.port", "nifi.registry.web.http.host"] + logger.info("${completeSensitiveProperties.size()} total sensitive properties: ${completeSensitiveProperties.join(", ")}") + + ProtectedNiFiRegistryProperties properties = loadFromResourceFile("/conf/nifi-registry.with_additional_sensitive_keys.properties") + + // Act + List retrievedSensitiveProperties = properties.getSensitivePropertyKeys() + logger.info("${retrievedSensitiveProperties.size()} retrieved sensitive properties: ${retrievedSensitiveProperties.join(", ")}") + + // Assert + assert retrievedSensitiveProperties.size() == completeSensitiveProperties.size() + assert retrievedSensitiveProperties.containsAll(completeSensitiveProperties) + } + + /** + * In the default (no protection enabled) scenario, a call to retrieve a sensitive property should return the raw value transparently. + * @throws Exception + */ + @Test + void testShouldGetUnprotectedValueOfSensitiveProperty() throws Exception { + // Arrange + final String expectedKeystorePassword = "thisIsABadKeystorePassword" + + ProtectedNiFiRegistryProperties properties = loadFromResourceFile("/conf/nifi-registry.with_sensitive_props_unprotected.properties") + + boolean isSensitive = properties.isPropertySensitive(KEYSTORE_PASSWORD_KEY) + boolean isProtected = properties.isPropertyProtected(KEYSTORE_PASSWORD_KEY) + logger.info("The property is ${isSensitive ? "sensitive" : "not sensitive"} and ${isProtected ? "protected" : "not protected"}") + + // Act + String retrievedKeystorePassword = properties.getProperty(KEYSTORE_PASSWORD_KEY) + logger.info("${KEYSTORE_PASSWORD_KEY}: ${retrievedKeystorePassword}") + + // Assert + assert retrievedKeystorePassword == expectedKeystorePassword + assert isSensitive + assert !isProtected + } + + /** + * In the default (no protection enabled) scenario, a call to retrieve a sensitive property (which is empty) should return the raw value transparently. + * @throws Exception + */ + @Test + void testShouldGetEmptyUnprotectedValueOfSensitiveProperty() throws Exception { + // Arrange + final String expectedTruststorePassword = "" + + ProtectedNiFiRegistryProperties properties = loadFromResourceFile("/conf/nifi-registry.with_sensitive_props_unprotected.properties") + + boolean isSensitive = properties.isPropertySensitive(TRUSTSTORE_PASSWORD_KEY) + boolean isProtected = properties.isPropertyProtected(TRUSTSTORE_PASSWORD_KEY) + logger.info("The property is ${isSensitive ? "sensitive" : "not sensitive"} and ${isProtected ? "protected" : "not protected"}") + + // Act + NiFiRegistryProperties unprotectedProperties = properties.getUnprotectedProperties() + String retrievedTruststorePassword = unprotectedProperties.getProperty(TRUSTSTORE_PASSWORD_KEY) + logger.info("${TRUSTSTORE_PASSWORD_KEY}: ${retrievedTruststorePassword}") + + // Assert + assert retrievedTruststorePassword == expectedTruststorePassword + assert isSensitive + assert !isProtected + } + + /** + * The new model no longer needs to maintain the protected state -- it is used as a wrapper/decorator during load to unprotect the sensitive properties and then return an instance of raw properties. + * + * @throws Exception + */ + @Test + void testShouldGetUnprotectedValueOfSensitivePropertyWhenProtected() throws Exception { + // Arrange + final String expectedKeystorePassword = "thisIsABadPassword" + + ProtectedNiFiRegistryProperties properties = loadFromResourceFile("/conf/nifi-registry.with_sensitive_props_protected_aes_128.properties", KEY_HEX_128) + + boolean isSensitive = properties.isPropertySensitive(KEYSTORE_PASSWORD_KEY) + boolean isProtected = properties.isPropertyProtected(KEYSTORE_PASSWORD_KEY) + logger.info("The property is ${isSensitive ? "sensitive" : "not sensitive"} and ${isProtected ? "protected" : "not protected"}") + + // Act + NiFiRegistryProperties unprotectedProperties = properties.getUnprotectedProperties() + String retrievedKeystorePassword = unprotectedProperties.getProperty(KEYSTORE_PASSWORD_KEY) + logger.info("${KEYSTORE_PASSWORD_KEY}: ${retrievedKeystorePassword}") + + // Assert + assert retrievedKeystorePassword == expectedKeystorePassword + assert isSensitive + assert isProtected + } + + /** + * In the protection enabled scenario, a call to retrieve a sensitive property should handle if the property is protected with an unknown protection scheme. + * @throws Exception + */ + @Test + void testGetValueOfSensitivePropertyShouldHandleUnknownProtectionScheme() throws Exception { + // Arrange + + // Raw properties + Properties rawProperties = new Properties() + rawProperties.load(new FileReader(fileForResource("/conf/nifi-registry.with_sensitive_props_protected_unknown.properties"))) + final String expectedKeystorePassword = rawProperties.getProperty(KEYSTORE_PASSWORD_KEY) + logger.info("Raw value for ${KEYSTORE_PASSWORD_KEY}: ${expectedKeystorePassword}") + + ProtectedNiFiRegistryProperties properties = loadFromResourceFile("/conf/nifi-registry.with_sensitive_props_protected_unknown.properties") + + boolean isSensitive = properties.isPropertySensitive(KEYSTORE_PASSWORD_KEY) + boolean isProtected = properties.isPropertyProtected(KEYSTORE_PASSWORD_KEY) + + // While the value is "protected", the scheme is not registered, so treat it as raw + logger.info("The property is ${isSensitive ? "sensitive" : "not sensitive"} and ${isProtected ? "protected" : "not protected"}") + + // Act + NiFiRegistryProperties unprotectedProperties = properties.getUnprotectedProperties() + String retrievedKeystorePassword = unprotectedProperties.getProperty(KEYSTORE_PASSWORD_KEY) + logger.info("${KEYSTORE_PASSWORD_KEY}: ${retrievedKeystorePassword}") + + // Assert + assert retrievedKeystorePassword == expectedKeystorePassword + assert isSensitive + assert isProtected + } + + /** + * In the protection enabled scenario, a call to retrieve a sensitive property should handle if the property is unable to be unprotected due to a malformed value. + * @throws Exception + */ + @Test + void testGetValueOfSensitivePropertyShouldHandleSingleMalformedValue() throws Exception { + // Arrange + ProtectedNiFiRegistryProperties properties = loadFromResourceFile("/conf/nifi-registry.with_sensitive_props_protected_aes_single_malformed.properties", KEY_HEX_128) + boolean isSensitive = properties.isPropertySensitive(KEYSTORE_PASSWORD_KEY) + boolean isProtected = properties.isPropertyProtected(KEYSTORE_PASSWORD_KEY) + logger.info("The property is ${isSensitive ? "sensitive" : "not sensitive"} and ${isProtected ? "protected" : "not protected"}") + + // Act + def msg = shouldFail(SensitivePropertyProtectionException) { + NiFiRegistryProperties unprotectedProperties = properties.getUnprotectedProperties() + String retrievedKeystorePassword = unprotectedProperties.getProperty(KEYSTORE_PASSWORD_KEY) + logger.info("${KEYSTORE_PASSWORD_KEY}: ${retrievedKeystorePassword}") + } + logger.info(msg) + + // Assert + assert msg =~ "Failed to unprotect key ${KEYSTORE_PASSWORD_KEY}" + assert isSensitive + assert isProtected + } + + /** + * In the protection enabled scenario, a call to retrieve a sensitive property should handle if the property is unable to be unprotected due to a malformed value. + * @throws Exception + */ + @Test + void testGetValueOfSensitivePropertyShouldHandleMultipleMalformedValues() throws Exception { + // Arrange + + // Raw properties + ProtectedNiFiRegistryProperties properties = + loadFromResourceFile("/conf/nifi-registry.with_sensitive_props_protected_aes_multiple_malformed.properties", KEY_HEX_128) + + // Iterate over the protected keys and track the ones that fail to decrypt + SensitivePropertyProvider spp = new AESSensitivePropertyProvider(KEY_HEX_128) + Set malformedKeys = properties.getProtectedPropertyKeys() + .findAll { String key, String scheme -> scheme == spp.identifierKey } + .keySet() + .findAll { String key -> + try { + spp.unprotect(properties.getProperty(key)) + return false + } catch (SensitivePropertyProtectionException e) { + logger.expected("Caught a malformed value for ${key}") + return true + } + } + + logger.expected("Malformed keys: ${malformedKeys.join(", ")}") + + // Act + def e = groovy.test.GroovyAssert.shouldFail(SensitivePropertyProtectionException) { + NiFiRegistryProperties unprotectedProperties = properties.getUnprotectedProperties() + String retrievedKeystorePassword = unprotectedProperties.getProperty(KEYSTORE_PASSWORD_KEY) + logger.info("${KEYSTORE_PASSWORD_KEY}: ${retrievedKeystorePassword}") + } + logger.expected(e.getMessage()) + + // Assert + assert e instanceof MultipleSensitivePropertyProtectionException + assert e.getMessage() =~ "Failed to unprotect keys" + assert e.getFailedKeys() == malformedKeys + + } + + /** + * In the protection enabled scenario, a call to retrieve a sensitive property should handle if the internal cache of providers is empty. + * @throws Exception + */ + @Test + void testGetValueOfSensitivePropertyShouldHandleInvalidatedInternalCache() throws Exception { + // Arrange + ProtectedNiFiRegistryProperties properties = + loadFromResourceFile("/conf/nifi-registry.with_sensitive_props_protected_aes_128.properties", KEY_HEX_128) + final String expectedKeystorePassword = properties.getProperty(KEYSTORE_PASSWORD_KEY) + logger.info("Read raw value from properties: ${expectedKeystorePassword}") + + // Overwrite the internal cache + properties.localProviderCache = [:] + + boolean isSensitive = properties.isPropertySensitive(KEYSTORE_PASSWORD_KEY) + boolean isProtected = properties.isPropertyProtected(KEYSTORE_PASSWORD_KEY) + logger.info("The property is ${isSensitive ? "sensitive" : "not sensitive"} and ${isProtected ? "protected" : "not protected"}") + + // Act + NiFiRegistryProperties unprotectedProperties = properties.getUnprotectedProperties() + String retrievedKeystorePassword = unprotectedProperties.getProperty(KEYSTORE_PASSWORD_KEY) + logger.info("${KEYSTORE_PASSWORD_KEY}: ${retrievedKeystorePassword}") + + // Assert + assert retrievedKeystorePassword == expectedKeystorePassword + assert isSensitive + assert isProtected + } + + @Test + void testShouldDetectIfPropertyIsProtected() throws Exception { + // Arrange + final String unprotectedPropertyKey = TRUSTSTORE_PASSWORD_KEY + final String protectedPropertyKey = KEYSTORE_PASSWORD_KEY + + ProtectedNiFiRegistryProperties properties = + loadFromResourceFile("/conf/nifi-registry.with_sensitive_props_protected_aes_128.properties", KEY_HEX_128) + + // Act + boolean unprotectedPasswordIsSensitive = properties.isPropertySensitive(unprotectedPropertyKey) + boolean unprotectedPasswordIsProtected = properties.isPropertyProtected(unprotectedPropertyKey) + logger.info("${unprotectedPropertyKey} is ${unprotectedPasswordIsSensitive ? "SENSITIVE" : "NOT SENSITIVE"}") + logger.info("${unprotectedPropertyKey} is ${unprotectedPasswordIsProtected ? "PROTECTED" : "NOT PROTECTED"}") + boolean protectedPasswordIsSensitive = properties.isPropertySensitive(protectedPropertyKey) + boolean protectedPasswordIsProtected = properties.isPropertyProtected(protectedPropertyKey) + logger.info("${protectedPropertyKey} is ${protectedPasswordIsSensitive ? "SENSITIVE" : "NOT SENSITIVE"}") + logger.info("${protectedPropertyKey} is ${protectedPasswordIsProtected ? "PROTECTED" : "NOT PROTECTED"}") + + // Assert + assert unprotectedPasswordIsSensitive + assert !unprotectedPasswordIsProtected + + assert protectedPasswordIsSensitive + assert protectedPasswordIsProtected + } + + @Test + void testShouldDetectIfPropertyWithEmptyProtectionSchemeIsProtected() throws Exception { + // Arrange + final String unprotectedPropertyKey = KEY_PASSWORD_KEY + ProtectedNiFiRegistryProperties properties = + loadFromResourceFile("/conf/nifi-registry.with_sensitive_props_unprotected_extra_line.properties") + + // Act + boolean unprotectedPasswordIsSensitive = properties.isPropertySensitive(unprotectedPropertyKey) + boolean unprotectedPasswordIsProtected = properties.isPropertyProtected(unprotectedPropertyKey) + logger.info("${unprotectedPropertyKey} is ${unprotectedPasswordIsSensitive ? "SENSITIVE" : "NOT SENSITIVE"}") + logger.info("${unprotectedPropertyKey} is ${unprotectedPasswordIsProtected ? "PROTECTED" : "NOT PROTECTED"}") + + // Assert + assert unprotectedPasswordIsSensitive + assert !unprotectedPasswordIsProtected + } + + @Test + void testShouldGetPercentageOfSensitivePropertiesProtected_0() throws Exception { + // Arrange + ProtectedNiFiRegistryProperties properties = loadFromResourceFile("/conf/nifi-registry.properties") + + logger.info("Sensitive property keys: ${properties.getSensitivePropertyKeys()}") + logger.info("Protected property keys: ${properties.getProtectedPropertyKeys().keySet()}") + + // Act + double percentProtected = properties.getPercentOfSensitivePropertiesProtected() + logger.info("${percentProtected}% (${properties.getProtectedPropertyKeys().size()} of ${properties.getPopulatedSensitivePropertyKeys().size()}) protected") + + // Assert + assert percentProtected == 0.0 + } + + @Test + void testShouldGetPercentageOfSensitivePropertiesProtected_75() throws Exception { + // Arrange + ProtectedNiFiRegistryProperties properties = + loadFromResourceFile("/conf/nifi-registry.with_sensitive_props_protected_aes_128.properties", KEY_HEX_128) + + logger.info("Sensitive property keys: ${properties.getSensitivePropertyKeys()}") + logger.info("Protected property keys: ${properties.getProtectedPropertyKeys().keySet()}") + + // Act + double percentProtected = properties.getPercentOfSensitivePropertiesProtected() + logger.info("${percentProtected}% (${properties.getProtectedPropertyKeys().size()} of ${properties.getPopulatedSensitivePropertyKeys().size()}) protected") + + // Assert + assert percentProtected == 67.0 + } + + @Test + void testShouldGetPercentageOfSensitivePropertiesProtected_100() throws Exception { + // Arrange + ProtectedNiFiRegistryProperties properties = + loadFromResourceFile("/conf/nifi-registry.with_sensitive_props_fully_protected_aes_128.properties", KEY_HEX_128) + + logger.info("Sensitive property keys: ${properties.getSensitivePropertyKeys()}") + logger.info("Protected property keys: ${properties.getProtectedPropertyKeys().keySet()}") + + // Act + double percentProtected = properties.getPercentOfSensitivePropertiesProtected() + logger.info("${percentProtected}% (${properties.getProtectedPropertyKeys().size()} of ${properties.getPopulatedSensitivePropertyKeys().size()}) protected") + + // Assert + assert percentProtected == 100.0 + } + + @Test + void testInstanceWithNoProtectedPropertiesShouldNotLoadSPP() throws Exception { + // Arrange + ProtectedNiFiRegistryProperties properties = loadFromResourceFile("/conf/nifi-registry.properties") + assert properties.@localProviderCache?.isEmpty() + + logger.info("Has protected properties: ${properties.hasProtectedKeys()}") + assert !properties.hasProtectedKeys() + + // Act + Map localCache = properties.@localProviderCache + logger.info("Internal cache ${localCache} has ${localCache.size()} providers loaded") + + // Assert + assert localCache.isEmpty() + } + + @Test + void testShouldAddSensitivePropertyProvider() throws Exception { + // Arrange + ProtectedNiFiRegistryProperties properties = new ProtectedNiFiRegistryProperties() + assert properties.getSensitivePropertyProviders().isEmpty() + + SensitivePropertyProvider mockProvider = + [unprotect : { String input -> + logger.mock("Mock call to #unprotect(${input})") + input.reverse() + }, + getIdentifierKey: { -> "mockProvider" }] as SensitivePropertyProvider + + // Act + properties.addSensitivePropertyProvider(mockProvider) + + // Assert + assert properties.getSensitivePropertyProviders().size() == 1 + } + + @Test + void testShouldNotAddNullSensitivePropertyProvider() throws Exception { + // Arrange + ProtectedNiFiRegistryProperties properties = new ProtectedNiFiRegistryProperties() + assert properties.@localProviderCache?.isEmpty() + + // Act + def msg = shouldFail(IllegalArgumentException) { + properties.addSensitivePropertyProvider(null) + } + logger.info(msg) + + // Assert + assert properties.getSensitivePropertyProviders().size() == 0 + assert msg == "Cannot add null SensitivePropertyProvider" + } + + @Test + void testShouldNotAllowOverwriteOfProvider() throws Exception { + // Arrange + ProtectedNiFiRegistryProperties properties = new ProtectedNiFiRegistryProperties() + assert properties.getSensitivePropertyProviders().isEmpty() + + SensitivePropertyProvider mockProvider = + [unprotect : { String input -> + logger.mock("Mock call to 1#unprotect(${input})") + input.reverse() + }, + getIdentifierKey: { -> "mockProvider" }] as SensitivePropertyProvider + properties.addSensitivePropertyProvider(mockProvider) + assert properties.getSensitivePropertyProviders().size() == 1 + + SensitivePropertyProvider mockProvider2 = + [unprotect : { String input -> + logger.mock("Mock call to 2#unprotect(${input})") + input.reverse() + }, + getIdentifierKey: { -> "mockProvider" }] as SensitivePropertyProvider + + // Act + def msg = shouldFail(UnsupportedOperationException) { + properties.addSensitivePropertyProvider(mockProvider2) + } + logger.info(msg) + + // Assert + assert msg == "Cannot overwrite existing sensitive property provider registered for mockProvider" + assert properties.getSensitivePropertyProviders().size() == 1 + } + + @Test + void testGetUnprotectedPropertiesShouldReturnInternalInstanceWhenNoneProtected() { + // Arrange + ProtectedNiFiRegistryProperties protectedNiFiProperties = loadFromResourceFile("/conf/nifi-registry.properties") + logger.info("Loaded ${protectedNiFiProperties.size()} properties from conf/nifi.properties") + + int hashCode = protectedNiFiProperties.internalNiFiProperties.hashCode() + logger.info("Hash code of internal instance: ${hashCode}") + + // Act + NiFiRegistryProperties unprotectedNiFiProperties = protectedNiFiProperties.getUnprotectedProperties() + logger.info("Unprotected ${unprotectedNiFiProperties.size()} properties") + + // Assert + assert unprotectedNiFiProperties.size() == protectedNiFiProperties.size() + assert unprotectedNiFiProperties.getPropertyKeys().every { + !unprotectedNiFiProperties.getProperty(it).endsWith(".protected") + } + logger.info("Hash code from returned unprotected instance: ${unprotectedNiFiProperties.hashCode()}") + assert unprotectedNiFiProperties.hashCode() == hashCode + } + + @Test + void testGetUnprotectedPropertiesShouldDecryptProtectedProperties() { + // Arrange + ProtectedNiFiRegistryProperties protectedNiFiProperties = + loadFromResourceFile("/conf/nifi-registry.with_sensitive_props_protected_aes_128.properties", KEY_HEX_128) + + int protectedPropertyCount = protectedNiFiProperties.getProtectedPropertyKeys().size() + int protectionSchemeCount = protectedNiFiProperties + .getPropertyKeysIncludingProtectionSchemes() + .findAll { it.endsWith(".protected") } + .size() + int expectedUnprotectedPropertyCount = protectedNiFiProperties.size() + + String protectedProps = protectedNiFiProperties + .getProtectedPropertyKeys() + .collectEntries { + [(it.key): protectedNiFiProperties.getProperty(it.key)] + }.entrySet() + .join("\n") + + logger.info("Detected ${protectedPropertyCount} protected properties and ${protectionSchemeCount} protection scheme properties") + logger.info("Protected properties: \n${protectedProps}") + + logger.info("Expected unprotected property count: ${expectedUnprotectedPropertyCount}") + + int hashCode = protectedNiFiProperties.internalNiFiProperties.hashCode() + logger.info("Hash code of internal instance: ${hashCode}") + + // Act + NiFiRegistryProperties unprotectedNiFiProperties = protectedNiFiProperties.getUnprotectedProperties() + logger.info("Unprotected ${unprotectedNiFiProperties.size()} properties") + + // Assert + assert unprotectedNiFiProperties.size() == expectedUnprotectedPropertyCount + assert unprotectedNiFiProperties.getPropertyKeys().every { + !unprotectedNiFiProperties.getProperty(it).endsWith(".protected") + } + logger.info("Hash code from returned unprotected instance: ${unprotectedNiFiProperties.hashCode()}") + assert unprotectedNiFiProperties.hashCode() != hashCode + } + + @Test + void testGetUnprotectedPropertiesShouldDecryptProtectedPropertiesWith256Bit() { + // Arrange + Assume.assumeTrue("JCE unlimited strength crypto policy must be installed for this test", Cipher.getMaxAllowedKeyLength("AES") > 128) + ProtectedNiFiRegistryProperties protectedNiFiProperties = + loadFromResourceFile("/conf/nifi-registry.with_sensitive_props_protected_aes_256.properties", KEY_HEX_256) + + int protectedPropertyCount = protectedNiFiProperties.getProtectedPropertyKeys().size() + int protectionSchemeCount = protectedNiFiProperties + .getPropertyKeysIncludingProtectionSchemes() + .findAll { it.endsWith(".protected") } + .size() + int expectedUnprotectedPropertyCount = protectedNiFiProperties.size() + + String protectedProps = protectedNiFiProperties + .getProtectedPropertyKeys() + .collectEntries { + [(it.key): protectedNiFiProperties.getProperty(it.key)] + }.entrySet() + .join("\n") + + logger.info("Detected ${protectedPropertyCount} protected properties and ${protectionSchemeCount} protection scheme properties") + logger.info("Protected properties: \n${protectedProps}") + + logger.info("Expected unprotected property count: ${expectedUnprotectedPropertyCount}") + + int hashCode = protectedNiFiProperties.internalNiFiProperties.hashCode() + logger.info("Hash code of internal instance: ${hashCode}") + + // Act + NiFiRegistryProperties unprotectedNiFiProperties = protectedNiFiProperties.getUnprotectedProperties() + logger.info("Unprotected ${unprotectedNiFiProperties.size()} properties") + + // Assert + assert unprotectedNiFiProperties.size() == expectedUnprotectedPropertyCount + assert unprotectedNiFiProperties.getPropertyKeys().every { + !unprotectedNiFiProperties.getProperty(it).endsWith(".protected") + } + logger.info("Hash code from returned unprotected instance: ${unprotectedNiFiProperties.hashCode()}") + assert unprotectedNiFiProperties.hashCode() != hashCode + } + + @Test + void testShouldCalculateSize() { + // Arrange + NiFiRegistryProperties rawProperties = [key: "protectedValue", "key.protected": "scheme", "key2": "value2"] as NiFiRegistryProperties + ProtectedNiFiRegistryProperties protectedNiFiProperties = new ProtectedNiFiRegistryProperties(rawProperties) + logger.info("Raw properties (${rawProperties.size()}): ${rawProperties.keySet().join(", ")}") + + // Act + int protectedSize = protectedNiFiProperties.size() + logger.info("Protected properties (${protectedNiFiProperties.size()}): " + + "${protectedNiFiProperties.getPropertyKeysExcludingProtectionSchemes().join(", ")}") + + // Assert + assert protectedSize == rawProperties.size() - 1 + } + + @Test + void testGetPropertyKeysShouldMatchSize() { + // Arrange + NiFiRegistryProperties rawProperties = [key: "protectedValue", "key.protected": "scheme", "key2": "value2"] as NiFiRegistryProperties + ProtectedNiFiRegistryProperties protectedNiFiProperties = new ProtectedNiFiRegistryProperties(rawProperties) + logger.info("Raw properties (${rawProperties.size()}): ${rawProperties.keySet().join(", ")}") + + // Act + def filteredKeys = protectedNiFiProperties.getPropertyKeysExcludingProtectionSchemes() + logger.info("Protected properties (${protectedNiFiProperties.size()}): ${filteredKeys.join(", ")}") + + // Assert + assert protectedNiFiProperties.size() == rawProperties.size() - 1 + assert filteredKeys == rawProperties.keySet() - "key.protected" + } + + @Test + void testShouldGetPropertyKeysIncludingProtectionSchemes() { + // Arrange + NiFiRegistryProperties rawProperties = [key: "protectedValue", "key.protected": "scheme", "key2": "value2"] as NiFiRegistryProperties + ProtectedNiFiRegistryProperties protectedNiFiProperties = new ProtectedNiFiRegistryProperties(rawProperties) + logger.info("Raw properties (${rawProperties.size()}): ${rawProperties.keySet().join(", ")}") + + // Act + def allKeys = protectedNiFiProperties.getPropertyKeysIncludingProtectionSchemes() + logger.info("Protected properties with schemes (${allKeys.size()}): ${allKeys.join(", ")}") + + // Assert + assert allKeys.size() == rawProperties.size() + assert allKeys == rawProperties.keySet() + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/security/crypto/CryptoKeyLoaderGroovyTest.groovy b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/security/crypto/CryptoKeyLoaderGroovyTest.groovy new file mode 100644 index 0000000000..6d8623ee90 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/security/crypto/CryptoKeyLoaderGroovyTest.groovy @@ -0,0 +1,125 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.security.crypto + +import org.apache.commons.lang3.SystemUtils +import org.apache.nifi.registry.security.crypto.CryptoKeyLoader +import org.apache.nifi.registry.security.crypto.CryptoKeyProvider +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.junit.Assume +import org.junit.BeforeClass +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.nio.file.Files +import java.nio.file.attribute.PosixFilePermission +import java.security.Security + +@RunWith(JUnit4.class) +class CryptoKeyLoaderGroovyTest extends GroovyTestCase { + + private static final Logger logger = LoggerFactory.getLogger(CryptoKeyLoaderGroovyTest.class) + + private static final String KEY_HEX_128 = "0123456789ABCDEFFEDCBA9876543210" + private static final String KEY_HEX_256 = KEY_HEX_128 * 2 + + @BeforeClass + public static void setUpOnce() throws Exception { + Assume.assumeTrue("Test only runs on *nix", !SystemUtils.IS_OS_WINDOWS) + + Security.addProvider(new BouncyCastleProvider()) + + logger.metaClass.methodMissing = { String name, args -> + logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}") + } + } + + @Test + public void testShouldExtractKeyFromBootstrapFile() throws Exception { + // Arrange + final String expectedKey = KEY_HEX_256 + + // Act + String key = CryptoKeyLoader.extractKeyFromBootstrapFile("src/test/resources/conf/bootstrap.conf") + + // Assert + assert key == expectedKey + } + + @Test + public void testShouldNotExtractKeyFromBootstrapFileWithoutKeyLine() throws Exception { + // Arrange + + // Act + String key = CryptoKeyLoader.extractKeyFromBootstrapFile("src/test/resources/conf/bootstrap.with_missing_key_line.conf") + + // Assert + assert key == CryptoKeyProvider.EMPTY_KEY + } + + @Test + public void testShouldNotExtractKeyFromBootstrapFileWithoutKey() throws Exception { + // Arrange + + // Act + String key = CryptoKeyLoader.extractKeyFromBootstrapFile("src/test/resources/conf/bootstrap.with_missing_key.conf") + + // Assert + assert key == CryptoKeyProvider.EMPTY_KEY + } + + @Test + public void testShouldNotExtractKeyFromMissingBootstrapFile() throws Exception { + // Arrange + + // Act + def msg = shouldFail(IOException) { + CryptoKeyLoader.extractKeyFromBootstrapFile("src/test/resources/conf/bootstrap.missing.conf") + } + logger.info(msg) + + // Assert + assert msg == "Cannot read from bootstrap.conf" + } + + @Test + public void testShouldNotExtractKeyFromUnreadableBootstrapFile() throws Exception { + // Arrange + File unreadableFile = new File("src/test/resources/conf/bootstrap.unreadable_file_permissions.conf") + Set originalPermissions = Files.getPosixFilePermissions(unreadableFile.toPath()) + Files.setPosixFilePermissions(unreadableFile.toPath(), [] as Set) + try { + assert !unreadableFile.canRead() + + // Act + def msg = shouldFail(IOException) { + CryptoKeyLoader.extractKeyFromBootstrapFile("src/test/resources/conf/bootstrap.unreadable_file_permissions.conf") + } + logger.info(msg) + + // Assert + assert msg == "Cannot read from bootstrap.conf" + } finally { + // Clean up to allow for indexing, etc. + Files.setPosixFilePermissions(unreadableFile.toPath(), originalPermissions) + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/bootstrap.conf b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/bootstrap.conf new file mode 100644 index 0000000000..4321bca7b2 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/bootstrap.conf @@ -0,0 +1,60 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# Java command to use when running nifi-registry +java=java + +# Username to use when running nifi-registry. This value will be ignored on Windows. +run.as= + +# Configure where nifi-registry's lib and conf directories live +lib.dir=./lib +conf.dir=./conf + +# How long to wait after telling nifi-registry to shutdown before explicitly killing the Process +graceful.shutdown.seconds=20 + +# Disable JSR 199 so that we can use JSP's without running a JDK +java.arg.1=-Dorg.apache.jasper.compiler.disablejsr199=true + +# JVM memory settings +java.arg.2=-Xms512m +java.arg.3=-Xmx512m + +# Enable Remote Debugging +#java.arg.debug=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000 + +java.arg.4=-Djava.net.preferIPv4Stack=true + +# allowRestrictedHeaders is required for Cluster/Node communications to work properly +java.arg.5=-Dsun.net.http.allowRestrictedHeaders=true +java.arg.6=-Djava.protocol.handler.pkgs=sun.net.www.protocol + +# Java 7 and below have issues with Code Cache. The following lines allow us to run well even with +# many classes loaded in the JVM. +#java.arg.7=-XX:ReservedCodeCacheSize=256m +#java.arg.8=-XX:CodeCacheFlushingMinimumFreeSpace=10m +#java.arg.9=-XX:+UseCodeCacheFlushing +#java.arg.11=-XX:PermSize=128M +#java.arg.12=-XX:MaxPermSize=128M + +# The G1GC is still considered experimental but has proven to be very advantageous in providing great +# performance without significant "stop-the-world" delays. +#java.arg.10=-XX:+UseG1GC + +# Master key in hexadecimal format for encrypted sensitive configuration values +nifi.registry.bootstrap.sensitive.key=0123456789ABCDEFFEDCBA98765432100123456789ABCDEFFEDCBA9876543210 \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/bootstrap.unreadable_file_permissions.conf b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/bootstrap.unreadable_file_permissions.conf new file mode 100644 index 0000000000..30436353a0 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/bootstrap.unreadable_file_permissions.conf @@ -0,0 +1,22 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# The POSIX file permissions for this file are emptied (i.e., chmod 000) during test and then reverted +# See org.apache.nifi.registry.properties.NiFiRegistryPropertiesLoaderGroovyTest#testShouldNotExtractKeyFromUnreadableBootstrapFile + +# Master key in hexadecimal format for encrypted sensitive configuration values +nifi.registry.bootstrap.sensitive.key=0123456789ABCDEFFEDCBA98765432100123456789ABCDEFFEDCBA9876543210 \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/bootstrap.with_missing_key.conf b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/bootstrap.with_missing_key.conf new file mode 100644 index 0000000000..7317ab0299 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/bootstrap.with_missing_key.conf @@ -0,0 +1,60 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# Java command to use when running nifi-registry +java=java + +# Username to use when running nifi-registry. This value will be ignored on Windows. +run.as= + +# Configure where nifi-registry's lib and conf directories live +lib.dir=./lib +conf.dir=./conf + +# How long to wait after telling nifi-registry to shutdown before explicitly killing the Process +graceful.shutdown.seconds=20 + +# Disable JSR 199 so that we can use JSP's without running a JDK +java.arg.1=-Dorg.apache.jasper.compiler.disablejsr199=true + +# JVM memory settings +java.arg.2=-Xms512m +java.arg.3=-Xmx512m + +# Enable Remote Debugging +#java.arg.debug=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000 + +java.arg.4=-Djava.net.preferIPv4Stack=true + +# allowRestrictedHeaders is required for Cluster/Node communications to work properly +java.arg.5=-Dsun.net.http.allowRestrictedHeaders=true +java.arg.6=-Djava.protocol.handler.pkgs=sun.net.www.protocol + +# Java 7 and below have issues with Code Cache. The following lines allow us to run well even with +# many classes loaded in the JVM. +#java.arg.7=-XX:ReservedCodeCacheSize=256m +#java.arg.8=-XX:CodeCacheFlushingMinimumFreeSpace=10m +#java.arg.9=-XX:+UseCodeCacheFlushing +#java.arg.11=-XX:PermSize=128M +#java.arg.12=-XX:MaxPermSize=128M + +# The G1GC is still considered experimental but has proven to be very advantageous in providing great +# performance without significant "stop-the-world" delays. +#java.arg.10=-XX:+UseG1GC + +# Master key in hexadecimal format for encrypted sensitive configuration values +nifi.registry.bootstrap.sensitive.key= \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/bootstrap.with_missing_key_line.conf b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/bootstrap.with_missing_key_line.conf new file mode 100644 index 0000000000..6ccdaaf041 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/bootstrap.with_missing_key_line.conf @@ -0,0 +1,60 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# Java command to use when running nifi-registry +java=java + +# Username to use when running nifi-registry. This value will be ignored on Windows. +run.as= + +# Configure where nifi-registry's lib and conf directories live +lib.dir=./lib +conf.dir=./conf + +# How long to wait after telling nifi-registry to shutdown before explicitly killing the Process +graceful.shutdown.seconds=20 + +# Disable JSR 199 so that we can use JSP's without running a JDK +java.arg.1=-Dorg.apache.jasper.compiler.disablejsr199=true + +# JVM memory settings +java.arg.2=-Xms512m +java.arg.3=-Xmx512m + +# Enable Remote Debugging +#java.arg.debug=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000 + +java.arg.4=-Djava.net.preferIPv4Stack=true + +# allowRestrictedHeaders is required for Cluster/Node communications to work properly +java.arg.5=-Dsun.net.http.allowRestrictedHeaders=true +java.arg.6=-Djava.protocol.handler.pkgs=sun.net.www.protocol + +# Java 7 and below have issues with Code Cache. The following lines allow us to run well even with +# many classes loaded in the JVM. +#java.arg.7=-XX:ReservedCodeCacheSize=256m +#java.arg.8=-XX:CodeCacheFlushingMinimumFreeSpace=10m +#java.arg.9=-XX:+UseCodeCacheFlushing +#java.arg.11=-XX:PermSize=128M +#java.arg.12=-XX:MaxPermSize=128M + +# The G1GC is still considered experimental but has proven to be very advantageous in providing great +# performance without significant "stop-the-world" delays. +#java.arg.10=-XX:+UseG1GC + +# Master key in hexadecimal format for encrypted sensitive configuration values +# nifi.registry.bootstrap.sensitive.key is intentionally absent from this file \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.properties b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.properties new file mode 100644 index 0000000000..a7efedbeed --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.properties @@ -0,0 +1,45 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# web properties # +nifi.registry.web.war.directory=./target/lib +nifi.registry.web.http.host= +nifi.registry.web.http.port=8080 +nifi.registry.web.https.host= +nifi.registry.web.https.port= +nifi.registry.web.jetty.working.directory=./target/work/jetty +nifi.registry.web.jetty.threads=1 + +# security properties # +nifi.registry.security.keystore= +nifi.registry.security.keystoreType= +nifi.registry.security.keystorePasswd= +nifi.registry.security.keyPasswd= +nifi.registry.security.truststore= +nifi.registry.security.truststoreType= +nifi.registry.security.truststorePasswd= +nifi.registry.security.needClientAuth= +nifi.registry.security.authorizers.configuration.file= +nifi.registry.security.authorizer= +nifi.registry.security.identity.providers.configuration.file= +nifi.registry.security.identity.provider= + +# kerberos properties +nifi.registry.kerberos.krb5.file=/path/to/krb5.conf +nifi.registry.kerberos.spnego.authentication.expiration=12 hours +nifi.registry.kerberos.spnego.principal=HTTP/localhost@LOCALHOST +nifi.registry.kerberos.spnego.keytab.location=/path/to/keytab diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_additional_sensitive_keys.properties b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_additional_sensitive_keys.properties new file mode 100644 index 0000000000..5afb3dd234 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_additional_sensitive_keys.properties @@ -0,0 +1,55 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# web properties # +nifi.registry.web.war.directory=./target/lib +nifi.registry.web.http.host= +nifi.registry.web.http.port=8080 +nifi.registry.web.https.host= +nifi.registry.web.https.port= +nifi.registry.web.jetty.working.directory=./target/work/jetty +nifi.registry.web.jetty.threads=1 + +# security properties # +nifi.registry.security.keystore= +nifi.registry.security.keystoreType= +nifi.registry.security.keystorePasswd= +nifi.registry.security.keyPasswd= +nifi.registry.security.truststore= +nifi.registry.security.truststoreType= +nifi.registry.security.truststorePasswd= +nifi.registry.security.needClientAuth= +nifi.registry.security.authorizers.configuration.file= +nifi.registry.security.authorizer= +nifi.registry.security.identity.providers.configuration.file= +nifi.registry.security.identity.provider= + +# providers properties # +nifi.registry.providers.configuration.file= + +# database properties +nifi.registry.db.directory=./target/db +nifi.registry.db.url.append= + +# kerberos properties # +nifi.registry.kerberos.krb5.file=/path/to/krb5.conf +nifi.registry.kerberos.spnego.authentication.expiration=12 hours +nifi.registry.kerberos.spnego.principal=HTTP/localhost@LOCALHOST +nifi.registry.kerberos.spnego.keytab.location=/path/to/keytab + +# security properties # +nifi.registry.sensitive.props.additional.keys=nifi.registry.web.http.port, nifi.registry.web.http.host, nifi.registry.sensitive.props.additional.keys diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_fully_protected_aes_128.properties b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_fully_protected_aes_128.properties new file mode 100644 index 0000000000..90eb64fc40 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_fully_protected_aes_128.properties @@ -0,0 +1,43 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# web properties # +nifi.registry.web.war.directory=./target/lib +nifi.registry.web.http.host= +nifi.registry.web.http.port=8080 +nifi.registry.web.https.host= +nifi.registry.web.https.port= +nifi.registry.web.jetty.working.directory=./target/work/jetty +nifi.registry.web.jetty.threads=1 + +# security properties # +nifi.registry.security.keystore=/path/to/keystore.jks +nifi.registry.security.keystoreType=JKS +nifi.registry.security.keystorePasswd=6WUpex+VZiN05LXu||joWJMuoSzYniEC7IAoingTimlG7+RGk8I2irl/WTlIuMcg +nifi.registry.security.keystorePasswd.protected=aes/gcm/128 +nifi.registry.security.keyPasswd=6WUpex+VZiN05LXu||joWJMuoSzYniEC7IAoingTimlG7+RGk8I2irl/WTlIuMcg +nifi.registry.security.keyPasswd.protected=aes/gcm/128 +nifi.registry.security.truststore= +nifi.registry.security.truststoreType= +nifi.registry.security.truststorePasswd= +nifi.registry.security.needClientAuth= +nifi.registry.security.authorizers.configuration.file= +nifi.registry.security.authorizer= +nifi.registry.security.identity.providers.configuration.file= +nifi.registry.security.identity.provider= + +nifi.registry.sensitive.props.additional.keys= diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_128.properties b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_128.properties new file mode 100644 index 0000000000..6ecd281922 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_128.properties @@ -0,0 +1,43 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# web properties # +nifi.registry.web.war.directory=./target/lib +nifi.registry.web.http.host= +nifi.registry.web.http.port=8080 +nifi.registry.web.https.host= +nifi.registry.web.https.port= +nifi.registry.web.jetty.working.directory=./target/work/jetty +nifi.registry.web.jetty.threads=1 + +# security properties # +nifi.registry.security.keystore=/path/to/keystore.jks +nifi.registry.security.keystoreType=JKS +nifi.registry.security.keystorePasswd=6WUpex+VZiN05LXu||joWJMuoSzYniEC7IAoingTimlG7+RGk8I2irl/WTlIuMcg +nifi.registry.security.keystorePasswd.protected=aes/gcm/128 +nifi.registry.security.keyPasswd=6WUpex+VZiN05LXu||joWJMuoSzYniEC7IAoingTimlG7+RGk8I2irl/WTlIuMcg +nifi.registry.security.keyPasswd.protected=aes/gcm/128 +nifi.registry.security.truststore= +nifi.registry.security.truststoreType= +nifi.registry.security.truststorePasswd= +nifi.registry.security.needClientAuth= +nifi.registry.security.authorizers.configuration.file= +nifi.registry.security.authorizer= +nifi.registry.security.identity.providers.configuration.file= +nifi.registry.security.identity.provider= + +nifi.registry.sensitive.props.additional.keys=nifi.registry.web.http.port diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_128_password.properties b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_128_password.properties new file mode 100644 index 0000000000..a3b272d99c --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_128_password.properties @@ -0,0 +1,43 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# web properties # +nifi.registry.web.war.directory=./target/lib +nifi.registry.web.http.host= +nifi.registry.web.http.port=8080 +nifi.registry.web.https.host= +nifi.registry.web.https.port= +nifi.registry.web.jetty.working.directory=./target/work/jetty +nifi.registry.web.jetty.threads=1 + +# security properties # +nifi.registry.security.keystore=/path/to/keystore.jks +nifi.registry.security.keystoreType=JKS +nifi.registry.security.keystorePasswd=oa6Aaz5tlFprPuKt||IlVgftF2VqvBIambkP5HVDbRoyKzZl8wwKSw4O9tjHTALA +nifi.registry.security.keystorePasswd.protected=aes/gcm/128 +nifi.registry.security.keyPasswd=oa6Aaz5tlFprPuKt||IlVgftF2VqvBIambkP5HVDbRoyKzZl8wwKSw4O9tjHTALA +nifi.registry.security.keyPasswd.protected=aes/gcm/128 +nifi.registry.security.truststore= +nifi.registry.security.truststoreType= +nifi.registry.security.truststorePasswd= +nifi.registry.security.needClientAuth= +nifi.registry.security.authorizers.configuration.file= +nifi.registry.security.authorizer= +nifi.registry.security.identity.providers.configuration.file= +nifi.registry.security.identity.provider= + +nifi.registry.sensitive.props.additional.keys=nifi.registry.web.http.port diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_256.properties b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_256.properties new file mode 100644 index 0000000000..97aaba0139 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_256.properties @@ -0,0 +1,43 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# web properties # +nifi.registry.web.war.directory=./target/lib +nifi.registry.web.http.host= +nifi.registry.web.http.port=8080 +nifi.registry.web.https.host= +nifi.registry.web.https.port= +nifi.registry.web.jetty.working.directory=./target/work/jetty +nifi.registry.web.jetty.threads=1 + +# security properties # +nifi.registry.security.keystore=/path/to/keystore.jks +nifi.registry.security.keystoreType=JKS +nifi.registry.security.keystorePasswd=oBjT92hIGRElIGOh||MZ6uYuWNBrOA6usq/Jt3DaD2e4otNirZDytac/w/KFe0HOkrJR03vcbo +nifi.registry.security.keystorePasswd.protected=aes/gcm/256 +nifi.registry.security.keyPasswd=ac/BaE35SL/esLiJ||+ULRvRLYdIDA2VqpE0eQXDEMjaLBMG2kbKOdOwBk/hGebDKlVg== +nifi.registry.security.keyPasswd.protected=aes/gcm/256 +nifi.registry.security.truststore= +nifi.registry.security.truststoreType= +nifi.registry.security.truststorePasswd= +nifi.registry.security.needClientAuth= +nifi.registry.security.authorizers.configuration.file= +nifi.registry.security.authorizer= +nifi.registry.security.identity.providers.configuration.file= +nifi.registry.security.identity.provider= + +nifi.registry.sensitive.props.additional.keys=nifi.registry.web.http.port diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_multiple_malformed.properties b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_multiple_malformed.properties new file mode 100644 index 0000000000..d408df06b9 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_multiple_malformed.properties @@ -0,0 +1,43 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# web properties # +nifi.registry.web.war.directory=./target/lib +nifi.registry.web.http.host= +nifi.registry.web.http.port=8080 +nifi.registry.web.https.host= +nifi.registry.web.https.port= +nifi.registry.web.jetty.working.directory=./target/work/jetty +nifi.registry.web.jetty.threads=1 + +# security properties # +nifi.registry.security.keystore=/path/to/keystore.jks +nifi.registry.security.keystoreType=JKS +nifi.registry.security.keystorePasswd=6WUpex+VZiN05LXu||thisIsAnIntentionallyMalformedCipherValue +nifi.registry.security.keystorePasswd.protected=aes/gcm/128 +nifi.registry.security.keyPasswd=6WUpex+VZiN05LXu||thisIsAnIntentionallyMalformedCipherValue +nifi.registry.security.keyPasswd.protected=aes/gcm/128 +nifi.registry.security.truststore= +nifi.registry.security.truststoreType= +nifi.registry.security.truststorePasswd= +nifi.registry.security.needClientAuth= +nifi.registry.security.authorizers.configuration.file= +nifi.registry.security.authorizer= +nifi.registry.security.identity.providers.configuration.file= +nifi.registry.security.identity.provider= + +nifi.registry.sensitive.props.additional.keys= diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_single_malformed.properties b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_single_malformed.properties new file mode 100644 index 0000000000..8552f9e2a4 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_single_malformed.properties @@ -0,0 +1,43 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# web properties # +nifi.registry.web.war.directory=./target/lib +nifi.registry.web.http.host= +nifi.registry.web.http.port=8080 +nifi.registry.web.https.host= +nifi.registry.web.https.port= +nifi.registry.web.jetty.working.directory=./target/work/jetty +nifi.registry.web.jetty.threads=1 + +# security properties # +nifi.registry.security.keystore=/path/to/keystore.jks +nifi.registry.security.keystoreType=JKS +nifi.registry.security.keystorePasswd=6WUpex+VZiN05LXu||thisIsAnIntentionallyMalformedCipherValue +nifi.registry.security.keystorePasswd.protected=aes/gcm/128 +nifi.registry.security.keyPasswd=6WUpex+VZiN05LXu||joWJMuoSzYniEC7IAoingTimlG7+RGk8I2irl/WTlIuMcg +nifi.registry.security.keyPasswd.protected=aes/gcm/128 +nifi.registry.security.truststore= +nifi.registry.security.truststoreType= +nifi.registry.security.truststorePasswd= +nifi.registry.security.needClientAuth= +nifi.registry.security.authorizers.configuration.file= +nifi.registry.security.authorizer= +nifi.registry.security.identity.providers.configuration.file= +nifi.registry.security.identity.provider= + +nifi.registry.sensitive.props.additional.keys= diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_unknown.properties b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_unknown.properties new file mode 100644 index 0000000000..8bd6f4fbba --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_unknown.properties @@ -0,0 +1,43 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# web properties # +nifi.registry.web.war.directory=./target/lib +nifi.registry.web.http.host= +nifi.registry.web.http.port=8080 +nifi.registry.web.https.host= +nifi.registry.web.https.port= +nifi.registry.web.jetty.working.directory=./target/work/jetty +nifi.registry.web.jetty.threads=1 + +# security properties # +nifi.registry.security.keystore=/path/to/keystore.jks +nifi.registry.security.keystoreType=JKS +nifi.registry.security.keystorePasswd=oBjT92hIGRElIGOh||MZ6uYuWNBrOA6usq/Jt3DaD2e4otNirZDytac/w/KFe0HOkrJR03vcbo +nifi.registry.security.keystorePasswd.protected=unknown +nifi.registry.security.keyPasswd=ac/BaE35SL/esLiJ||+ULRvRLYdIDA2VqpE0eQXDEMjaLBMG2kbKOdOwBk/hGebDKlVg== +nifi.registry.security.keyPasswd.protected=unknown +nifi.registry.security.truststore= +nifi.registry.security.truststoreType= +nifi.registry.security.truststorePasswd= +nifi.registry.security.needClientAuth= +nifi.registry.security.authorizers.configuration.file= +nifi.registry.security.authorizer= +nifi.registry.security.identity.providers.configuration.file= +nifi.registry.security.identity.provider= + +nifi.registry.sensitive.props.additional.keys=nifi.registry.web.http.port, nifi.registry.web.http.host diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_unprotected.properties b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_unprotected.properties new file mode 100644 index 0000000000..b0f9f40418 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_unprotected.properties @@ -0,0 +1,41 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# web properties # +nifi.registry.web.war.directory=./target/lib +nifi.registry.web.http.host= +nifi.registry.web.http.port=8080 +nifi.registry.web.https.host= +nifi.registry.web.https.port= +nifi.registry.web.jetty.working.directory=./target/work/jetty +nifi.registry.web.jetty.threads=1 + +# security properties # +nifi.registry.security.keystore=path/to/keystore.jks +nifi.registry.security.keystoreType=JKS +nifi.registry.security.keystorePasswd=thisIsABadKeystorePassword +nifi.registry.security.keyPasswd=thisIsABadKeyPassword +nifi.registry.security.truststore= +nifi.registry.security.truststoreType= +nifi.registry.security.truststorePasswd= +nifi.registry.security.needClientAuth= +nifi.registry.security.authorizers.configuration.file= +nifi.registry.security.authorizer= +nifi.registry.security.identity.providers.configuration.file= +nifi.registry.security.identity.provider= + +nifi.registry.sensitive.props.additional.keys=nifi.registry.web.http.port, nifi.registry.web.http.host diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_unprotected_extra_line.properties b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_unprotected_extra_line.properties new file mode 100644 index 0000000000..34b80a3115 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_unprotected_extra_line.properties @@ -0,0 +1,42 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# web properties # +nifi.registry.web.war.directory=./target/lib +nifi.registry.web.http.host= +nifi.registry.web.http.port=8080 +nifi.registry.web.https.host= +nifi.registry.web.https.port= +nifi.registry.web.jetty.working.directory=./target/work/jetty +nifi.registry.web.jetty.threads=1 + +# security properties # +nifi.registry.security.keystore=path/to/keystore.jks +nifi.registry.security.keystoreType=JKS +nifi.registry.security.keystorePasswd=thisIsABadKeystorePassword +nifi.registry.security.keyPasswd=thisIsABadKeyPassword +nifi.registry.security.keyPasswd.protected= +nifi.registry.security.truststore= +nifi.registry.security.truststoreType= +nifi.registry.security.truststorePasswd= +nifi.registry.security.needClientAuth= +nifi.registry.security.authorizers.configuration.file= +nifi.registry.security.authorizer= +nifi.registry.security.identity.providers.configuration.file= +nifi.registry.security.identity.provider= + +nifi.registry.sensitive.props.additional.keys=nifi.registry.web.http.port, nifi.registry.web.http.host diff --git a/nifi-registry/nifi-registry-core/nifi-registry-provider-api/pom.xml b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/pom.xml new file mode 100644 index 0000000000..e5fb42c63f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/pom.xml @@ -0,0 +1,28 @@ + + + + + org.apache.nifi.registry + nifi-registry-core + 1.14.0-SNAPSHOT + + 4.0.0 + nifi-registry-provider-api + jar + + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/extension/BundleCoordinate.java b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/extension/BundleCoordinate.java new file mode 100644 index 0000000000..3c2c8ed799 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/extension/BundleCoordinate.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension; + +/** + * The coordinate of a bundle. + * + * Implementations of {@link BundlePersistenceProvider} will be expected to be able to delete all versions for a given BundleCoordinate. + */ +public interface BundleCoordinate { + + /** + * @return the NiFi Registry bucket id where the bundle is located + */ + String getBucketId(); + + /** + * @return the group id of the bundle + */ + String getGroupId(); + + /** + * @return the artifact id of the bundle + */ + String getArtifactId(); + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/extension/BundlePersistenceContext.java b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/extension/BundlePersistenceContext.java new file mode 100644 index 0000000000..12b4c32189 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/extension/BundlePersistenceContext.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension; + +/** + * The context that will be passed to the {@link BundlePersistenceProvider} when saving a new version of an extension bundle. + */ +public interface BundlePersistenceContext { + + /** + * @return the unique identifier of the bundle version + */ + BundleVersionCoordinate getCoordinate(); + + /** + * @return the size of the bundle content in bytes + */ + long getSize(); + + /** + * @return the timestamp the bundle was created + */ + long getTimestamp(); + + /** + * @return the user that created the bundle + */ + String getAuthor(); + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/extension/BundlePersistenceException.java b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/extension/BundlePersistenceException.java new file mode 100644 index 0000000000..4acb833d98 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/extension/BundlePersistenceException.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension; + +/** + * An Exception for errors encountered when a BundlePersistenceProvider saves or retrieves a bundle. + */ +public class BundlePersistenceException extends RuntimeException { + + public BundlePersistenceException(String message) { + super(message); + } + + public BundlePersistenceException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/extension/BundlePersistenceProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/extension/BundlePersistenceProvider.java new file mode 100644 index 0000000000..f5c3471b35 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/extension/BundlePersistenceProvider.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension; + +import org.apache.nifi.registry.provider.Provider; + +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Responsible for storing and retrieving the binary content of a version of an extension bundle. + */ +public interface BundlePersistenceProvider extends Provider { + + /** + * Persists the binary content of a version of an extension bundle. + * + * This method should throw a BundlePersistenceException if content already exists for the BundleVersionCoordinate + * specified in the BundlePersistenceContext. + * + * @param context the context about the bundle version being persisted + * @param contentStream the stream of binary content to persist + * @throws BundlePersistenceException if an error occurs storing the content, or if content already exists for version coordinate + */ + void createBundleVersion(BundlePersistenceContext context, InputStream contentStream) throws BundlePersistenceException; + + /** + * Updates the binary content for a version of an extension bundle. + * + * @param context the context about the bundle version being updated + * @param contentStream the stream of the updated binary content + * @throws BundlePersistenceException if an error occurs storing the content + */ + void updateBundleVersion(BundlePersistenceContext context, InputStream contentStream) throws BundlePersistenceException; + + /** + * Writes the binary content of the bundle specified by the bucket-group-artifact-version to the provided OutputStream. + * + * @param versionCoordinate the versionCoordinate of the bundle version + * @param outputStream the output stream to write the contents to + * @throws BundlePersistenceException if an error occurs retrieving the content + */ + void getBundleVersionContent(BundleVersionCoordinate versionCoordinate, OutputStream outputStream) throws BundlePersistenceException; + + /** + * Deletes the content of the bundle version specified by bucket-group-artifact-version. + * + * @param versionCoordinate the versionCoordinate of the bundle version + * @throws BundlePersistenceException if an error occurs deleting the content + */ + void deleteBundleVersion(BundleVersionCoordinate versionCoordinate) throws BundlePersistenceException; + + /** + * Deletes the content for all versions of the bundle specified by group-artifact. + * + * @param bundleCoordinate the coordinate of the bundle to delete all versions for + * @throws BundlePersistenceException if an error occurs deleting the content + */ + void deleteAllBundleVersions(BundleCoordinate bundleCoordinate) throws BundlePersistenceException; + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/extension/BundleVersionCoordinate.java b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/extension/BundleVersionCoordinate.java new file mode 100644 index 0000000000..5dd6146754 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/extension/BundleVersionCoordinate.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension; + +/** + * The coordinate of a version of a bundle. + * + * BundlePersistenceProviders will be expected to retrieve the content of a given BundleVersionCoordinate. + */ +public interface BundleVersionCoordinate { + + /** + * @return the NiFi Registry bucket id where the bundle is located + */ + String getBucketId(); + + /** + * @return the group id of the bundle + */ + String getGroupId(); + + /** + * @return the artifact id of the bundle + */ + String getArtifactId(); + + /** + * @return the version of the bundle + */ + String getVersion(); + + /** + * @return the type of the bundle + */ + BundleVersionType getType(); + + /** + * @return the string representation of the coordinate + */ + String toString(); +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/extension/BundleVersionType.java b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/extension/BundleVersionType.java new file mode 100644 index 0000000000..e5bdd75237 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/extension/BundleVersionType.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.extension; + +/** + * The types of bundles that can be persisted. + */ +public enum BundleVersionType { + + NIFI_NAR, + + MINIFI_CPP; + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/flow/FlowPersistenceException.java b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/flow/FlowPersistenceException.java new file mode 100644 index 0000000000..4287fc89a4 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/flow/FlowPersistenceException.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +/** + * An Exception for errors encountered when a FlowPersistenceProvider saves or retrieves a flow. + */ +public class FlowPersistenceException extends RuntimeException { + + public FlowPersistenceException(String message) { + super(message); + } + + public FlowPersistenceException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/flow/FlowPersistenceProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/flow/FlowPersistenceProvider.java new file mode 100644 index 0000000000..90c872ffac --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/flow/FlowPersistenceProvider.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +import org.apache.nifi.registry.provider.Provider; + +/** + * A service that can store and retrieve flow contents. + * + * The flow contents will be a serialized VersionProcessGroup which came from the flowContents + * field of a VersionedFlowSnapshot. + * + * NOTE: Although this interface is intended to be an extension point, it is not yet considered stable and thus may + * change across releases until the registry matures. + */ +public interface FlowPersistenceProvider extends Provider { + + /** + * Persists the serialized content. + * + * @param context the context for the content being persisted + * @param content the serialized flow content to persist + * @throws FlowPersistenceException if the content could not be persisted + */ + void saveFlowContent(FlowSnapshotContext context, byte[] content) throws FlowPersistenceException; + + /** + * Retrieves the serialized content. + * + * @param bucketId the bucket id where the flow snapshot is located + * @param flowId the id of the versioned flow the snapshot belongs to + * @param version the version of the snapshot + * @return the bytes for the requested snapshot, or null if not found + * @throws FlowPersistenceException if the snapshot could not be retrieved due to an error in underlying provider + */ + byte[] getFlowContent(String bucketId, String flowId, int version) throws FlowPersistenceException; + + /** + * Deletes all content for the versioned flow with the given id in the given bucket. + * + * @param bucketId the bucket the versioned flow belongs to + * @param flowId the id of the versioned flow + * @throws FlowPersistenceException if the snapshots could not be deleted due to an error in underlying provider + */ + void deleteAllFlowContent(String bucketId, String flowId) throws FlowPersistenceException; + + /** + * Deletes the content for the given snapshot. + * + * @param bucketId the bucket id where the snapshot is located + * @param flowId the id of the versioned flow the snapshot belongs to + * @param version the version of the snapshot + * @throws FlowPersistenceException if the snapshot could not be deleted due to an error in underlying provider + */ + void deleteFlowContent(String bucketId, String flowId, int version) throws FlowPersistenceException; + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/flow/FlowSnapshotContext.java b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/flow/FlowSnapshotContext.java new file mode 100644 index 0000000000..569de8c180 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/flow/FlowSnapshotContext.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +/** + * The context that will be passed to the flow provider when saving a snapshot of a versioned flow. + */ +public interface FlowSnapshotContext { + + /** + * @return the id of the bucket this snapshot belongs to + */ + String getBucketId(); + + /** + * @return the name of the bucket this snapshot belongs to + */ + String getBucketName(); + + /** + * @return the id of the versioned flow this snapshot belongs to + */ + String getFlowId(); + + /** + * @return the name of the versioned flow this snapshot belongs to + */ + String getFlowName(); + + /** + * @return the description of the flow this snapshot belongs to + */ + String getFlowDescription(); + + /** + * @return the version of the snapshot + */ + int getVersion(); + + /** + * @return the comments for the snapshot + */ + String getComments(); + + /** + * @return the timestamp the snapshot was created + */ + long getSnapshotTimestamp(); + + /** + * @return the author of the snapshot + */ + String getAuthor(); + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/flow/MetadataAwareFlowPersistenceProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/flow/MetadataAwareFlowPersistenceProvider.java new file mode 100644 index 0000000000..b769380d95 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/flow/MetadataAwareFlowPersistenceProvider.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.flow; + +import org.apache.nifi.registry.metadata.BucketMetadata; + +import java.util.List; + +/** + * A FlowPersistenceProvider that is able to provide metadata about the flows and buckets. + * + * If the application is started with an empty metadata database AND a MetadataAwareFlowPersistenceProvider, + * then the application will use this information to rebuild the database. + * + * NOTE: Some information will be lost, such as created date, last modified date, and original author. + */ +public interface MetadataAwareFlowPersistenceProvider extends FlowPersistenceProvider { + + /** + * @return the list of metadata for each bucket + */ + List getMetadata(); + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/Event.java b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/Event.java new file mode 100644 index 0000000000..294896208f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/Event.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.hook; + +import java.util.List; + +/** + * An event that will be passed to EventHookProviders. + */ +public interface Event { + + /** + * @return the type of the event + */ + EventType getEventType(); + + /** + * @return the fields of the event in the order they were added to the event + */ + List getFields(); + + /** + * @param fieldName the name of the field to return + * @return the EventField with the given name, or null if it does not exist + */ + EventField getField(EventFieldName fieldName); + + /** + * Will be called before publishing the event to ensure the event contains the required + * fields for the given event type in the order specified by the type. + * + * @throws IllegalStateException if the event does not contain the required fields + */ + void validate() throws IllegalStateException; + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventField.java b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventField.java new file mode 100644 index 0000000000..4859266540 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventField.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.hook; + +/** + * A field for an event. + */ +public interface EventField { + + /** + * @return the name of the field + */ + EventFieldName getName(); + + /** + * @return the value of the field + */ + String getValue(); +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventFieldName.java b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventFieldName.java new file mode 100644 index 0000000000..3e35058be6 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventFieldName.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.hook; + +/** + * Enumeration of possible field names for an EventField. + */ +public enum EventFieldName { + + BUCKET_ID, + FLOW_ID, + EXTENSION_BUNDLE_ID, + VERSION, + USER, + USER_ID, + USER_IDENTITY, + USER_GROUP_ID, + USER_GROUP_IDENTITY, + COMMENT +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventHookException.java b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventHookException.java new file mode 100644 index 0000000000..2d91735aad --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventHookException.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.hook; + +/** + * An Exception for errors encountered when a EventHookProvider executes an action before/after a commit. + */ +public class EventHookException extends RuntimeException { + + public EventHookException(String message) { + super(message); + } + + public EventHookException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventHookProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventHookProvider.java new file mode 100644 index 0000000000..8d9f51cdf9 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventHookProvider.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.hook; + +import org.apache.nifi.registry.provider.Provider; + +/** + * An extension point that will be passed events produced by actions take in the registry. + * + * The list of event types can be found in {@link org.apache.nifi.registry.hook.EventType}. + * + * NOTE: Although this interface is intended to be an extension point, it is not yet considered stable and thus may + * change across releases until the registry matures. + */ +public interface EventHookProvider extends Provider { + + /** + * Handles the given event. + * + * @param event the event to handle + * @throws EventHookException if an error occurs handling the event + */ + void handle(Event event) throws EventHookException; + + /** + * Examines the values from the 'Whitelisted Event Type ' properties in the hook provider definition to determine + * if the Event should be invoked for this particular EventType + * + * @param eventType + * EventType that has been fired by the framework. + * + * @return + * True if the hook provider should be 'handled' and false otherwise. + */ + default boolean shouldHandle(EventType eventType) { + return true; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventType.java b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventType.java new file mode 100644 index 0000000000..3568611404 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventType.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.hook; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Enumeration of possible EventTypes with the expected fields for each event. + * + * Producers of events must produce events with the fields in the same order specified here. + */ +public enum EventType { + + CREATE_BUCKET( + EventFieldName.BUCKET_ID, + EventFieldName.USER), + CREATE_FLOW( + EventFieldName.BUCKET_ID, + EventFieldName.FLOW_ID, + EventFieldName.USER), + CREATE_FLOW_VERSION( + EventFieldName.BUCKET_ID, + EventFieldName.FLOW_ID, + EventFieldName.VERSION, + EventFieldName.USER, + EventFieldName.COMMENT), + CREATE_EXTENSION_BUNDLE( + EventFieldName.BUCKET_ID, + EventFieldName.EXTENSION_BUNDLE_ID, + EventFieldName.USER + ), + CREATE_EXTENSION_BUNDLE_VERSION( + EventFieldName.BUCKET_ID, + EventFieldName.EXTENSION_BUNDLE_ID, + EventFieldName.VERSION, + EventFieldName.USER + ), + REGISTRY_START(), + UPDATE_BUCKET( + EventFieldName.BUCKET_ID, + EventFieldName.USER), + UPDATE_FLOW( + EventFieldName.BUCKET_ID, + EventFieldName.FLOW_ID, + EventFieldName.USER), + DELETE_BUCKET( + EventFieldName.BUCKET_ID, + EventFieldName.USER), + DELETE_FLOW( + EventFieldName.BUCKET_ID, + EventFieldName.FLOW_ID, + EventFieldName.USER), + DELETE_EXTENSION_BUNDLE( + EventFieldName.BUCKET_ID, + EventFieldName.EXTENSION_BUNDLE_ID, + EventFieldName.USER + ), + DELETE_EXTENSION_BUNDLE_VERSION( + EventFieldName.BUCKET_ID, + EventFieldName.EXTENSION_BUNDLE_ID, + EventFieldName.VERSION, + EventFieldName.USER + ), + CREATE_USER( + EventFieldName.USER_ID, + EventFieldName.USER_IDENTITY + ), + UPDATE_USER( + EventFieldName.USER_ID, + EventFieldName.USER_IDENTITY + ), + DELETE_USER( + EventFieldName.USER_ID, + EventFieldName.USER_IDENTITY + ), + CREATE_USER_GROUP( + EventFieldName.USER_GROUP_ID, + EventFieldName.USER_GROUP_IDENTITY + ), + UPDATE_USER_GROUP( + EventFieldName.USER_GROUP_ID, + EventFieldName.USER_GROUP_IDENTITY + ), + DELETE_USER_GROUP( + EventFieldName.USER_GROUP_ID, + EventFieldName.USER_GROUP_IDENTITY + ) + ; + + + private List fieldNames; + + EventType(EventFieldName... fieldNames) { + this.fieldNames = Collections.unmodifiableList(Arrays.asList(fieldNames)); + } + + public List getFieldNames() { + return this.fieldNames; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/WhitelistFilteringEventHookProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/WhitelistFilteringEventHookProvider.java new file mode 100644 index 0000000000..24ac3a4c1a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/WhitelistFilteringEventHookProvider.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.hook; + +import org.apache.nifi.registry.provider.ProviderConfigurationContext; +import org.apache.nifi.registry.provider.ProviderCreationException; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public abstract class WhitelistFilteringEventHookProvider + implements EventHookProvider { + + static final String EVENT_WHITELIST_PREFIX = "Whitelisted Event Type "; + static final Pattern EVENT_WHITELIST_PATTERN = Pattern.compile(EVENT_WHITELIST_PREFIX + "\\S+"); + + protected Set whiteListEvents = null; + + @Override + public void onConfigured(ProviderConfigurationContext configurationContext) throws ProviderCreationException { + whiteListEvents = new HashSet<>(); + for (Map.Entry entry : configurationContext.getProperties().entrySet()) { + Matcher matcher = EVENT_WHITELIST_PATTERN.matcher(entry.getKey()); + if (matcher.matches() && (entry.getValue() != null && entry.getValue().length() > 0)) { + whiteListEvents.add(EventType.valueOf(entry.getValue())); + } + + } + } + + /** + * Standard method for deciding if the EventType should be handled by the Hook provider or not. + * + * @param eventType + * EventType that was fired by the framework. + * + * @return + * True if the EventType is in the whitelist set and false otherwise. + */ + @Override + public boolean shouldHandle(EventType eventType) { + if (whiteListEvents != null && whiteListEvents.size() > 0) { + if (whiteListEvents.contains(eventType)) { + return true; + } + } else { + // If the whitelist property is not set or empty we want to fire for all events. + return true; + } + return false; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/metadata/BucketMetadata.java b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/metadata/BucketMetadata.java new file mode 100644 index 0000000000..a145c5afad --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/metadata/BucketMetadata.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.metadata; + +import java.util.List; + +/** + * Meatadata about a bucket returned from MetadataAwareFlowPersistenceProvider. + */ +public class BucketMetadata { + + private String identifier; + + private String name; + + private String description; + + private List flowMetadata; + + + public String getIdentifier() { + return identifier; + } + + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public List getFlowMetadata() { + return flowMetadata; + } + + public void setFlowMetadata(List flowMetadata) { + this.flowMetadata = flowMetadata; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/metadata/FlowMetadata.java b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/metadata/FlowMetadata.java new file mode 100644 index 0000000000..da49565d92 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/metadata/FlowMetadata.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.metadata; + +import java.util.List; + +/** + * Meatadata about a flow returned from MetadataAwareFlowPersistenceProvider. + */ +public class FlowMetadata { + + private String identifier; + + private String name; + + private String description; + + private List flowSnapshotMetadata; + + + public String getIdentifier() { + return identifier; + } + + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public List getFlowSnapshotMetadata() { + return flowSnapshotMetadata; + } + + public void setFlowSnapshotMetadata(List flowSnapshotMetadata) { + this.flowSnapshotMetadata = flowSnapshotMetadata; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/metadata/FlowSnapshotMetadata.java b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/metadata/FlowSnapshotMetadata.java new file mode 100644 index 0000000000..2d27df1cb2 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/metadata/FlowSnapshotMetadata.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.metadata; + +/** + * Meatadata about a snapshot returned from MetadataAwareFlowPersistenceProvider. + */ +public class FlowSnapshotMetadata { + + private Integer version; + + private String author; + + private String comments; + + private Long created; + + + public Integer getVersion() { + return version; + } + + public void setVersion(Integer version) { + this.version = version; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + public String getComments() { + return comments; + } + + public void setComments(String comments) { + this.comments = comments; + } + + public Long getCreated() { + return created; + } + + public void setCreated(Long created) { + this.created = created; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/provider/Provider.java b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/provider/Provider.java new file mode 100644 index 0000000000..7f79d54e49 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/provider/Provider.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider; + +/** + * Base interface for providers. + */ +public interface Provider { + + /** + * Called to configure the Provider. + * + * @param configurationContext the context containing configuration for the given provider + * @throws ProviderCreationException if an error occurs while the provider is configured + */ + void onConfigured(ProviderConfigurationContext configurationContext) throws ProviderCreationException; + + /** + * Called prior to destroying the provider. + */ + default void preDestruction() { + + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/provider/ProviderConfigurationContext.java b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/provider/ProviderConfigurationContext.java new file mode 100644 index 0000000000..b4f7ed6ef4 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/provider/ProviderConfigurationContext.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider; + +import java.util.Map; + +/** + * A context that will passed to providers in order to obtain configuration. + */ +public interface ProviderConfigurationContext { + + /** + * Retrieves all properties the provider currently understands regardless + * of whether a value has been set for them or not. If no value is present + * then its value is null and thus any registered default for the property + * descriptor applies. + * + * @return Map of all properties + */ + Map getProperties(); + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/provider/ProviderContext.java b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/provider/ProviderContext.java new file mode 100644 index 0000000000..97291767a6 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/provider/ProviderContext.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation for setter methods in a provider to indicate the framework should inject the requested resource. + */ +@Documented +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface ProviderContext { +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/provider/ProviderCreationException.java b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/provider/ProviderCreationException.java new file mode 100644 index 0000000000..d1e106c942 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/provider/ProviderCreationException.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.provider; + +/** + * An exception that will be thrown if a provider can not be created. + */ +public class ProviderCreationException extends RuntimeException { + + public ProviderCreationException() { + } + + public ProviderCreationException(String message) { + super(message); + } + + public ProviderCreationException(String message, Throwable cause) { + super(message, cause); + } + + public ProviderCreationException(Throwable cause) { + super(cause); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-resources/pom.xml b/nifi-registry/nifi-registry-core/nifi-registry-resources/pom.xml new file mode 100644 index 0000000000..bf17246edb --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-resources/pom.xml @@ -0,0 +1,50 @@ + + + + 4.0.0 + + org.apache.nifi.registry + nifi-registry-core + 1.14.0-SNAPSHOT + + nifi-registry-resources + pom + holds common resources used to build installers + + + + maven-assembly-plugin + + true + + + + make shared resource + + single + + package + + + src/main/assembly/dependencies.xml + + + + + + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/assembly/dependencies.xml b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/assembly/dependencies.xml new file mode 100644 index 0000000000..fed8098942 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/assembly/dependencies.xml @@ -0,0 +1,36 @@ + + + + resources + + zip + + false + + + src/main/resources + / + + + src/main/resources/bin + /bin/ + + nifi-registry.sh + + 0750 + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/dump-nifi-registry.bat b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/dump-nifi-registry.bat new file mode 100644 index 0000000000..31e72c0f63 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/dump-nifi-registry.bat @@ -0,0 +1,49 @@ +@echo off +rem +rem Licensed to the Apache Software Foundation (ASF) under one or more +rem contributor license agreements. See the NOTICE file distributed with +rem this work for additional information regarding copyright ownership. +rem The ASF licenses this file to You under the Apache License, Version 2.0 +rem (the "License"); you may not use this file except in compliance with +rem the License. You may obtain a copy of the License at +rem +rem http://www.apache.org/licenses/LICENSE-2.0 +rem +rem Unless required by applicable law or agreed to in writing, software +rem distributed under the License is distributed on an "AS IS" BASIS, +rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +rem See the License for the specific language governing permissions and +rem limitations under the License. +rem + +rem Use JAVA_HOME if it's set; otherwise, just use java + +if "%JAVA_HOME%" == "" goto noJavaHome +if not exist "%JAVA_HOME%\bin\java.exe" goto noJavaHome +set JAVA_EXE=%JAVA_HOME%\bin\java.exe +goto startNiFiRegistry + +:noJavaHome +echo The JAVA_HOME environment variable is not defined correctly. +echo Instead the PATH will be used to find the java executable. +echo. +set JAVA_EXE=java +goto startNiFiRegistry + +:startNiFiRegistry +set NIFI_REGISTRY_ROOT=%~dp0..\ +pushd "%NIFI_REGISTRY%" +set LIB_DIR=%NIFI_REGISTRY_ROOT%\lib +set SHARED_DIR=%NIFI_REGISTRY_ROOT%\lib\shared +set BOOTSTRAP_DIR=%NIFI_REGISTRY_ROOT%\lib\bootstrap +set CONF_DIR=conf + +set BOOTSTRAP_CONF_FILE=%CONF_DIR%\bootstrap.conf +set JAVA_ARGS=-Dorg.apache.nifi.registry.bootstrap.config.file=%BOOTSTRAP_CONF_FILE% + +SET JAVA_PARAMS=-cp %CONF_DIR%;%LIB_DIR%\*;%SHARED_DIR%\*;%BOOTSTRAP_DIR%\* -Xms12m -Xmx24m %JAVA_ARGS% org.apache.nifi.registry.NiFiRegistry +set BOOTSTRAP_ACTION=dump + +cmd.exe /C "%JAVA_EXE%" %JAVA_PARAMS% %BOOTSTRAP_ACTION% + +popd diff --git a/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/nifi-registry-env.sh b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/nifi-registry-env.sh new file mode 100644 index 0000000000..216d484402 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/nifi-registry-env.sh @@ -0,0 +1,28 @@ +#!/bin/sh +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# The java implementation to use. +#export JAVA_HOME=/usr/java/jdk1.8.0/ + +export NIFI_REGISTRY_HOME=$(cd "${SCRIPT_DIR}" && cd .. && pwd) + +#The directory for the NiFi Registry pid file +export NIFI_REGISTRY_PID_DIR="${NIFI_REGISTRY_HOME}/run" + +#The directory for NiFi Registry log files +export NIFI_REGISTRY_LOG_DIR="${NIFI_REGISTRY_HOME}/logs" \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/nifi-registry.sh b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/nifi-registry.sh new file mode 100644 index 0000000000..ef7619926c --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/nifi-registry.sh @@ -0,0 +1,357 @@ +#!/bin/sh +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +# Script structure inspired from Apache Karaf and other Apache projects with similar startup approaches + +# Discover the path of the file + + +# Since MacOS X, FreeBSD and some other systems lack gnu readlink, we use a more portable +# approach based on following StackOverflow comment https://stackoverflow.com/a/1116890/888876 + +TARGET_FILE=$0 + +cd $(dirname $TARGET_FILE) +TARGET_FILE=$(basename $TARGET_FILE) + +# Iterate down a (possible) chain of symlinks +while [ -L "$TARGET_FILE" ] +do + TARGET_FILE=$(readlink $TARGET_FILE) + cd $(dirname $TARGET_FILE) + TARGET_FILE=$(basename $TARGET_FILE) +done + +# Compute the canonicalized name by finding the physical path +# for the directory we're in and appending the target file. +PHYS_DIR=$(pwd -P) + +SCRIPT_DIR=$PHYS_DIR +PROGNAME=$(basename "$0") + +. "${SCRIPT_DIR}/nifi-registry-env.sh" + + + +warn() { + echo "${PROGNAME}: $*" +} + +die() { + warn "$*" + exit 1 +} + +detectOS() { + # OS specific support (must be 'true' or 'false'). + cygwin=false; + aix=false; + os400=false; + darwin=false; + case "$(uname)" in + CYGWIN*) + cygwin=true + ;; + AIX*) + aix=true + ;; + OS400*) + os400=true + ;; + Darwin) + darwin=true + ;; + esac + # For AIX, set an environment variable + if ${aix}; then + export LDR_CNTRL=MAXDATA=0xB0000000@DSA + echo ${LDR_CNTRL} + fi + # In addition to those, go around the linux space and query the widely + # adopted /etc/os-release to detect linux variants + if [ -f /etc/os-release ] + then + . /etc/os-release + fi +} + +unlimitFD() { + # Use the maximum available, or set MAX_FD != -1 to use that + if [ "x${MAX_FD}" = "x" ]; then + MAX_FD="maximum" + fi + + # Increase the maximum file descriptors if we can + if [ "${os400}" = "false" ] && [ "${cygwin}" = "false" ]; then + MAX_FD_LIMIT=$(ulimit -H -n) + if [ "${MAX_FD_LIMIT}" != 'unlimited' ]; then + if [ $? -eq 0 ]; then + if [ "${MAX_FD}" = "maximum" -o "${MAX_FD}" = "max" ]; then + # use the system max + MAX_FD="${MAX_FD_LIMIT}" + fi + + ulimit -n ${MAX_FD} > /dev/null + # echo "ulimit -n" `ulimit -n` + if [ $? -ne 0 ]; then + warn "Could not set maximum file descriptor limit: ${MAX_FD}" + fi + else + warn "Could not query system maximum file descriptor limit: ${MAX_FD_LIMIT}" + fi + fi + fi +} + + + +locateJava() { + # Setup the Java Virtual Machine + if $cygwin ; then + [ -n "${JAVA}" ] && JAVA=$(cygpath --unix "${JAVA}") + [ -n "${JAVA_HOME}" ] && JAVA_HOME=$(cygpath --unix "${JAVA_HOME}") + fi + + if [ "x${JAVA}" = "x" ] && [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi + if [ "x${JAVA}" = "x" ]; then + if [ "x${JAVA_HOME}" != "x" ]; then + if [ ! -d "${JAVA_HOME}" ]; then + die "JAVA_HOME is not valid: ${JAVA_HOME}" + fi + JAVA="${JAVA_HOME}/bin/java" + else + warn "JAVA_HOME not set; results may vary" + JAVA=$(type java) + JAVA=$(expr "${JAVA}" : '.* \(/.*\)$') + if [ "x${JAVA}" = "x" ]; then + die "java command not found" + fi + fi + fi + # if command is env, attempt to add more to the classpath + if [ "$1" = "env" ]; then + [ "x${TOOLS_JAR}" = "x" ] && [ -n "${JAVA_HOME}" ] && TOOLS_JAR=$(find -H "${JAVA_HOME}" -name "tools.jar") + [ "x${TOOLS_JAR}" = "x" ] && [ -n "${JAVA_HOME}" ] && TOOLS_JAR=$(find -H "${JAVA_HOME}" -name "classes.jar") + if [ "x${TOOLS_JAR}" = "x" ]; then + warn "Could not locate tools.jar or classes.jar. Please set manually to avail all command features." + fi + fi + +} + +init() { + # Determine if there is special OS handling we must perform + detectOS + + # Unlimit the number of file descriptors if possible + unlimitFD + + # Locate the Java VM to execute + locateJava "$1" +} + + +install() { + detectOS + + if [ "${darwin}" = "true" ] || [ "${cygwin}" = "true" ]; then + echo 'Installing Apache NiFi Registry as a service is not supported on OS X or Cygwin.' + exit 1 + fi + + SVC_NAME=nifi-registry + if [ "x$2" != "x" ] ; then + SVC_NAME=$2 + fi + + # since systemd seems to honour /etc/init.d we don't still create native systemd services + # yet... + initd_dir='/etc/init.d' + SVC_FILE="${initd_dir}/${SVC_NAME}" + + if [ ! -w "${initd_dir}" ]; then + echo "Current user does not have write permissions to ${initd_dir}. Cannot install NiFi Registry as a service." + exit 1 + fi + +# Create the init script, overwriting anything currently present +cat < ${SVC_FILE} +#!/bin/sh + +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# +# chkconfig: 2345 20 80 +# description: Apache NiFi Registry is a complementary application that provides a central location for storage and management of shared resources across one or more instances of NiFi and/or MiNiFi. + +# Make use of the configured NIFI_REGISTRY_HOME directory and pass service requests to the nifi-registry.sh executable +NIFI_REGISTRY_HOME=${NIFI_REGISTRY_HOME} +bin_dir=\${NIFI_REGISTRY_HOME}/bin +nifi_registry_executable=\${bin_dir}/nifi-registry.sh + +\${nifi_registry_executable} "\$@" +SERVICEDESCRIPTOR + + if [ ! -f "${SVC_FILE}" ]; then + echo "Could not create service file ${SVC_FILE}" + exit 1 + fi + + # Provide the user execute access on the file + chmod u+x ${SVC_FILE} + + + # If SLES or OpenSuse... + if [ "${ID}" = "opensuse" ] || [ "${ID}" = "sles" ]; then + rm -f "/etc/rc.d/rc2.d/S65${SVC_NAME}" + ln -s "/etc/init.d/${SVC_NAME}" "/etc/rc.d/rc2.d/S65${SVC_NAME}" || { echo "Could not create link /etc/rc.d/rc2.d/S65${SVC_NAME}"; exit 1; } + rm -f "/etc/rc.d/rc2.d/K65${SVC_NAME}" + ln -s "/etc/init.d/${SVC_NAME}" "/etc/rc.d/rc2.d/K65${SVC_NAME}" || { echo "Could not create link /etc/rc.d/rc2.d/K65${SVC_NAME}"; exit 1; } + echo "Service ${SVC_NAME} installed" + # Anything other fallback to the old approach + else + rm -f "/etc/rc2.d/S65${SVC_NAME}" + ln -s "/etc/init.d/${SVC_NAME}" "/etc/rc2.d/S65${SVC_NAME}" || { echo "Could not create link /etc/rc2.d/S65${SVC_NAME}"; exit 1; } + rm -f "/etc/rc2.d/K65${SVC_NAME}" + ln -s "/etc/init.d/${SVC_NAME}" "/etc/rc2.d/K65${SVC_NAME}" || { echo "Could not create link /etc/rc2.d/K65${SVC_NAME}"; exit 1; } + echo "Service ${SVC_NAME} installed" + fi +} + +run() { + BOOTSTRAP_CONF_DIR="${NIFI_REGISTRY_HOME}/conf" + BOOTSTRAP_CONF="${BOOTSTRAP_CONF_DIR}/bootstrap.conf"; + BOOTSTRAP_LIBS="${NIFI_REGISTRY_HOME}/lib/bootstrap/*" + SHARED_LIBS="${NIFI_REGISTRY_HOME}/lib/shared/*" + + run_as_user=$(grep '^\s*run.as' "${BOOTSTRAP_CONF}" | cut -d'=' -f2) + # If the run as user is the same as that starting the process, ignore this configuration + if [ "${run_as_user}" = "$(whoami)" ]; then + unset run_as_user + fi + + if $cygwin; then + if [ -n "${run_as_user}" ]; then + echo "The run.as option is not supported in a Cygwin environment. Exiting." + exit 1 + fi; + + NIFI_REGISTRY_HOME=$(cygpath --path --windows "${NIFI_REGISTRY_HOME}") + NIFI_REGISTRY_LOG_DIR=$(cygpath --path --windows "${NIFI_REGISTRY_LOG_DIR}") + NIFI_REGISTRY_PID_DIR=$(cygpath --path --windows "${NIFI_REGISTRY_PID_DIR}") + BOOTSTRAP_CONF=$(cygpath --path --windows "${BOOTSTRAP_CONF}") + BOOTSTRAP_CONF_DIR=$(cygpath --path --windows "${BOOTSTRAP_CONF_DIR}") + BOOTSTRAP_LIBS=$(cygpath --path --windows "${BOOTSTRAP_LIBS}") + SHARED_LIBS=$(cygpath --path --windows "${SHARED_LIBS}") + BOOTSTRAP_CLASSPATH="${BOOTSTRAP_CONF_DIR};${SHARED_LIBS};${BOOTSTRAP_LIBS}" + if [ -n "${TOOLS_JAR}" ]; then + TOOLS_JAR=$(cygpath --path --windows "${TOOLS_JAR}") + BOOTSTRAP_CLASSPATH="${TOOLS_JAR};${BOOTSTRAP_CLASSPATH}" + fi + else + if [ -n "${run_as_user}" ]; then + if ! id -u "${run_as_user}" >/dev/null 2>&1; then + echo "The specified run.as user ${run_as_user} does not exist. Exiting." + exit 1 + fi + fi; + BOOTSTRAP_CLASSPATH="${BOOTSTRAP_CONF_DIR}:${SHARED_LIBS}:${BOOTSTRAP_LIBS}" + if [ -n "${TOOLS_JAR}" ]; then + BOOTSTRAP_CLASSPATH="${TOOLS_JAR}:${BOOTSTRAP_CLASSPATH}" + fi + fi + + echo + echo "Java home: ${JAVA_HOME}" + echo "NiFi Registry home: ${NIFI_REGISTRY_HOME}" + echo + echo "Bootstrap Config File: ${BOOTSTRAP_CONF}" + echo + + # run 'start' in the background because the process will continue to run, monitoring NiFi Registry. + # all other commands will terminate quickly so want to just wait for them + + #setup directory parameters + BOOTSTRAP_LOG_PARAMS="-Dorg.apache.nifi.registry.bootstrap.config.log.dir='${NIFI_REGISTRY_LOG_DIR}'" + BOOTSTRAP_PID_PARAMS="-Dorg.apache.nifi.registry.bootstrap.config.pid.dir='${NIFI_REGISTRY_PID_DIR}'" + BOOTSTRAP_CONF_PARAMS="-Dorg.apache.nifi.registry.bootstrap.config.file='${BOOTSTRAP_CONF}'" + + BOOTSTRAP_DIR_PARAMS="${BOOTSTRAP_LOG_PARAMS} ${BOOTSTRAP_PID_PARAMS} ${BOOTSTRAP_CONF_PARAMS}" + + run_nifi_registry_cmd="'${JAVA}' -cp '${BOOTSTRAP_CLASSPATH}' -Xms12m -Xmx24m ${BOOTSTRAP_DIR_PARAMS} org.apache.nifi.registry.bootstrap.RunNiFiRegistry $@" + + if [ -n "${run_as_user}" ]; then + # Provide SCRIPT_DIR and execute nifi-env for the run.as user command + run_nifi_registry_cmd="sudo -u ${run_as_user} sh -c \"SCRIPT_DIR='${SCRIPT_DIR}' && . '${SCRIPT_DIR}/nifi-registry-env.sh' && ${run_nifi_registry_cmd}\"" + fi + + if [ "$1" = "run" ]; then + # Use exec to handover PID to RunNiFi java process, instead of forking it as a child process + run_nifi_registry_cmd="exec ${run_nifi_registry_cmd}" + fi + + if [ "$1" = "start" ]; then + ( eval "cd ${NIFI_REGISTRY_HOME} && ${run_nifi_registry_cmd}" & )> /dev/null 1>&- + else + eval "cd ${NIFI_REGISTRY_HOME} && ${run_nifi_registry_cmd}" + fi + EXIT_STATUS=$? + + # Wait just a bit (3 secs) to wait for the logging to finish and then echo a new-line. + # We do this to avoid having logs spewed on the console after running the command and then not giving + # control back to the user + sleep 3 + echo +} + +main() { + init "$1" + run "$@" +} + + +case "$1" in + install) + install "$@" + ;; + start|stop|run|status|dump|env) + main "$@" + ;; + restart) + init + run "stop" + run "start" + ;; + *) + echo "Usage nifi-registry {start|stop|run|restart|status|dump|install}" + ;; +esac diff --git a/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/run-nifi-registry.bat b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/run-nifi-registry.bat new file mode 100644 index 0000000000..34d9c8dda2 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/run-nifi-registry.bat @@ -0,0 +1,50 @@ +@echo off +rem +rem Licensed to the Apache Software Foundation (ASF) under one or more +rem contributor license agreements. See the NOTICE file distributed with +rem this work for additional information regarding copyright ownership. +rem The ASF licenses this file to You under the Apache License, Version 2.0 +rem (the "License"); you may not use this file except in compliance with +rem the License. You may obtain a copy of the License at +rem +rem http://www.apache.org/licenses/LICENSE-2.0 +rem +rem Unless required by applicable law or agreed to in writing, software +rem distributed under the License is distributed on an "AS IS" BASIS, +rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +rem See the License for the specific language governing permissions and +rem limitations under the License. +rem + +rem Use JAVA_HOME if it's set; otherwise, just use java + +if "%JAVA_HOME%" == "" goto noJavaHome +if not exist "%JAVA_HOME%\bin\java.exe" goto noJavaHome +set JAVA_EXE=%JAVA_HOME%\bin\java.exe +goto startNiFiRegistry + +:noJavaHome +echo The JAVA_HOME environment variable is not defined correctly. +echo Instead the PATH will be used to find the java executable. +echo. +set JAVA_EXE=java +goto startNiFiRegistry + +:startNiFiRegistry +set NIFI_REGISTRY_ROOT=%~dp0.. +pushd "%NIFI_REGISTRY_ROOT%\" +set LIB_DIR=%NIFI_REGISTRY_ROOT%\lib +set SHARED_DIR=%NIFI_REGISTRY_ROOT%\lib\shared +set BOOTSTRAP_DIR=%NIFI_REGISTRY_ROOT%\lib\bootstrap +set CONF_DIR=%NIFI_REGISTRY_ROOT%\conf + +set BOOTSTRAP_CONF_FILE=%CONF_DIR%\bootstrap.conf +set JAVA_ARGS=-Dorg.apache.nifi.registry.bootstrap.config.file=%BOOTSTRAP_CONF_FILE% + +SET JAVA_PARAMS=-cp %CONF_DIR%;%LIB_DIR%\*;%SHARED_DIR%\*;%BOOTSTRAP_DIR%\* -Xms512m -Xmx1024m %JAVA_ARGS% org.apache.nifi.registry.NiFiRegistry +set BOOTSTRAP_ACTION=run + +echo cmd.exe /C "%JAVA_EXE%" %JAVA_PARAMS% %BOOTSTRAP_ACTION% +cmd.exe /C "%JAVA_EXE%" %JAVA_PARAMS% %BOOTSTRAP_ACTION% + +popd diff --git a/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/status-nifi-registry.bat b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/status-nifi-registry.bat new file mode 100644 index 0000000000..c6ef5a28f8 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/status-nifi-registry.bat @@ -0,0 +1,50 @@ +@echo off +rem +rem Licensed to the Apache Software Foundation (ASF) under one or more +rem contributor license agreements. See the NOTICE file distributed with +rem this work for additional information regarding copyright ownership. +rem The ASF licenses this file to You under the Apache License, Version 2.0 +rem (the "License"); you may not use this file except in compliance with +rem the License. You may obtain a copy of the License at +rem +rem http://www.apache.org/licenses/LICENSE-2.0 +rem +rem Unless required by applicable law or agreed to in writing, software +rem distributed under the License is distributed on an "AS IS" BASIS, +rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +rem See the License for the specific language governing permissions and +rem limitations under the License. +rem + +rem Use JAVA_HOME if it's set; otherwise, just use java + +if "%JAVA_HOME%" == "" goto noJavaHome +if not exist "%JAVA_HOME%\bin\java.exe" goto noJavaHome +set JAVA_EXE=%JAVA_HOME%\bin\java.exe +goto startNiFiRegistry + +:noJavaHome +echo The JAVA_HOME environment variable is not defined correctly. +echo Instead the PATH will be used to find the java executable. +echo. +set JAVA_EXE=java +goto startNiFiRegistry + +:startNiFiRegistry +set NIFI_REGISTRY_ROOT=%~dp0..\ +pushd "%NIFI_REGISTRY_ROOT%" +set LIB_DIR=%NIFI_REGISTRY_ROOT%\lib +set SHARED_DIR=%NIFI_REGISTRY_ROOT%\lib\shared +set BOOTSTRAP_DIR=%NIFI_REGISTRY_ROOT%\lib\bootstrap +set CONF_DIR=conf +set NIFI_REGISTRY_LOG_DIR=%NIFI_REGISTRY_ROOT%\logs + +set BOOTSTRAP_CONF_FILE=%CONF_DIR%\bootstrap.conf +set JAVA_ARGS=-Dorg.apache.nifi.registry.bootstrap.config.file=%BOOTSTRAP_CONF_FILE% -Dorg.apache.nifi.registry.bootstrap.config.log.dir=%NIFI_REGISTRY_LOG_DIR% + +set JAVA_PARAMS=-cp %LIB_DIR%\*;%SHARED_DIR%\*;%BOOTSTRAP_DIR%\* -Xms12m -Xmx24m %JAVA_ARGS% org.apache.nifi.registry.NiFiRegistry +set BOOTSTRAP_ACTION=status + +cmd.exe /C "%JAVA_EXE%" %JAVA_PARAMS% %BOOTSTRAP_ACTION% + +popd diff --git a/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/authorizers.xml b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/authorizers.xml new file mode 100644 index 0000000000..9db7acad82 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/authorizers.xml @@ -0,0 +1,323 @@ + + + + + + + + file-user-group-provider + org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider + ./conf/users.xml + + + + + + + + + + + + + + + + + + + + + file-access-policy-provider + org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider + file-user-group-provider + ./conf/authorizations.xml + + + + + + + + + + + + managed-authorizer + org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer + file-access-policy-provider + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/bootstrap.conf b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/bootstrap.conf new file mode 100644 index 0000000000..e4bf3135ae --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/bootstrap.conf @@ -0,0 +1,54 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# Java command to use when running nifi-registry +java=java + +# Username to use when running nifi-registry. This value will be ignored on Windows. +run.as= + +# Configure the working directory for launching the NiFi Registry process +# If not specified, the working directory will fall back to using the NIFI_REGISTRY_HOME env variable +# If the environment variable is not specified, the working directory will fall back to the parent of this file's parent +working.dir= + +# Configure where nifi-registry's lib and conf directories live +lib.dir=./lib +conf.dir=./conf +docs.dir=./docs + +# How long to wait after telling nifi-registry to shutdown before explicitly killing the Process +graceful.shutdown.seconds=20 + +# Disable JSR 199 so that we can use JSP's without running a JDK +java.arg.1=-Dorg.apache.jasper.compiler.disablejsr199=true + +# JVM memory settings +java.arg.2=-Xms512m +java.arg.3=-Xmx512m + +# Enable Remote Debugging +#java.arg.debug=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000 + +java.arg.4=-Djava.net.preferIPv4Stack=true + +# allowRestrictedHeaders is required for Cluster/Node communications to work properly +java.arg.5=-Dsun.net.http.allowRestrictedHeaders=true +java.arg.6=-Djava.protocol.handler.pkgs=sun.net.www.protocol + +# Master key in hexadecimal format for encrypted sensitive configuration values +nifi.registry.bootstrap.sensitive.key= \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/identity-providers.xml b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/identity-providers.xml new file mode 100644 index 0000000000..1e8cf6467e --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/identity-providers.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/logback.xml b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/logback.xml new file mode 100644 index 0000000000..e05e95fc1b --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/logback.xml @@ -0,0 +1,122 @@ + + + + + true + + + + ${org.apache.nifi.registry.bootstrap.config.log.dir}/nifi-registry-app.log + + + ${org.apache.nifi.registry.bootstrap.config.log.dir}/nifi-registry-app_%d{yyyy-MM-dd_HH}.%i.log + 100MB + + 30 + + 10GB + + true + + %date %level [%thread] %logger{40} %msg%n + + + + + ${org.apache.nifi.registry.bootstrap.config.log.dir}/nifi-registry-bootstrap.log + + + ${org.apache.nifi.registry.bootstrap.config.log.dir}/nifi-registry-bootstrap_%d.log + + 5 + + + %date %level [%thread] %logger{40} %msg%n + + + + + ${org.apache.nifi.registry.bootstrap.config.log.dir}/nifi-registry-event.log + + + ${org.apache.nifi.registry.bootstrap.config.log.dir}/nifi-registry-event_%d.log + + 5 + + + %date ## %msg%n + + + + + + %date %level [%thread] %logger{40} %msg%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties new file mode 100644 index 0000000000..db5b429dec --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties @@ -0,0 +1,113 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +# web properties # +nifi.registry.web.war.directory=${nifi.registry.web.war.directory} +nifi.registry.web.http.host=${nifi.registry.web.http.host} +nifi.registry.web.http.port=${nifi.registry.web.http.port} +nifi.registry.web.https.host=${nifi.registry.web.https.host} +nifi.registry.web.https.port=${nifi.registry.web.https.port} +nifi.registry.web.jetty.working.directory=${nifi.registry.jetty.work.dir} +nifi.registry.web.jetty.threads=${nifi.registry.web.jetty.threads} +nifi.registry.web.should.send.server.version=${nifi.registry.web.should.send.server.version} + +# security properties # +nifi.registry.security.keystore=${nifi.registry.security.keystore} +nifi.registry.security.keystoreType=${nifi.registry.security.keystoreType} +nifi.registry.security.keystorePasswd=${nifi.registry.security.keystorePasswd} +nifi.registry.security.keyPasswd=${nifi.registry.security.keyPasswd} +nifi.registry.security.truststore=${nifi.registry.security.truststore} +nifi.registry.security.truststoreType=${nifi.registry.security.truststoreType} +nifi.registry.security.truststorePasswd=${nifi.registry.security.truststorePasswd} +nifi.registry.security.needClientAuth=${nifi.registry.security.needClientAuth} +nifi.registry.security.authorizers.configuration.file=${nifi.registry.security.authorizers.configuration.file} +nifi.registry.security.authorizer=${nifi.registry.security.authorizer} +nifi.registry.security.identity.providers.configuration.file=${nifi.registry.security.identity.providers.configuration.file} +nifi.registry.security.identity.provider=${nifi.registry.security.identity.provider} + +# sensitive property protection properties # +# nifi.registry.sensitive.props.additional.keys= + +# providers properties # +nifi.registry.providers.configuration.file=${nifi.registry.providers.configuration.file} + +# registry alias properties # +nifi.registry.registry.alias.configuration.file=${nifi.registry.registry.alias.configuration.file} + +# extensions working dir # +nifi.registry.extensions.working.directory=${nifi.registry.extensions.working.directory} + +# legacy database properties, used to migrate data from original DB to new DB below +# NOTE: Users upgrading from 0.1.0 should leave these populated, but new installs after 0.1.0 should leave these empty +nifi.registry.db.directory=${nifi.registry.db.directory} +nifi.registry.db.url.append=${nifi.registry.db.url.append} + +# database properties +nifi.registry.db.url=${nifi.registry.db.url} +nifi.registry.db.driver.class=${nifi.registry.db.driver.class} +nifi.registry.db.driver.directory=${nifi.registry.db.driver.directory} +nifi.registry.db.username=${nifi.registry.db.username} +nifi.registry.db.password=${nifi.registry.db.password} +nifi.registry.db.maxConnections=${nifi.registry.db.maxConnections} +nifi.registry.db.sql.debug=${nifi.registry.db.sql.debug} + +# extension directories # +# Each property beginning with "nifi.registry.extension.dir." will be treated as location for an extension, +# and a class loader will be created for each location, with the system class loader as the parent +# +#nifi.registry.extension.dir.1=/path/to/extension1 +#nifi.registry.extension.dir.2=/path/to/extension2 + +nifi.registry.extension.dir.aws=${nifi.registry.extension.dir.aws} + +# Identity Mapping Properties # +# These properties allow normalizing user identities such that identities coming from different identity providers +# (certificates, LDAP, Kerberos) can be treated the same internally in NiFi. The following example demonstrates normalizing +# DNs from certificates and principals from Kerberos into a common identity string: +# +# nifi.registry.security.identity.mapping.pattern.dn=^CN=(.*?), OU=(.*?), O=(.*?), L=(.*?), ST=(.*?), C=(.*?)$ +# nifi.registry.security.identity.mapping.value.dn=$1@$2 +# nifi.registry.security.identity.mapping.transform.dn=NONE + +# nifi.registry.security.identity.mapping.pattern.kerb=^(.*?)/instance@(.*?)$ +# nifi.registry.security.identity.mapping.value.kerb=$1@$2 +# nifi.registry.security.identity.mapping.transform.kerb=UPPER + +# Group Mapping Properties # +# These properties allow normalizing group names coming from external sources like LDAP. The following example +# lowercases any group name. +# +# nifi.registry.security.group.mapping.pattern.anygroup=^(.*)$ +# nifi.registry.security.group.mapping.value.anygroup=$1 +# nifi.registry.security.group.mapping.transform.anygroup=LOWER + + +# kerberos properties # +nifi.registry.kerberos.krb5.file=${nifi.registry.kerberos.krb5.file} +nifi.registry.kerberos.spnego.principal=${nifi.registry.kerberos.spnego.principal} +nifi.registry.kerberos.spnego.keytab.location=${nifi.registry.kerberos.spnego.keytab.location} +nifi.registry.kerberos.spnego.authentication.expiration=${nifi.registry.kerberos.spnego.authentication.expiration} + +# OIDC # +nifi.registry.security.user.oidc.discovery.url=${nifi.registry.security.user.oidc.discovery.url} +nifi.registry.security.user.oidc.connect.timeout=${nifi.registry.security.user.oidc.connect.timeout} +nifi.registry.security.user.oidc.read.timeout=${nifi.registry.security.user.oidc.read.timeout} +nifi.registry.security.user.oidc.client.id=${nifi.registry.security.user.oidc.client.id} +nifi.registry.security.user.oidc.client.secret=${nifi.registry.security.user.oidc.client.secret} +nifi.registry.security.user.oidc.preferred.jwsalgorithm=${nifi.registry.security.user.oidc.preferred.jwsalgorithm} + +# revision management # +# This feature should remain disabled until a future NiFi release that supports the revision API changes +nifi.registry.revisions.enabled=${nifi.registry.revisions.enabled} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/providers.xml b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/providers.xml new file mode 100644 index 0000000000..70169b5248 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/providers.xml @@ -0,0 +1,100 @@ + + + + + + + + org.apache.nifi.registry.provider.flow.FileSystemFlowPersistenceProvider + ./flow_storage + + + + + + + + + + + + + + + org.apache.nifi.registry.provider.extension.FileSystemBundlePersistenceProvider + ./extension_bundles + + + + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/registry-aliases.xml b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/registry-aliases.xml new file mode 100644 index 0000000000..9bd1b2d855 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/registry-aliases.xml @@ -0,0 +1,23 @@ + + + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/pom.xml b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/pom.xml new file mode 100644 index 0000000000..3e7e3350ae --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/pom.xml @@ -0,0 +1,27 @@ + + + + 4.0.0 + + org.apache.nifi.registry + nifi-registry-revision + 1.14.0-SNAPSHOT + + nifi-registry-revision-api + jar + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/DeleteRevisionTask.java b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/DeleteRevisionTask.java new file mode 100644 index 0000000000..a31c5b5c97 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/DeleteRevisionTask.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.revision.api; + +/** + * A task that is responsible for deleting some entities. + * + * NOTE: This API is considered a framework level API for the NiFi ecosystem and may evolve as + * the NiFi PMC and committers deem necessary. It is not considered a public extension point. + */ +public interface DeleteRevisionTask { + + T performTask(); + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/EntityModification.java b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/EntityModification.java new file mode 100644 index 0000000000..a693582e8a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/EntityModification.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.revision.api; + +/** + * A holder for a Revision and the identity of the user that made the last modification. + * + * NOTE: This API is considered a framework level API for the NiFi ecosystem and may evolve as + * the NiFi PMC and committers deem necessary. It is not considered a public extension point. + */ +public class EntityModification { + + private final Revision revision; + private final String lastModifier; + + /** + * Creates a new EntityModification. + * + * @param revision revision + * @param lastModifier modifier + */ + public EntityModification(final Revision revision, final String lastModifier) { + this.revision = revision; + this.lastModifier = lastModifier; + } + + /** + * Get the revision. + * + * @return the revision + */ + public Revision getRevision() { + return revision; + } + + /** + * Get the last modifier. + * + * @return the modifier + */ + public String getLastModifier() { + return lastModifier; + } + + @Override + public String toString() { + return "Last Modified by '" + lastModifier + "' with Revision " + revision; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/ExpiredRevisionClaimException.java b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/ExpiredRevisionClaimException.java new file mode 100644 index 0000000000..2dbb4f277b --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/ExpiredRevisionClaimException.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.revision.api; + +/** + * An exception to be thrown when an expired RevisionClaim is encountered. + * + * NOTE: This API is considered a framework level API for the NiFi ecosystem and may evolve as + * the NiFi PMC and committers deem necessary. It is not considered a public extension point. + */ +public class ExpiredRevisionClaimException extends InvalidRevisionException { + private static final long serialVersionUID = 5648579322377770273L; + + public ExpiredRevisionClaimException(final String message) { + super(message); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/InvalidRevisionException.java b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/InvalidRevisionException.java new file mode 100644 index 0000000000..84f67bba24 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/InvalidRevisionException.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.revision.api; + +/** + * Exception indicating that the client has included an old revision in their request. + * + * NOTE: This API is considered a framework level API for the NiFi ecosystem and may evolve as + * the NiFi PMC and committers deem necessary. It is not considered a public extension point. + */ +public class InvalidRevisionException extends RuntimeException { + + public InvalidRevisionException(String message) { + super(message); + } + + public InvalidRevisionException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/Revision.java b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/Revision.java new file mode 100644 index 0000000000..1404c060bb --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/Revision.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.revision.api; + +/** + * A revision for an entity which is made up of the entity id, a version, and an optional client id. + * + * NOTE: This API is considered a framework level API for the NiFi ecosystem and may evolve as + * the NiFi PMC and committers deem necessary. It is not considered a public extension point. + */ +public class Revision { + + private final Long version; + private final String clientId; + private final String entityId; + + /** + * @param version the version number for the revision + * @param clientId the id of the client creating the revision, or null if one is not provided + * @param entityId the id of the component the revision belongs to + */ + public Revision(final Long version, final String clientId, final String entityId) { + if (version == null) { + throw new IllegalArgumentException("The revision must be specified."); + } + if (entityId == null) { + throw new IllegalArgumentException("The entityId must be specified."); + } + + this.version = version; + this.clientId = clientId; + this.entityId = entityId; + } + + public String getClientId() { + return clientId; + } + + public Long getVersion() { + return version; + } + + public String getEntityId() { + return entityId; + } + + /** + * Returns a new Revision that has the same Client ID and Component ID as this one, but with a larger version. + * + * @return the updated Revision + */ + public Revision incrementRevision(final String clientId) { + return new Revision(version + 1, clientId, entityId); + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (obj == this) { + return true; + } + + if ((obj instanceof Revision) == false) { + return false; + } + + final Revision thatRevision = (Revision) obj; + // ensure that component ID's are the same (including null) + if (thatRevision.getEntityId() == null && getEntityId() != null) { + return false; + } + if (thatRevision.getEntityId() != null && getEntityId() == null) { + return false; + } + if (thatRevision.getEntityId() != null && !thatRevision.getEntityId().equals(getEntityId())) { + return false; + } + + if (this.version != null && this.version.equals(thatRevision.version)) { + return true; + } else { + return clientId != null && !clientId.trim().isEmpty() && clientId.equals(thatRevision.getClientId()); + } + + } + + @Override + public int hashCode() { + int hash = 5; + hash = 59 * hash + (this.entityId != null ? this.entityId.hashCode() : 0); + hash = 59 * hash + (this.version != null ? this.version.hashCode() : 0); + hash = 59 * hash + (this.clientId != null ? this.clientId.hashCode() : 0); + return hash; + } + + @Override + public String toString() { + return "[" + version + ", " + clientId + ", " + entityId + ']'; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/RevisionClaim.java b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/RevisionClaim.java new file mode 100644 index 0000000000..8e0a04e9c3 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/RevisionClaim.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.revision.api; + +import java.util.Set; + +/** + * A set of Revisions submitted by a client. + * + * NOTE: This API is considered a framework level API for the NiFi ecosystem and may evolve as + * the NiFi PMC and committers deem necessary. It is not considered a public extension point. + */ +public interface RevisionClaim { + + Set getRevisions(); + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/RevisionManager.java b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/RevisionManager.java new file mode 100644 index 0000000000..0f20c354cd --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/RevisionManager.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.revision.api; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + *

+ * A Revision Manager provides the ability to prevent clients of the Web API from stepping on one another. + * This is done by providing revisions for entities individually. + *

+ * + * NOTE: This API is considered a framework level API for the NiFi ecosystem and may evolve as + * the NiFi PMC and committers deem necessary. It is not considered a public extension point. + */ +public interface RevisionManager { + + /** + * Returns the current Revision for the entity with the given ID. If no Revision yet exists for the + * entity with the given ID, one will be created with a Version of 0 and no Client ID. + * + * @param entityId the ID of the entity + * @return the current Revision for the entity with the given ID + */ + Revision getRevision(String entityId); + + /** + * Performs the given task without allowing the given Revision Claim to expire. Once this method + * returns or an Exception is thrown (with the Exception of ExpiredRevisionClaimException), + * the Revision may have been updated for each entity that the RevisionClaim holds a Claim for. + * If an ExpiredRevisionClaimException is thrown, the Revisions claimed by RevisionClaim + * will not be updated. + * + * @param claim the Revision Claim that is responsible for holding a Claim on the Revisions for each entity that is + * to be updated + * @param task the task that is responsible for updating the entities whose Revisions are claimed by the given + * RevisionClaim. The returned Revision set should include a Revision for each Revision that is the + * supplied Revision Claim. If there exists any Revision in the provided RevisionClaim that is not part + * of the RevisionClaim returned by the task, then the Revision is assumed to have not been modified. + * + * @return a RevisionUpdate object that represents the new version of the entity that was updated + * + * @throws ExpiredRevisionClaimException if the Revision Claim has expired + */ + RevisionUpdate updateRevision(RevisionClaim claim, UpdateRevisionTask task); + + /** + * Performs the given task that is expected to remove a entity from the flow. As a result, + * the Revision for the entity referenced by the RevisionClaim will be removed. + * + * @param claim the Revision Claim that is responsible for holding a Claim on the Revisions for each entity that is + * to be removed + * @param task the task that is responsible for deleting the entities whose Revisions are claimed by the given RevisionClaim + * @return the value returned from the DeleteRevisionTask + * + * @throws ExpiredRevisionClaimException if the Revision Claim has expired + */ + T deleteRevision(RevisionClaim claim, DeleteRevisionTask task) throws ExpiredRevisionClaimException; + + /** + * Clears any revisions that are currently held and resets the Revision Manager so that the revisions + * present are those provided in the given collection + */ + void reset(Collection revisions); + + /** + * @return a List of all Revisions managed by this Revision Manager + */ + List getAllRevisions(); + + /** + * @return a Map of all Revisions where the key is the entity id + */ + Map getRevisionMap(); + +} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/RevisionUpdate.java b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/RevisionUpdate.java new file mode 100644 index 0000000000..8785110264 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/RevisionUpdate.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.revision.api; + +import java.util.Set; + +/** + * A packaging of an entity and the corresponding Revision for that component. + * + * NOTE: This API is considered a framework level API for the NiFi ecosystem and may evolve as + * the NiFi PMC and committers deem necessary. It is not considered a public extension point. + */ +public interface RevisionUpdate { + + /** + * @return the entity that was updated + */ + T getEntity(); + + /** + * @return the last modification that was made for this component + */ + EntityModification getLastModification(); + + /** + * @return a Set of all Revisions that were updated + */ + Set getUpdatedRevisions(); +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/UpdateResult.java b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/UpdateResult.java new file mode 100644 index 0000000000..4181460688 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/UpdateResult.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.revision.api; + +/** + *

+ * The result of an update task. + *

+ * + * NOTE: This API is considered a framework level API for the NiFi ecosystem and may evolve as + * the NiFi PMC and committers deem necessary. It is not considered a public extension point. + */ +public interface UpdateResult { + + /** + * @return the entity that was updated + */ + T getEntity(); + + /** + * @return the id of the entity that was updated + */ + String getEntityId(); + + /** + * @return the identity of the user that updated the entity + */ + String updaterIdentity(); + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/UpdateRevisionTask.java b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/UpdateRevisionTask.java new file mode 100644 index 0000000000..c9d57484c1 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/UpdateRevisionTask.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.revision.api; + +/** + *

+ * A task that is responsible for updating an entity. + *

+ * + * NOTE: This API is considered a framework level API for the NiFi ecosystem and may evolve as + * the NiFi PMC and committers deem necessary. It is not considered a public extension point. + */ +public interface UpdateRevisionTask { + /** + * Updates an entity and returns the resulting entity. + * + * @return the update result containing the updated entity + */ + UpdateResult update(); +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/pom.xml b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/pom.xml new file mode 100644 index 0000000000..8a6fd6bf75 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/pom.xml @@ -0,0 +1,35 @@ + + + + 4.0.0 + + org.apache.nifi.registry + nifi-registry-revision + 1.14.0-SNAPSHOT + + nifi-registry-revision-common + jar + + + + + org.apache.nifi.registry + nifi-registry-revision-api + 1.14.0-SNAPSHOT + + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/naive/NaiveRevisionManager.java b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/naive/NaiveRevisionManager.java new file mode 100644 index 0000000000..641a36097a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/naive/NaiveRevisionManager.java @@ -0,0 +1,163 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.revision.naive; + +import org.apache.nifi.registry.revision.api.DeleteRevisionTask; +import org.apache.nifi.registry.revision.api.EntityModification; +import org.apache.nifi.registry.revision.api.ExpiredRevisionClaimException; +import org.apache.nifi.registry.revision.api.InvalidRevisionException; +import org.apache.nifi.registry.revision.api.Revision; +import org.apache.nifi.registry.revision.api.RevisionClaim; +import org.apache.nifi.registry.revision.api.RevisionManager; +import org.apache.nifi.registry.revision.api.RevisionUpdate; +import org.apache.nifi.registry.revision.api.UpdateResult; +import org.apache.nifi.registry.revision.api.UpdateRevisionTask; +import org.apache.nifi.registry.revision.standard.RevisionComparator; +import org.apache.nifi.registry.revision.standard.StandardRevisionUpdate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + *

+ * This class implements a naive approach for Revision Management. + * Each call into the Revision Manager will block until any previously held + * lock is expired or unlocked. This provides a very simple solution but can + * likely be improved by allowing, for instance, multiple threads to obtain + * temporary locks simultaneously, etc. + *

+ */ +public class NaiveRevisionManager implements RevisionManager { + private static final Logger logger = LoggerFactory.getLogger(NaiveRevisionManager.class); + + private final ConcurrentMap revisionMap = new ConcurrentHashMap<>(); + + + @Override + public void reset(final Collection revisions) { + synchronized (this) { // avoid allowing two threads to reset versions concurrently + revisionMap.clear(); + + for (final Revision revision : revisions) { + revisionMap.put(revision.getEntityId(), revision); + } + } + } + + @Override + public List getAllRevisions() { + return new ArrayList<>(revisionMap.values()); + } + + @Override + public Map getRevisionMap() { + return new HashMap<>(revisionMap); + } + + @Override + public Revision getRevision(final String componentId) { + return revisionMap.computeIfAbsent(componentId, id -> new Revision(0L, null, componentId)); + } + + @Override + public T deleteRevision(final RevisionClaim claim, final DeleteRevisionTask task) throws ExpiredRevisionClaimException { + logger.debug("Attempting to delete revision using {}", claim); + final List revisionList = new ArrayList<>(claim.getRevisions()); + revisionList.sort(new RevisionComparator()); + + // Verify the provided revisions. + String failedId = null; + for (final Revision revision : revisionList) { + final Revision curRevision = getRevision(revision.getEntityId()); + if (!curRevision.equals(revision)) { + throw new ExpiredRevisionClaimException("Invalid Revision was given for entity with ID '" + failedId + "'"); + } + } + + // Perform the action provided + final T taskResult = task.performTask(); + + for (final Revision revision : revisionList) { + revisionMap.remove(revision.getEntityId()); + } + + return taskResult; + } + + @Override + public RevisionUpdate updateRevision(final RevisionClaim originalClaim, final UpdateRevisionTask task) + throws ExpiredRevisionClaimException { + logger.debug("Attempting to update revision using {}", originalClaim); + + final List revisionList = new ArrayList<>(originalClaim.getRevisions()); + revisionList.sort(new RevisionComparator()); + + for (final Revision revision : revisionList) { + final Revision currentRevision = getRevision(revision.getEntityId()); + final boolean verified = revision.equals(currentRevision); + + if (!verified) { + // Throw an Exception indicating that we failed to obtain the locks + throw new InvalidRevisionException("Invalid Revision was given for entity with ID '" + revision.getEntityId() + "'"); + } + } + + // We successfully verified all revisions. + logger.debug("Successfully verified Revision Claim for all revisions"); + + // Perform the update + // If an exception is thrown we don't want to update revision so it is ok to bounce out of this method + final UpdateResult updateResult = task.update(); + if (updateResult == null) { + return null; + } + + // The update succeeded so increment the revisions + final Set incrementedRevisions = new HashSet<>(); + for (final Revision incomingRevision : revisionList) { + final String entityId = incomingRevision.getEntityId(); + final String clientId = incomingRevision.getClientId(); + + // retrieve the revision from the map here because the incoming revision may have been + // verified based on the client id and may not contain the latest version + final Revision existingRevision = revisionMap.get(entityId); + final Revision incrementedRevision = existingRevision.incrementRevision(clientId); + incrementedRevisions.add(incrementedRevision); + + revisionMap.put(entityId, incrementedRevision); + } + + // Create the result with the updated entity and updated revisions + final T updatedEntity = updateResult.getEntity(); + final String updaterIdentity = updateResult.updaterIdentity(); + + final Revision updatedEntityRevision = revisionMap.get(updateResult.getEntityId()); + final EntityModification entityModification = new EntityModification(updatedEntityRevision, updaterIdentity); + + return new StandardRevisionUpdate<>(updatedEntity, entityModification, incrementedRevisions); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/standard/RevisionComparator.java b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/standard/RevisionComparator.java new file mode 100644 index 0000000000..0bf317a161 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/standard/RevisionComparator.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.revision.standard; + +import org.apache.nifi.registry.revision.api.Revision; + +import java.util.Comparator; +import java.util.Objects; + +public class RevisionComparator implements Comparator { + + @Override + public int compare(final Revision o1, final Revision o2) { + final int entityComparison = o1.getEntityId().compareTo(o2.getEntityId()); + if (entityComparison != 0) { + return entityComparison; + } + + final Comparator nullSafeStringComparator = Comparator.nullsFirst(String::compareTo); + final int clientComparison = Objects.compare(o1.getClientId(), o2.getClientId(), nullSafeStringComparator); + if (clientComparison != 0) { + return clientComparison; + } + + return o1.getVersion().compareTo(o2.getVersion()); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/standard/StandardRevisionClaim.java b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/standard/StandardRevisionClaim.java new file mode 100644 index 0000000000..2903ea2c2f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/standard/StandardRevisionClaim.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.revision.standard; + +import org.apache.nifi.registry.revision.api.Revision; +import org.apache.nifi.registry.revision.api.RevisionClaim; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +public class StandardRevisionClaim implements RevisionClaim { + private final Set revisions; + + public StandardRevisionClaim(final Revision... revisions) { + this.revisions = new HashSet<>(revisions.length); + for (final Revision revision : revisions) { + this.revisions.add(revision); + } + } + + public StandardRevisionClaim(final Collection revisions) { + this.revisions = new HashSet<>(revisions); + } + + @Override + public Set getRevisions() { + return revisions; + } + + @Override + public String toString() { + return revisions.toString(); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/standard/StandardRevisionUpdate.java b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/standard/StandardRevisionUpdate.java new file mode 100644 index 0000000000..3a6012eda2 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/standard/StandardRevisionUpdate.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.revision.standard; + +import org.apache.nifi.registry.revision.api.EntityModification; +import org.apache.nifi.registry.revision.api.Revision; +import org.apache.nifi.registry.revision.api.RevisionUpdate; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public class StandardRevisionUpdate implements RevisionUpdate { + private final T entity; + private final EntityModification lastModification; + private final Set updatedRevisions; + + public StandardRevisionUpdate(final T entity, final EntityModification lastModification) { + this(entity, lastModification, null); + } + + public StandardRevisionUpdate(final T entity, final EntityModification lastModification, final Set updatedRevisions) { + this.entity = entity; + this.lastModification = lastModification; + this.updatedRevisions = updatedRevisions == null ? new HashSet<>() : new HashSet<>(updatedRevisions); + if (lastModification != null) { + this.updatedRevisions.add(lastModification.getRevision()); + } + } + + + @Override + public T getEntity() { + return entity; + } + + @Override + public EntityModification getLastModification() { + return lastModification; + } + + @Override + public Set getUpdatedRevisions() { + return Collections.unmodifiableSet(updatedRevisions); + } + + @Override + public String toString() { + return "[Entity=" + entity + ", Last Modification=" + lastModification + "]"; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/standard/StandardUpdateResult.java b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/standard/StandardUpdateResult.java new file mode 100644 index 0000000000..a09ed0a12a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/standard/StandardUpdateResult.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.revision.standard; + +import org.apache.nifi.registry.revision.api.UpdateResult; + +public class StandardUpdateResult implements UpdateResult { + + private final T entity; + private final String entityId; + private final String updaterIdentity; + + public StandardUpdateResult(final T entity, final String entityId, final String updaterIdentity) { + this.entity = entity; + this.entityId = entityId; + this.updaterIdentity = updaterIdentity; + + if (this.entity == null) { + throw new IllegalArgumentException("Entity is required"); + } + + if (this.entityId == null || this.entityId.trim().isEmpty()) { + throw new IllegalArgumentException("Entity id is required"); + } + + if (this.updaterIdentity == null || this.updaterIdentity.trim().isEmpty()) { + throw new IllegalArgumentException("Updater identity is required"); + } + } + + @Override + public T getEntity() { + return entity; + } + + @Override + public String getEntityId() { + return entityId; + } + + @Override + public String updaterIdentity() { + return updaterIdentity; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/web/ClientIdParameter.java b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/web/ClientIdParameter.java new file mode 100644 index 0000000000..1e4d09c3ae --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/web/ClientIdParameter.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.revision.web; + +import java.util.UUID; + +/** + * Class for parsing handling client ids. If the client id is not specified, one will be generated. + */ +public class ClientIdParameter { + + private final String clientId; + + public ClientIdParameter(String clientId) { + if (clientId == null || clientId.trim().isEmpty()) { + this.clientId = UUID.randomUUID().toString(); + } else { + this.clientId = clientId; + } + } + + public ClientIdParameter() { + this.clientId = UUID.randomUUID().toString(); + } + + public String getClientId() { + return clientId; + } +} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/web/LongParameter.java b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/web/LongParameter.java new file mode 100644 index 0000000000..c568d76a28 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/web/LongParameter.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.revision.web; + +/** + * Class for parsing long parameters and providing a user friendly error message. + */ +public class LongParameter { + + private static final String INVALID_LONG_MESSAGE = "Unable to parse '%s' as a long value."; + + private Long longValue; + + public LongParameter(String rawLongValue) { + try { + longValue = Long.parseLong(rawLongValue); + } catch (NumberFormatException nfe) { + throw new IllegalArgumentException(String.format(INVALID_LONG_MESSAGE, rawLongValue)); + } + } + + public Long getLong() { + return longValue; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-model/pom.xml b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-model/pom.xml new file mode 100644 index 0000000000..7953e8ba36 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-model/pom.xml @@ -0,0 +1,34 @@ + + + + 4.0.0 + + org.apache.nifi.registry + nifi-registry-revision + 1.14.0-SNAPSHOT + + nifi-registry-revision-entity-model + jar + + + + + io.swagger + swagger-annotations + + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-model/src/main/java/org/apache/nifi/registry/revision/entity/RevisableEntity.java b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-model/src/main/java/org/apache/nifi/registry/revision/entity/RevisableEntity.java new file mode 100644 index 0000000000..260cbb1e6e --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-model/src/main/java/org/apache/nifi/registry/revision/entity/RevisableEntity.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.revision.entity; + +/** + * An entity that supports revision tracking. + */ +public interface RevisableEntity { + + /** + * @return the identifier for the entity + */ + String getIdentifier(); + + /** + * Sets the identifier for the entity. + * + * @param identifier the identifier + */ + void setIdentifier(String identifier); + + /** + * @return the revision information for the entity + */ + RevisionInfo getRevision(); + + /** + * Sets the revision info for the entity. + * + * @param revision the revision info + */ + void setRevision(RevisionInfo revision); + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-model/src/main/java/org/apache/nifi/registry/revision/entity/RevisionInfo.java b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-model/src/main/java/org/apache/nifi/registry/revision/entity/RevisionInfo.java new file mode 100644 index 0000000000..dec19fe73c --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-model/src/main/java/org/apache/nifi/registry/revision/entity/RevisionInfo.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.revision.entity; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +@ApiModel(description = "The revision information for an entity managed through the REST API.") +public class RevisionInfo { + + private String clientId; + private Long version; + private String lastModifier; + + public RevisionInfo() { + } + + public RevisionInfo(String clientId, Long version) { + this(clientId, version, null); + } + + public RevisionInfo(String clientId, Long version, String lastModifier) { + this.clientId = clientId; + this.version = version; + this.lastModifier = lastModifier; + } + + @ApiModelProperty( + value = "A client identifier used to make a request. By including a client identifier, the API can allow multiple requests " + + "without needing the current revision. Due to the asynchronous nature of requests/responses this was implemented to " + + "allow the client to make numerous requests without having to wait for the previous response to come back." + ) + public String getClientId() { + return clientId; + } + + public void setClientId(final String clientId) { + this.clientId = clientId; + } + + @ApiModelProperty( + value = "NiFi Registry employs an optimistic locking strategy where the client must include a revision in their request " + + "when performing an update. In a response to a mutable flow request, this field represents the updated base version." + ) + public Long getVersion() { + return version; + } + + public void setVersion(final Long version) { + this.version = version; + } + + @ApiModelProperty( + value = "The user that last modified the entity.", + readOnly = true + ) + public String getLastModifier() { + return lastModifier; + } + + public void setLastModifier(final String lastModifier) { + this.lastModifier = lastModifier; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/pom.xml b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/pom.xml new file mode 100644 index 0000000000..9f77f57370 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/pom.xml @@ -0,0 +1,46 @@ + + + + 4.0.0 + + org.apache.nifi.registry + nifi-registry-revision + 1.14.0-SNAPSHOT + + nifi-registry-revision-entity-service + jar + + + + org.apache.nifi.registry + nifi-registry-revision-entity-model + 1.14.0-SNAPSHOT + + + org.apache.nifi.registry + nifi-registry-revision-common + 1.14.0-SNAPSHOT + + + + org.slf4j + slf4j-simple + ${org.slf4j.version} + test + + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/src/main/java/org/apache/nifi/registry/revision/entity/RevisableEntityService.java b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/src/main/java/org/apache/nifi/registry/revision/entity/RevisableEntityService.java new file mode 100644 index 0000000000..fec764c798 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/src/main/java/org/apache/nifi/registry/revision/entity/RevisableEntityService.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.revision.entity; + +import java.util.Collection; +import java.util.List; +import java.util.function.Supplier; + +/** + * A service to perform CRUD operations on a RevisableEntity. + */ +public interface RevisableEntityService { + + /** + * Creates an entity using the RevisionManager. + * + * @param requestEntity the entity to create + * @param creatorIdentity the identity of the user performing the create operation + * @param createEntity a function that creates the entity and returns the created reference + * @param the type of RevisableEntity + * @return the created entity + */ + T create(T requestEntity, String creatorIdentity, Supplier createEntity); + + /** + * Retrieves a RevisableEntity and populates the RevisionInfo. + * + * @param getEntity a function that retrieves an entity + * @param the type of RevisableEntity + * @return the retrieved entity + */ + T get(Supplier getEntity); + + /** + * Retrieves a List of RevisableEntity instances and populates the RevisionInfo. + * + * @param getEntities a function that retrieves a list of entities + * @param the type of RevisableEntity + * @return the list of retrieved entity + */ + List getEntities(Supplier> getEntities); + + /** + * Updates a RevisableEntity using the RevisionManager. + * + * @param requestEntity the entity to update + * @param updaterIdentity the identity of the user performing the update operation + * @param updateEntity a function that updates the entity and returns the updated reference + * @param the type of RevisableEntity + * @return the updated entity + */ + T update(T requestEntity, String updaterIdentity, Supplier updateEntity); + + /** + * Deletes a RevisableEntity using the RevisionManager. + * + * @param entityIdentifier the identifier of the entity to delete + * @param revisionInfo the RevisionInfo for the entity to delete + * @param deleteEntity a function that deletes the entity and returns the deleted reference + * @param the type of RevisableEntity + * @return the deleted entity + */ + T delete(String entityIdentifier, RevisionInfo revisionInfo, Supplier deleteEntity); + + /** + * Populates RevisionInfo on any objects in the collection that implement RevisableEntity. + * + * @param entities the entities collection which may contain one or more RevisableEntity instances + */ + void populateRevisions(Collection entities); + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/src/main/java/org/apache/nifi/registry/revision/entity/StandardRevisableEntityService.java b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/src/main/java/org/apache/nifi/registry/revision/entity/StandardRevisableEntityService.java new file mode 100644 index 0000000000..a0f0ab7950 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/src/main/java/org/apache/nifi/registry/revision/entity/StandardRevisableEntityService.java @@ -0,0 +1,196 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.revision.entity; + +import org.apache.nifi.registry.revision.api.EntityModification; +import org.apache.nifi.registry.revision.api.Revision; +import org.apache.nifi.registry.revision.api.RevisionClaim; +import org.apache.nifi.registry.revision.api.RevisionManager; +import org.apache.nifi.registry.revision.api.RevisionUpdate; +import org.apache.nifi.registry.revision.standard.StandardRevisionClaim; +import org.apache.nifi.registry.revision.standard.StandardUpdateResult; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +/** + * Standard implementation of RevisableEntityService. + */ +public class StandardRevisableEntityService implements RevisableEntityService { + + private final RevisionManager revisionManager; + + public StandardRevisableEntityService(final RevisionManager revisionManager) { + this.revisionManager = revisionManager; + } + + @Override + public T create(final T requestEntity, final String creatorIdentity, final Supplier createEntity) { + if (requestEntity == null) { + throw new IllegalArgumentException("Request entity is required"); + } + + if (requestEntity.getRevision() == null || requestEntity.getRevision().getVersion() == null) { + throw new IllegalArgumentException("Revision info is required"); + } + + if (requestEntity.getRevision().getVersion() != 0) { + throw new IllegalArgumentException("A revision version of 0 must be specified when creating a new entity"); + } + + return createOrUpdate(requestEntity, creatorIdentity, createEntity); + } + + @Override + public T get(final Supplier getEntity) { + final T entity = getEntity.get(); + if (entity != null) { + populateRevision(entity); + } + return entity; + } + + @Override + public List getEntities(final Supplier> getEntities) { + final List entities = getEntities.get(); + populateRevisableEntityRevisions(entities); + return entities; + } + + @Override + public T update(final T requestEntity, final String updaterIdentity, final Supplier updateEntity) { + return createOrUpdate(requestEntity, updaterIdentity, updateEntity); + } + + private T createOrUpdate(final T requestEntity, final String userIdentity, final Supplier createOrUpdateEntity) { + if (requestEntity == null) { + throw new IllegalArgumentException("Request entity is required"); + } + + if (requestEntity.getRevision() == null || requestEntity.getRevision().getVersion() == null) { + throw new IllegalArgumentException("Revision info is required"); + } + + if (userIdentity == null || userIdentity.trim().isEmpty()) { + throw new IllegalArgumentException("User identity is required"); + } + + final Revision revision = createRevision(requestEntity.getIdentifier(), requestEntity.getRevision()); + final RevisionClaim claim = new StandardRevisionClaim(revision); + + final RevisionUpdate revisionUpdate = revisionManager.updateRevision(claim, () -> { + final T updatedEntity = createOrUpdateEntity.get(); + return new StandardUpdateResult<>(updatedEntity, updatedEntity.getIdentifier(), userIdentity); + }); + + final T resultEntity = revisionUpdate.getEntity(); + resultEntity.setRevision(createRevisionInfo(revisionUpdate.getLastModification())); + return resultEntity; + } + + @Override + public T delete(final String entityIdentifier, final RevisionInfo revisionInfo, final Supplier deleteEntity) { + if (entityIdentifier == null || entityIdentifier.trim().isEmpty()) { + throw new IllegalArgumentException("Entity identifier is required"); + } + + if (revisionInfo == null || revisionInfo.getVersion() == null) { + throw new IllegalArgumentException("Revision info is required"); + } + + final Revision revision = createRevision(entityIdentifier, revisionInfo); + final RevisionClaim claim = new StandardRevisionClaim(revision); + return revisionManager.deleteRevision(claim, () -> deleteEntity.get()); + } + + @Override + public void populateRevisions(final Collection entities) { + if (entities == null || entities.isEmpty()) { + return; + } + + // Note: This might be inefficient to retrieve all the revisions when there are lots of revisions + // and only a few entities that we might need revisions for, we could consider allowing a set of + // entity ids to be passed in, but then we also might end up with a massive OR statement when selecting + final Map revisionMap = revisionManager.getRevisionMap(); + + for (final Object obj : entities) { + if (obj instanceof RevisableEntity) { + populateRevision(revisionMap, (RevisableEntity) obj); + } + } + } + + private void populateRevisableEntityRevisions(final Collection revisableEntities) { + if (revisableEntities == null) { + return; + } + + final Map revisionMap = revisionManager.getRevisionMap(); + revisableEntities.forEach(e -> { + populateRevision(revisionMap, e); + }); + } + + private void populateRevision(final Map revisionMap, final RevisableEntity revisableEntity) { + final Revision revision = revisionMap.get(revisableEntity.getIdentifier()); + if (revision != null) { + final RevisionInfo revisionInfo = createRevisionInfo(revision); + revisableEntity.setRevision(revisionInfo); + } else { + // need to make sure that if there isn't an entry in the map, we call getRevision which will cause a + // revision to be created in the RevisionManager + populateRevision(revisableEntity); + } + } + + private void populateRevision(final RevisableEntity e) { + if (e == null) { + return; + } + + // get or create the revision + final Revision entityRevision = revisionManager.getRevision(e.getIdentifier()); + final RevisionInfo revisionInfo = createRevisionInfo(entityRevision); + e.setRevision(revisionInfo); + } + + private Revision createRevision(final String entityId, final RevisionInfo revisionInfo) { + return new Revision(revisionInfo.getVersion(), revisionInfo.getClientId(), entityId); + } + + private RevisionInfo createRevisionInfo(final Revision revision) { + return createRevisionInfo(revision, null); + } + + private RevisionInfo createRevisionInfo(final EntityModification entityModification) { + return createRevisionInfo(entityModification.getRevision(), entityModification); + } + + private RevisionInfo createRevisionInfo(final Revision revision, final EntityModification entityModification) { + final RevisionInfo revisionInfo = new RevisionInfo(); + revisionInfo.setVersion(revision.getVersion()); + revisionInfo.setClientId(revision.getClientId()); + if (entityModification != null) { + revisionInfo.setLastModifier(entityModification.getLastModifier()); + } + return revisionInfo; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/src/test/java/org/apache/nifi/registry/revision/entity/TestStandardRevisableEntityService.java b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/src/test/java/org/apache/nifi/registry/revision/entity/TestStandardRevisableEntityService.java new file mode 100644 index 0000000000..0458d5cc56 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/src/test/java/org/apache/nifi/registry/revision/entity/TestStandardRevisableEntityService.java @@ -0,0 +1,248 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.revision.entity; + +import org.apache.nifi.registry.revision.api.InvalidRevisionException; +import org.apache.nifi.registry.revision.api.RevisionManager; +import org.apache.nifi.registry.revision.naive.NaiveRevisionManager; +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +public class TestStandardRevisableEntityService { + + private RevisionManager revisionManager; + private RevisableEntityService entityService; + + @Before + public void setup() { + revisionManager = new NaiveRevisionManager(); + entityService = new StandardRevisableEntityService(revisionManager); + } + + @Test + public void testCreate() { + final String clientId = "client1"; + final RevisionInfo requestRevision = new RevisionInfo(clientId, 0L); + + final String userIdentity = "user1"; + + final String entityId = "1"; + final RevisableEntity requestEntity = new TestEntity(entityId, requestRevision); + + final RevisableEntity createdEntity = entityService.create( + requestEntity, userIdentity, () -> new TestEntity(entityId, null)); + assertNotNull(createdEntity); + assertEquals(requestEntity.getIdentifier(), createdEntity.getIdentifier()); + + final RevisionInfo createdRevision = createdEntity.getRevision(); + assertNotNull(createdRevision); + assertEquals(requestRevision.getVersion().longValue() + 1, createdRevision.getVersion().longValue()); + assertEquals(clientId, createdRevision.getClientId()); + assertEquals(userIdentity, createdRevision.getLastModifier()); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateWhenMissingRevision() { + final RevisableEntity requestEntity = new TestEntity("1", null); + entityService.create(requestEntity, "user1", () -> requestEntity); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateWhenNonZeroRevision() { + final RevisionInfo requestRevision = new RevisionInfo(null, 99L); + final RevisableEntity requestEntity = new TestEntity("1", requestRevision); + entityService.create(requestEntity, "user1", () -> requestEntity); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateWhenTaskThrowsException() { + final RevisionInfo requestRevision = new RevisionInfo("client1", 0L); + final RevisableEntity requestEntity = new TestEntity("1", requestRevision); + entityService.create(requestEntity, "user1", () -> { + throw new IllegalArgumentException(""); + }); + } + + @Test + public void testGetEntityWhenExists() { + final RevisableEntity entity = entityService.get(() -> new TestEntity("1", null)); + assertNotNull(entity); + + final RevisionInfo revision = entity.getRevision(); + assertNotNull(revision); + assertEquals(0, revision.getVersion().longValue()); + } + + @Test + public void testGetEntityWhenDoesNotExist() { + final RevisableEntity entity = entityService.get(() -> null); + assertNull(entity); + } + + @Test + public void testGetEntities() { + final TestEntity entity1 = new TestEntity("1", null); + final TestEntity entity2 = new TestEntity("2", null); + final List entities = Arrays.asList(entity1, entity2); + + final List resultEntities = entityService.getEntities(() -> entities); + assertNotNull(resultEntities); + resultEntities.forEach(e -> { + assertNotNull(e.getRevision()); + assertEquals(0, e.getRevision().getVersion().longValue()); + }); + } + + @Test + public void testUpdate() { + final RevisionInfo revisionInfo = new RevisionInfo(null, 0L); + final TestEntity requestEntity = new TestEntity("1", revisionInfo); + + final RevisableEntity createdEntity = entityService.create( + requestEntity, "user1", () -> requestEntity); + assertNotNull(createdEntity); + assertEquals(requestEntity.getIdentifier(), createdEntity.getIdentifier()); + assertNotNull(createdEntity.getRevision()); + assertEquals(1, createdEntity.getRevision().getVersion().longValue()); + + final RevisableEntity updatedEntity = entityService.update( + createdEntity, "user2", () -> createdEntity); + assertNotNull(updatedEntity.getRevision()); + assertEquals(2, updatedEntity.getRevision().getVersion().longValue()); + assertEquals("user2", updatedEntity.getRevision().getLastModifier()); + } + + @Test + public void testUpdateWithClientId() { + final RevisionInfo revisionInfo = new RevisionInfo("client-1", 0L); + final TestEntity requestEntity = new TestEntity("1", revisionInfo); + + final RevisableEntity createdEntity = entityService.create( + requestEntity, "user1", () -> requestEntity); + assertNotNull(createdEntity); + assertEquals(requestEntity.getIdentifier(), createdEntity.getIdentifier()); + assertNotNull(createdEntity.getRevision()); + assertEquals(1, createdEntity.getRevision().getVersion().longValue()); + + final RevisableEntity updatedEntity = entityService.update( + createdEntity, "user2", () -> createdEntity); + assertNotNull(updatedEntity.getRevision()); + assertEquals(2, updatedEntity.getRevision().getVersion().longValue()); + assertEquals("user2", updatedEntity.getRevision().getLastModifier()); + + // set the version back to 0 to prove that we can update based on client id being the same + updatedEntity.getRevision().setVersion(0L); + + final RevisableEntity updatedEntity2 = entityService.update( + createdEntity, "user3", () -> updatedEntity); + assertNotNull(updatedEntity2.getRevision()); + assertEquals(3, updatedEntity2.getRevision().getVersion().longValue()); + assertEquals("user3", updatedEntity2.getRevision().getLastModifier()); + } + + @Test(expected = IllegalArgumentException.class) + public void testUpdateWhenMissingRevision() { + final RevisionInfo revisionInfo = new RevisionInfo(null, 0L); + final TestEntity requestEntity = new TestEntity("1", revisionInfo); + + final RevisableEntity createdEntity = entityService.create( + requestEntity, "user1", () -> requestEntity); + assertNotNull(createdEntity); + assertEquals(requestEntity.getIdentifier(), createdEntity.getIdentifier()); + assertNotNull(createdEntity.getRevision()); + assertEquals(1, createdEntity.getRevision().getVersion().longValue()); + + createdEntity.setRevision(null); + entityService.update(createdEntity, "user2", () -> createdEntity); + } + + @Test + public void testDelete() { + final RevisionInfo revisionInfo = new RevisionInfo(null, 0L); + final TestEntity requestEntity = new TestEntity("1", revisionInfo); + + final RevisableEntity createdEntity = entityService.create( + requestEntity, "user1", () -> requestEntity); + assertNotNull(createdEntity); + + final RevisableEntity deletedEntity = entityService.delete( + createdEntity.getIdentifier(), createdEntity.getRevision(), () -> createdEntity); + assertNotNull(deletedEntity); + } + + @Test(expected = IllegalArgumentException.class) + public void testDeleteWhenMissingRevision() { + final RevisionInfo revisionInfo = new RevisionInfo(null, 0L); + final TestEntity requestEntity = new TestEntity("1", revisionInfo); + + final RevisableEntity createdEntity = entityService.create( + requestEntity, "user1", () -> requestEntity); + assertNotNull(createdEntity); + assertNotNull(createdEntity.getRevision()); + + createdEntity.setRevision(null); + entityService.delete(createdEntity.getIdentifier(), createdEntity.getRevision(), () -> createdEntity); + } + + @Test(expected = InvalidRevisionException.class) + public void testDeleteWhenDoesNotExist() { + final RevisionInfo revisionInfo = new RevisionInfo(null, 1L); + final RevisableEntity deletedEntity = entityService.delete("1", revisionInfo, () -> null); + assertNull(deletedEntity); + } + + /** + * A RevisableEntity for testing. + */ + static class TestEntity implements RevisableEntity { + + private String identifier; + private RevisionInfo revisionInfo; + + public TestEntity(String identifier, RevisionInfo revisionInfo) { + this.identifier = identifier; + this.revisionInfo = revisionInfo; + } + + @Override + public String getIdentifier() { + return identifier; + } + + @Override + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + + @Override + public RevisionInfo getRevision() { + return revisionInfo; + } + + @Override + public void setRevision(RevisionInfo revision) { + this.revisionInfo = revision; + } + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/pom.xml b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/pom.xml new file mode 100644 index 0000000000..486bc9c02d --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/pom.xml @@ -0,0 +1,62 @@ + + + + 4.0.0 + + org.apache.nifi.registry + nifi-registry-revision + 1.14.0-SNAPSHOT + + nifi-registry-revision-spring-jdbc + jar + + + + org.apache.nifi.registry + nifi-registry-revision-common + 1.14.0-SNAPSHOT + + + org.springframework + spring-jdbc + 5.2.5.RELEASE + + + + org.mockito + mockito-core + test + + + org.apache.nifi.registry + nifi-registry-test + 1.14.0-SNAPSHOT + test + + + com.h2database + h2 + ${h2.version} + test + + + org.flywaydb + flyway-core + ${flyway.version} + test + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/src/main/java/org/apache/nifi/registry/revision/jdbc/JdbcRevisionManager.java b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/src/main/java/org/apache/nifi/registry/revision/jdbc/JdbcRevisionManager.java new file mode 100644 index 0000000000..9aad556c7c --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/src/main/java/org/apache/nifi/registry/revision/jdbc/JdbcRevisionManager.java @@ -0,0 +1,247 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.revision.jdbc; + +import org.apache.nifi.registry.revision.api.DeleteRevisionTask; +import org.apache.nifi.registry.revision.api.EntityModification; +import org.apache.nifi.registry.revision.api.ExpiredRevisionClaimException; +import org.apache.nifi.registry.revision.api.InvalidRevisionException; +import org.apache.nifi.registry.revision.api.Revision; +import org.apache.nifi.registry.revision.api.RevisionClaim; +import org.apache.nifi.registry.revision.api.RevisionManager; +import org.apache.nifi.registry.revision.api.RevisionUpdate; +import org.apache.nifi.registry.revision.api.UpdateResult; +import org.apache.nifi.registry.revision.api.UpdateRevisionTask; +import org.apache.nifi.registry.revision.standard.RevisionComparator; +import org.apache.nifi.registry.revision.standard.StandardRevisionUpdate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * A database implementation of {@link RevisionManager} that use's Spring's {@link JdbcTemplate}. + * + * It is expected that the database has a table named REVISION with the following schema, but it is up to consumers + * of this library to manage the creation of this table: + * + *
+ * {@code
+ *  CREATE TABLE REVISION (
+ *     ENTITY_ID VARCHAR(50) NOT NULL,
+ *     VERSION BIGINT NOT NULL DEFAULT(0),
+ *     CLIENT_ID VARCHAR(100),
+ *     CONSTRAINT PK__REVISION_ENTITY_ID PRIMARY KEY (ENTITY_ID)
+ *  );
+ * }
+ * 
+ * + * This implementation leverages the transactional semantics of a relational database to implement an optimistic-locking strategy. + * + * In order to function correctly, this must be used with in a transaction with an isolation level of at least READ_COMMITTED. + */ +public class JdbcRevisionManager implements RevisionManager { + + private static Logger LOGGER = LoggerFactory.getLogger(JdbcRevisionManager.class); + + private final JdbcTemplate jdbcTemplate; + + public JdbcRevisionManager(final JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = Objects.requireNonNull(jdbcTemplate); + } + + @Override + public Revision getRevision(final String entityId) { + final Revision revision = retrieveRevision(entityId); + if (revision == null) { + return createRevision(entityId); + } else { + return revision; + } + } + + private Revision retrieveRevision(final String entityId) { + try { + final String selectSql = "SELECT * FROM REVISION WHERE ENTITY_ID = ?"; + return jdbcTemplate.queryForObject(selectSql, new Object[] {entityId}, new RevisionRowMapper()); + } catch (EmptyResultDataAccessException e) { + return null; + } + } + + private Revision createRevision(final String entityId) { + final Revision revision = new Revision(0L, null, entityId); + final String insertSql = "INSERT INTO REVISION(ENTITY_ID, VERSION) VALUES (?, ?)"; + jdbcTemplate.update(insertSql, revision.getEntityId(), revision.getVersion()); + return revision; + } + + @Override + public RevisionUpdate updateRevision(final RevisionClaim claim, final UpdateRevisionTask task) { + LOGGER.debug("Attempting to update revision using {}", claim); + + final List revisionList = new ArrayList<>(claim.getRevisions()); + revisionList.sort(new RevisionComparator()); + + // Update each revision which increments the version and locks the row. + // Since we are in transaction these changes won't be committed unless the entire task completes successfully. + // It is important this happens first so that the task won't execute unless the revision can be updated. + // This prevents any other changes from happening that might not be part of the database transaction. + final Set incrementedRevisions = new HashSet<>(); + for (final Revision incomingRevision : revisionList) { + final String entityId = incomingRevision.getEntityId(); + + // calling getRevision here will lazily create an initial revision + getRevision(entityId); + updateRevision(incomingRevision); + + // retrieve the updated revision since the incoming revision may have matched on the client id + // and may not have the latest version which we want to return with the result + final Revision incrementedRevision = getRevision(entityId); + incrementedRevisions.add(incrementedRevision); + } + + // We successfully verified all revisions. + LOGGER.debug("Successfully verified Revision Claim for all revisions"); + + // Perform the update + final UpdateResult updateResult = task.update(); + LOGGER.debug("Update task completed"); + + // Create the result with the updated entity and updated revisions + final T updatedEntity = updateResult.getEntity(); + final String updaterIdentity = updateResult.updaterIdentity(); + + final Revision updatedEntityRevision = getRevision(updateResult.getEntityId()); + final EntityModification entityModification = new EntityModification(updatedEntityRevision, updaterIdentity); + + return new StandardRevisionUpdate<>(updatedEntity, entityModification, incrementedRevisions); + } + + /* + * Issue an update that increments the version, but only if the incoming version OR client id match the existing revision. + * + * If no rows were updated, then the incoming revision is stale and an exception is thrown. + * + * If a row was updated, then the incoming revision is good and that row is no locked in the DB, and we can proceed. + */ + private void updateRevision(final Revision incomingRevision) { + final String sql = + "UPDATE REVISION SET " + + "VERSION = (VERSION + 1), " + + "CLIENT_ID = ? " + + "WHERE " + + "ENTITY_ID = ? AND (" + + "VERSION = ? OR CLIENT_ID = ? " + + ")"; + + final String entityId = incomingRevision.getEntityId(); + final String clientId = incomingRevision.getClientId(); + final Long version = incomingRevision.getVersion(); + + final int rowsUpdated = jdbcTemplate.update(sql, clientId, entityId, version, clientId); + if (rowsUpdated <= 0) { + throw new InvalidRevisionException("Invalid Revision was given for entity with ID '" + entityId + "'"); + } + } + + @Override + public T deleteRevision(final RevisionClaim claim, final DeleteRevisionTask task) + throws ExpiredRevisionClaimException { + LOGGER.debug("Attempting to delete revision using {}", claim); + + final List revisionList = new ArrayList<>(claim.getRevisions()); + revisionList.sort(new RevisionComparator()); + + // Issue the delete for each revision + // Since we are in transaction these changes won't be committed unless the entire task completes successfully. + // It is important this happens first so that the task won't execute unless the revision can be deleted. + // This prevents any other changes from happening that might not be part of the database transaction. + for (final Revision revision : revisionList) { + deleteRevision(revision); + } + + // Perform the action provided + final T taskResult = task.performTask(); + LOGGER.debug("Delete task completed"); + + return taskResult; + } + + /* + * Issue a delete for a revision of a given entity, but only if the incoming version OR client id match the existing revision. + * + * If no rows were updated, then the incoming revision is stale and an exception is thrown. + * + * If a row was deleted, then the incoming revision is good and that row is no locked in the DB, and we can proceed. + */ + private void deleteRevision(final Revision revision) { + final String sql = + "DELETE FROM REVISION WHERE " + + "ENTITY_ID = ? AND (" + + "VERSION = ? OR CLIENT_ID = ? " + + ")"; + + final String entityId = revision.getEntityId(); + final String clientId = revision.getClientId(); + final Long version = revision.getVersion(); + + final int rowsUpdated = jdbcTemplate.update(sql, entityId, version, clientId); + if (rowsUpdated <= 0) { + throw new ExpiredRevisionClaimException("Invalid Revision was given for entity with ID '" + entityId + "'"); + } + } + + @Override + public void reset(final Collection revisions) { + // delete all revisions + jdbcTemplate.update("DELETE FROM REVISION"); + + // insert all the provided revisions + final String insertSql = "INSERT INTO REVISION(ENTITY_ID, VERSION, CLIENT_ID) VALUES (?, ?, ?)"; + for (final Revision revision : revisions) { + jdbcTemplate.update(insertSql, revision.getEntityId(), revision.getVersion(), revision.getClientId()); + } + } + + @Override + public List getAllRevisions() { + return jdbcTemplate.query("SELECT * FROM REVISION", new RevisionRowMapper()); + } + + @Override + public Map getRevisionMap() { + final Map revisionMap = new HashMap<>(); + final RevisionRowMapper rowMapper = new RevisionRowMapper(); + + jdbcTemplate.query("SELECT * FROM REVISION", (rs) -> { + final Revision revision = rowMapper.mapRow(rs, 0); + revisionMap.put(revision.getEntityId(), revision); + }); + + return revisionMap; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/src/main/java/org/apache/nifi/registry/revision/jdbc/RevisionRowMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/src/main/java/org/apache/nifi/registry/revision/jdbc/RevisionRowMapper.java new file mode 100644 index 0000000000..431e7c6454 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/src/main/java/org/apache/nifi/registry/revision/jdbc/RevisionRowMapper.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.revision.jdbc; + +import org.apache.nifi.registry.revision.api.Revision; +import org.springframework.jdbc.core.RowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class RevisionRowMapper implements RowMapper { + + @Override + public Revision mapRow(final ResultSet rs, final int i) throws SQLException { + final String entityId = rs.getString("ENTITY_ID"); + final Long version = rs.getLong("VERSION"); + final String clientId = rs.getString("CLIENT_ID"); + return new Revision(version, clientId, entityId); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/src/test/java/org/apache/nifi/registry/TestApplication.java b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/src/test/java/org/apache/nifi/registry/TestApplication.java new file mode 100644 index 0000000000..133cf8f201 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/src/test/java/org/apache/nifi/registry/TestApplication.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Loads an application context for tests. + * + * This class is purposely in the package org.apache.nifi.registry so that it scans downward and finds beans inside + * this module, as well as in dependencies that use the same base package. This allows this module to pick up the test + * DataSource factories in nifi-registry-test and leverage test-containers for DB testing. + */ +@SpringBootApplication +public class TestApplication { + + public static void main(String[] args) { + SpringApplication.run(TestApplication.class, args); + } + +} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/src/test/java/org/apache/nifi/registry/revision/jdbc/TestJdbcRevisionManager.java b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/src/test/java/org/apache/nifi/registry/revision/jdbc/TestJdbcRevisionManager.java new file mode 100644 index 0000000000..cbb7e463ba --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/src/test/java/org/apache/nifi/registry/revision/jdbc/TestJdbcRevisionManager.java @@ -0,0 +1,412 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.revision.jdbc; + +import org.apache.nifi.registry.TestApplication; +import org.apache.nifi.registry.revision.api.DeleteRevisionTask; +import org.apache.nifi.registry.revision.api.EntityModification; +import org.apache.nifi.registry.revision.api.InvalidRevisionException; +import org.apache.nifi.registry.revision.api.Revision; +import org.apache.nifi.registry.revision.api.RevisionClaim; +import org.apache.nifi.registry.revision.api.RevisionManager; +import org.apache.nifi.registry.revision.api.RevisionUpdate; +import org.apache.nifi.registry.revision.api.UpdateRevisionTask; +import org.apache.nifi.registry.revision.standard.StandardRevisionClaim; +import org.apache.nifi.registry.revision.standard.StandardUpdateResult; +import org.flywaydb.core.internal.jdbc.DatabaseType; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.transaction.TransactionalTestExecutionListener; +import org.springframework.transaction.annotation.Transactional; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +@Transactional +@RunWith(SpringRunner.class) +@SpringBootTest(classes = TestApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE) +@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, TransactionalTestExecutionListener.class}) +public class TestJdbcRevisionManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(TestJdbcRevisionManager.class); + + private static final String CREATE_TABLE_SQL_DEFAULT = + "CREATE TABLE REVISION (\n" + + " ENTITY_ID VARCHAR(50) NOT NULL,\n" + + " VERSION BIGINT NOT NULL DEFAULT (0),\n" + + " CLIENT_ID VARCHAR(100),\n" + + " CONSTRAINT PK__REVISION_ENTITY_ID PRIMARY KEY (ENTITY_ID)\n" + + ")"; + + private static final String CREATE_TABLE_SQL_MYSQL = + "CREATE TABLE REVISION (\n" + + " ENTITY_ID VARCHAR(50) NOT NULL,\n" + + " VERSION BIGINT NOT NULL DEFAULT 0,\n" + + " CLIENT_ID VARCHAR(100),\n" + + " CONSTRAINT PK__REVISION_ENTITY_ID PRIMARY KEY (ENTITY_ID)\n" + + ")"; + + @Autowired + private JdbcTemplate jdbcTemplate; + + private RevisionManager revisionManager; + + @Before + public void setup() throws SQLException { + revisionManager = new JdbcRevisionManager(jdbcTemplate); + + // Create the REVISION table if it does not exist + final DataSource dataSource = jdbcTemplate.getDataSource(); + LOGGER.info("#### DataSource class is {}", new Object[]{dataSource.getClass().getCanonicalName()}); + + try (final Connection connection = dataSource.getConnection()) { + final String createTableSql; + final DatabaseType databaseType = DatabaseType.fromJdbcConnection(connection); + if (databaseType == DatabaseType.MYSQL) { + createTableSql = CREATE_TABLE_SQL_MYSQL; + } else { + createTableSql = CREATE_TABLE_SQL_DEFAULT; + } + + final DatabaseMetaData meta = connection.getMetaData(); + try (final ResultSet res = meta.getTables(null, null, "REVISION", new String[]{"TABLE"})) { + if (!res.next()) { + jdbcTemplate.execute(createTableSql); + } + } + } + } + + @Test + public void testGetRevisionWhenDoesNotExist() { + final String entityId = "entity1"; + final Revision revision = revisionManager.getRevision(entityId); + assertNotNull(revision); + assertEquals(entityId, revision.getEntityId()); + assertEquals(0L, revision.getVersion().longValue()); + assertNull(revision.getClientId()); + } + + @Test + public void testGetRevisionWhenExists() { + final String entityId = "entity1"; + final Long version = new Long(99); + createRevision(entityId, version, null); + + final Revision revision = revisionManager.getRevision(entityId); + assertNotNull(revision); + assertEquals(entityId, revision.getEntityId()); + assertEquals(version.longValue(), revision.getVersion().longValue()); + assertNull(revision.getClientId()); + } + + @Test + public void testUpdateRevisionWithCurrentVersionNoClientId() { + // create the revision being sent in by the client + final String entityId = "entity-1"; + final Revision revision = new Revision(99L, null, entityId); + final RevisionClaim revisionClaim = new StandardRevisionClaim(revision); + + // seed the database with a matching revision + createRevision(revision.getEntityId(), revision.getVersion(), null); + + // perform an update task + final RevisionUpdate revisionUpdate = revisionManager.updateRevision( + revisionClaim, createUpdateTask(entityId)); + assertNotNull(revisionUpdate); + + // version should go to 100 since it was 99 before + verifyRevisionUpdate(entityId, revisionUpdate, new Long(100), null); + } + + @Test(expected = InvalidRevisionException.class) + public void testUpdateRevisionWithStaleVersionNoClientId() { + // create the revision being sent in by the client + final String entityId = "entity-1"; + final Revision revision = new Revision(99L, null, entityId); + final RevisionClaim revisionClaim = new StandardRevisionClaim(revision); + + // seed the database with a revision that has a newer version + createRevision(revision.getEntityId(), revision.getVersion() + 1, null); + + // perform an update task which should throw InvalidRevisionException + revisionManager.updateRevision(revisionClaim, createUpdateTask(entityId)); + } + + @Test + public void testUpdateRevisionWithStaleVersionAndSameClientId() { + // create the revision being sent in by the client + final String entityId = "entity-1"; + final String clientId = "client-1"; + final Revision revision = new Revision(99L, clientId, entityId); + final RevisionClaim revisionClaim = new StandardRevisionClaim(revision); + + // seed the database with a revision that has a newer version + createRevision(revision.getEntityId(), revision.getVersion() + 1, clientId); + + // perform an update task + final RevisionUpdate revisionUpdate = revisionManager.updateRevision( + revisionClaim, createUpdateTask(entityId)); + assertNotNull(revisionUpdate); + + // client in 99 which was not latest version, but since client id was the same the update was allowed + // and the incremented version should be based on the version in the DB which was 100, so it goes to 101 + verifyRevisionUpdate(entityId, revisionUpdate, new Long(101), clientId); + } + + @Test + public void testUpdateRevisionWhenDoesNotExist() { + // create the revision being sent in by the client + final String entityId = "entity-1"; + final String clientId = "client-new"; + final Revision revision = new Revision(0L, clientId, entityId); + final RevisionClaim revisionClaim = new StandardRevisionClaim(revision); + + // perform an update task + final RevisionUpdate revisionUpdate = revisionManager.updateRevision( + revisionClaim, createUpdateTask(entityId)); + assertNotNull(revisionUpdate); + + // version should go to 1 and client id should be updated to client-new + verifyRevisionUpdate(entityId, revisionUpdate, new Long(1), clientId); + } + + @Test + public void testUpdateRevisionWithCurrentVersionAndNewClientId() { + // create the revision being sent in by the client + final String entityId = "entity-1"; + final String clientId = "client-new"; + final Revision revision = new Revision(99L, clientId, entityId); + final RevisionClaim revisionClaim = new StandardRevisionClaim(revision); + + // seed the database with a revision that has same version but a different client id + createRevision(revision.getEntityId(), revision.getVersion(), "client-old"); + + // perform an update task + final RevisionUpdate revisionUpdate = revisionManager.updateRevision( + revisionClaim, createUpdateTask(entityId)); + assertNotNull(revisionUpdate); + + // version should go to 100 and client id should be updated to client-new + verifyRevisionUpdate(entityId, revisionUpdate, new Long(100), clientId); + } + + @Test + public void testDeleteRevisionWithCurrentVersionAndNoClientId() { + // create the revision being sent in by the client + final String entityId = "entity-1"; + final Revision revision = new Revision(99L, null, entityId); + final RevisionClaim revisionClaim = new StandardRevisionClaim(revision); + + // seed the database with a matching revision + createRevision(revision.getEntityId(), revision.getVersion(), null); + + // perform an update task + final RevisableEntity deletedEntity = revisionManager.deleteRevision( + revisionClaim, createDeleteTask(entityId)); + assertNotNull(deletedEntity); + assertEquals(entityId, deletedEntity.getId()); + } + + @Test(expected = InvalidRevisionException.class) + public void testDeleteRevisionWithStaleVersionAndNoClientId() { + // create the revision being sent in by the client + final String entityId = "entity-1"; + final Revision revision = new Revision(99L, null, entityId); + final RevisionClaim revisionClaim = new StandardRevisionClaim(revision); + + // seed the database with a revision that has a newer version + createRevision(revision.getEntityId(), revision.getVersion() + 1, null); + + // perform an update task which should throw InvalidRevisionException + revisionManager.deleteRevision(revisionClaim, createDeleteTask(entityId)); + } + + @Test + public void testDeleteRevisionWithStaleVersionAndSameClientId() { + // create the revision being sent in by the client + final String entityId = "entity-1"; + final String clientId = "client-1"; + final Revision revision = new Revision(99L, clientId, entityId); + final RevisionClaim revisionClaim = new StandardRevisionClaim(revision); + + // seed the database with a revision that has a newer version + createRevision(revision.getEntityId(), revision.getVersion() + 1, clientId); + + // perform the delete + final RevisableEntity deletedEntity = revisionManager.deleteRevision( + revisionClaim, createDeleteTask(entityId)); + assertNotNull(deletedEntity); + assertEquals(entityId, deletedEntity.getId()); + } + + @Test + public void testDeleteRevisionWithCurrentVersionAndNewClientId() { + // create the revision being sent in by the client + final String entityId = "entity-1"; + final String clientId = "client-new"; + final Revision revision = new Revision(99L, clientId, entityId); + final RevisionClaim revisionClaim = new StandardRevisionClaim(revision); + + // seed the database with a revision that has same version but a different client id + createRevision(revision.getEntityId(), revision.getVersion(), "client-old"); + + // perform the delete + final RevisableEntity deletedEntity = revisionManager.deleteRevision( + revisionClaim, createDeleteTask(entityId)); + assertNotNull(deletedEntity); + assertEquals(entityId, deletedEntity.getId()); + } + + @Test + public void testGetAllAndReset() { + createRevision("entity1", new Long(1), null); + createRevision("entity2", new Long(1), null); + + final List allRevisions = revisionManager.getAllRevisions(); + assertNotNull(allRevisions); + assertEquals(2, allRevisions.size()); + + final Revision resetRevision1 = new Revision(10L, null, "resetEntity1"); + final Revision resetRevision2 = new Revision(50L, null, "resetEntity2"); + final Revision resetRevision3 = new Revision(20L, "client1", "resetEntity3"); + revisionManager.reset(Arrays.asList(resetRevision1, resetRevision2, resetRevision3)); + + final List afterResetRevisions = revisionManager.getAllRevisions(); + assertNotNull(afterResetRevisions); + assertEquals(3, afterResetRevisions.size()); + + assertTrue(afterResetRevisions.contains(resetRevision1)); + assertTrue(afterResetRevisions.contains(resetRevision2)); + assertTrue(afterResetRevisions.contains(resetRevision3)); + } + + @Test + public void testGetRevisionMap() { + createRevision("entity1", new Long(1), null); + createRevision("entity2", new Long(1), null); + + final Map revisions = revisionManager.getRevisionMap(); + assertNotNull(revisions); + assertEquals(2, revisions.size()); + + final Revision revision1 = revisions.get("entity1"); + assertNotNull(revision1); + assertEquals("entity1", revision1.getEntityId()); + + final Revision revision2 = revisions.get("entity2"); + assertNotNull(revision2); + assertEquals("entity2", revision2.getEntityId()); + } + + private DeleteRevisionTask createDeleteTask(final String entityId) { + return () -> { + // normally we would retrieve the entity from some kind of service/dao + final RevisableEntity entity = new RevisableEntity(); + entity.setId(entityId); + return entity; + }; + } + + private UpdateRevisionTask createUpdateTask(final String entityId) { + return () -> { + // normally we would retrieve the entity from some kind of service/dao + final RevisableEntity entity = new RevisableEntity(); + entity.setId(entityId); + + return new StandardUpdateResult<>(entity, entityId,"user1"); + }; + } + + private void verifyRevisionUpdate(final String entityId, final RevisionUpdate revisionUpdate, + final Long expectedVersion, final String expectedClientId) { + // verify we got back the entity we expected + final RevisableEntity updatedEntity = revisionUpdate.getEntity(); + assertNotNull(updatedEntity); + assertEquals(entityId, updatedEntity.getId()); + + // verify the entity modification is correctly populated + final EntityModification entityModification = revisionUpdate.getLastModification(); + assertNotNull(entityModification); + Assert.assertEquals("user1", entityModification.getLastModifier()); + + // verify the revision in the entity modification is set and is the updated revision (i.e. version of 100, not 99) + final Revision updatedRevision = entityModification.getRevision(); + assertNotNull(updatedRevision); + assertEquals(entityId, updatedRevision.getEntityId()); + assertEquals(expectedVersion, updatedRevision.getVersion()); + assertEquals(expectedClientId, updatedRevision.getClientId()); + + // verify the updated revisions is correctly populated and matches the updated entity revision + final Set updatedRevisions = revisionUpdate.getUpdatedRevisions(); + assertNotNull(updatedRevisions); + assertEquals(1, updatedRevisions.size()); + assertEquals(updatedRevision, updatedRevisions.stream().findFirst().get()); + } + + private void createRevision(final String entityId, final Long version, final String clientId) { + jdbcTemplate.update("INSERT INTO REVISION(ENTITY_ID, VERSION, CLIENT_ID) VALUES(?, ?, ?)", entityId, version, clientId); + } + + /** + * Test object to represent a model/entity that has a revision field. + */ + private static class RevisableEntity { + + private String id; + + private Revision revision; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public Revision getRevision() { + return revision; + } + + public void setRevision(Revision revision) { + this.revision = revision; + } + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/src/test/resources/application.properties b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/src/test/resources/application.properties new file mode 100644 index 0000000000..82b66dca95 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/src/test/resources/application.properties @@ -0,0 +1,22 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +# Properties for Spring Boot tests + +# We are only using Flyway in tests to determine the DB type so there are no actual migrations +spring.flyway.check-location=false + +# Controls logging of SQL queries and parameters +# logging.level.org.springframework.jdbc: TRACE \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-revision/pom.xml b/nifi-registry/nifi-registry-core/nifi-registry-revision/pom.xml new file mode 100644 index 0000000000..d255f31ada --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-revision/pom.xml @@ -0,0 +1,34 @@ + + + + 4.0.0 + + org.apache.nifi.registry + nifi-registry-core + 1.14.0-SNAPSHOT + + nifi-registry-revision + pom + + + nifi-registry-revision-api + nifi-registry-revision-common + nifi-registry-revision-spring-jdbc + nifi-registry-revision-entity-model + nifi-registry-revision-entity-service + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-runtime/pom.xml b/nifi-registry/nifi-registry-core/nifi-registry-runtime/pom.xml new file mode 100644 index 0000000000..97cf91fd5a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-runtime/pom.xml @@ -0,0 +1,46 @@ + + + + 4.0.0 + + org.apache.nifi.registry + nifi-registry-core + 1.14.0-SNAPSHOT + + nifi-registry-runtime + jar + + + org.apache.nifi.registry + nifi-registry-utils + 1.14.0-SNAPSHOT + + + org.apache.nifi.registry + nifi-registry-properties + 1.14.0-SNAPSHOT + + + org.apache.nifi.registry + nifi-registry-jetty + 1.14.0-SNAPSHOT + + + org.slf4j + jul-to-slf4j + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-runtime/src/main/java/org/apache/nifi/registry/BootstrapListener.java b/nifi-registry/nifi-registry-core/nifi-registry-runtime/src/main/java/org/apache/nifi/registry/BootstrapListener.java new file mode 100644 index 0000000000..0eabe944f0 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-runtime/src/main/java/org/apache/nifi/registry/BootstrapListener.java @@ -0,0 +1,395 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry; + +import org.apache.nifi.registry.util.LimitingInputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.lang.management.LockInfo; +import java.lang.management.ManagementFactory; +import java.lang.management.MonitorInfo; +import java.lang.management.ThreadInfo; +import java.lang.management.ThreadMXBean; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class BootstrapListener { + + private static final Logger logger = LoggerFactory.getLogger(BootstrapListener.class); + + private final NiFiRegistry nifi; + private final int bootstrapPort; + private final String secretKey; + + private volatile Listener listener; + private volatile ServerSocket serverSocket; + + public BootstrapListener(final NiFiRegistry nifi, final int bootstrapPort) { + this.nifi = nifi; + this.bootstrapPort = bootstrapPort; + secretKey = UUID.randomUUID().toString(); + } + + public void start() throws IOException { + logger.debug("Starting Bootstrap Listener to communicate with Bootstrap Port {}", bootstrapPort); + + serverSocket = new ServerSocket(); + serverSocket.bind(new InetSocketAddress("localhost", 0)); + serverSocket.setSoTimeout(2000); + + final int localPort = serverSocket.getLocalPort(); + logger.info("Started Bootstrap Listener, Listening for incoming requests on port {}", localPort); + + listener = new Listener(serverSocket); + final Thread listenThread = new Thread(listener); + listenThread.setDaemon(true); + listenThread.setName("Listen to Bootstrap"); + listenThread.start(); + + logger.debug("Notifying Bootstrap that local port is {}", localPort); + sendCommand("PORT", new String[] { String.valueOf(localPort), secretKey}); + } + + public void stop() { + if (listener != null) { + listener.stop(); + } + } + + public void sendStartedStatus(boolean status) throws IOException { + logger.debug("Notifying Bootstrap that the status of starting NiFi Registry is {}", status); + sendCommand("STARTED", new String[]{ String.valueOf(status) }); + } + + private void sendCommand(final String command, final String[] args) throws IOException { + try (final Socket socket = new Socket()) { + socket.setSoTimeout(60000); + socket.connect(new InetSocketAddress("localhost", bootstrapPort)); + socket.setSoTimeout(60000); + + final StringBuilder commandBuilder = new StringBuilder(command); + for (final String arg : args) { + commandBuilder.append(" ").append(arg); + } + commandBuilder.append("\n"); + + final String commandWithArgs = commandBuilder.toString(); + logger.debug("Sending command to Bootstrap: " + commandWithArgs); + + final OutputStream out = socket.getOutputStream(); + out.write((commandWithArgs).getBytes(StandardCharsets.UTF_8)); + out.flush(); + + logger.debug("Awaiting response from Bootstrap..."); + final BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); + final String response = reader.readLine(); + if ("OK".equals(response)) { + logger.info("Successfully initiated communication with Bootstrap"); + } else { + logger.error("Failed to communicate with Bootstrap. Bootstrap may be unable to issue or receive commands from NiFi Registry "); + } + } + } + + private class Listener implements Runnable { + + private final ServerSocket serverSocket; + private final ExecutorService executor; + private volatile boolean stopped = false; + + public Listener(final ServerSocket serverSocket) { + this.serverSocket = serverSocket; + this.executor = Executors.newFixedThreadPool(2); + } + + public void stop() { + stopped = true; + + executor.shutdownNow(); + + try { + serverSocket.close(); + } catch (final IOException ioe) { + // nothing to really do here. we could log this, but it would just become + // confusing in the logs, as we're shutting down and there's no real benefit + } + } + + @Override + public void run() { + while (!stopped) { + try { + final Socket socket; + try { + logger.debug("Listening for Bootstrap Requests"); + socket = serverSocket.accept(); + } catch (final SocketTimeoutException ste) { + if (stopped) { + return; + } + + continue; + } catch (final IOException ioe) { + if (stopped) { + return; + } + + throw ioe; + } + + logger.debug("Received connection from Bootstrap"); + socket.setSoTimeout(5000); + + executor.submit(new Runnable() { + @Override + public void run() { + try { + final BootstrapRequest request = readRequest(socket.getInputStream()); + final BootstrapRequest.RequestType requestType = request.getRequestType(); + + switch (requestType) { + case PING: + logger.debug("Received PING request from Bootstrap; responding"); + echoPing(socket.getOutputStream()); + logger.debug("Responded to PING request from Bootstrap"); + break; + case SHUTDOWN: + logger.info("Received SHUTDOWN request from Bootstrap"); + echoShutdown(socket.getOutputStream()); + nifi.shutdownHook(); + return; + case DUMP: + logger.info("Received DUMP request from Bootstrap"); + writeDump(socket.getOutputStream()); + break; + } + } catch (final Throwable t) { + logger.error("Failed to process request from Bootstrap due to " + t.toString(), t); + } finally { + try { + socket.close(); + } catch (final IOException ioe) { + logger.warn("Failed to close socket to Bootstrap due to {}", ioe.toString()); + } + } + } + }); + } catch (final Throwable t) { + logger.error("Failed to process request from Bootstrap due to " + t.toString(), t); + } + } + } + } + + private static void writeDump(final OutputStream out) throws IOException { + final ThreadMXBean mbean = ManagementFactory.getThreadMXBean(); + final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out)); + + final ThreadInfo[] infos = mbean.dumpAllThreads(true, true); + final long[] deadlockedThreadIds = mbean.findDeadlockedThreads(); + final long[] monitorDeadlockThreadIds = mbean.findMonitorDeadlockedThreads(); + + final List sortedInfos = new ArrayList<>(infos.length); + for (final ThreadInfo info : infos) { + sortedInfos.add(info); + } + Collections.sort(sortedInfos, new Comparator() { + @Override + public int compare(ThreadInfo o1, ThreadInfo o2) { + return o1.getThreadName().toLowerCase().compareTo(o2.getThreadName().toLowerCase()); + } + }); + + final StringBuilder sb = new StringBuilder(); + for (final ThreadInfo info : sortedInfos) { + sb.append("\n"); + sb.append("\"").append(info.getThreadName()).append("\" Id="); + sb.append(info.getThreadId()).append(" "); + sb.append(info.getThreadState().toString()).append(" "); + + switch (info.getThreadState()) { + case BLOCKED: + case TIMED_WAITING: + case WAITING: + sb.append(" on "); + sb.append(info.getLockInfo()); + break; + default: + break; + } + + if (info.isSuspended()) { + sb.append(" (suspended)"); + } + if (info.isInNative()) { + sb.append(" (in native code)"); + } + + if (deadlockedThreadIds != null && deadlockedThreadIds.length > 0) { + for (final long id : deadlockedThreadIds) { + if (id == info.getThreadId()) { + sb.append(" ** DEADLOCKED THREAD **"); + } + } + } + + if (monitorDeadlockThreadIds != null && monitorDeadlockThreadIds.length > 0) { + for (final long id : monitorDeadlockThreadIds) { + if (id == info.getThreadId()) { + sb.append(" ** MONITOR-DEADLOCKED THREAD **"); + } + } + } + + final StackTraceElement[] stackTraces = info.getStackTrace(); + for (final StackTraceElement element : stackTraces) { + sb.append("\n\tat ").append(element); + + final MonitorInfo[] monitors = info.getLockedMonitors(); + for (final MonitorInfo monitor : monitors) { + if (monitor.getLockedStackFrame().equals(element)) { + sb.append("\n\t- waiting on ").append(monitor); + } + } + } + + final LockInfo[] lockInfos = info.getLockedSynchronizers(); + if (lockInfos.length > 0) { + sb.append("\n\t"); + sb.append("Number of Locked Synchronizers: ").append(lockInfos.length); + for (final LockInfo lockInfo : lockInfos) { + sb.append("\n\t- ").append(lockInfo.toString()); + } + } + + sb.append("\n"); + } + + if (deadlockedThreadIds != null && deadlockedThreadIds.length > 0) { + sb.append("\n\nDEADLOCK DETECTED!"); + sb.append("\nThe following thread IDs are deadlocked:"); + for (final long id : deadlockedThreadIds) { + sb.append("\n").append(id); + } + } + + if (monitorDeadlockThreadIds != null && monitorDeadlockThreadIds.length > 0) { + sb.append("\n\nMONITOR DEADLOCK DETECTED!"); + sb.append("\nThe following thread IDs are deadlocked:"); + for (final long id : monitorDeadlockThreadIds) { + sb.append("\n").append(id); + } + } + + writer.write(sb.toString()); + writer.flush(); + } + + private void echoPing(final OutputStream out) throws IOException { + out.write("PING\n".getBytes(StandardCharsets.UTF_8)); + out.flush(); + } + + private void echoShutdown(final OutputStream out) throws IOException { + out.write("SHUTDOWN\n".getBytes(StandardCharsets.UTF_8)); + out.flush(); + } + + @SuppressWarnings("resource") // we don't want to close the stream, as the caller will do that + private BootstrapRequest readRequest(final InputStream in) throws IOException { + // We want to ensure that we don't try to read data from an InputStream directly + // by a BufferedReader because any user on the system could open a socket and send + // a multi-gigabyte file without any new lines in order to crash the NiFi instance + // (or at least cause OutOfMemoryErrors, which can wreak havoc on the running instance). + // So we will limit the Input Stream to only 4 KB, which should be plenty for any request. + final LimitingInputStream limitingIn = new LimitingInputStream(in, 4096); + final BufferedReader reader = new BufferedReader(new InputStreamReader(limitingIn)); + + final String line = reader.readLine(); + final String[] splits = line.split(" "); + if (splits.length < 1) { + throw new IOException("Received invalid request from Bootstrap: " + line); + } + + final String requestType = splits[0]; + final String[] args; + if (splits.length == 1) { + throw new IOException("Received invalid request from Bootstrap; request did not have a secret key; request type = " + requestType); + } else if (splits.length == 2) { + args = new String[0]; + } else { + args = Arrays.copyOfRange(splits, 2, splits.length); + } + + final String requestKey = splits[1]; + if (!secretKey.equals(requestKey)) { + throw new IOException("Received invalid Secret Key for request type " + requestType); + } + + try { + return new BootstrapRequest(requestType, args); + } catch (final Exception e) { + throw new IOException("Received invalid request from Bootstrap; request type = " + requestType); + } + } + + private static class BootstrapRequest { + + public static enum RequestType { + + SHUTDOWN, + DUMP, + PING; + } + + private final RequestType requestType; + private final String[] args; + + public BootstrapRequest(final String request, final String[] args) { + this.requestType = RequestType.valueOf(request); + this.args = args; + } + + public RequestType getRequestType() { + return requestType; + } + + @SuppressWarnings("unused") + public String[] getArgs() { + return args; + } + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-runtime/src/main/java/org/apache/nifi/registry/NiFiRegistry.java b/nifi-registry/nifi-registry-core/nifi-registry-runtime/src/main/java/org/apache/nifi/registry/NiFiRegistry.java new file mode 100644 index 0000000000..455ab9d896 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-runtime/src/main/java/org/apache/nifi/registry/NiFiRegistry.java @@ -0,0 +1,205 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry; + +import org.apache.nifi.registry.jetty.JettyServer; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.apache.nifi.registry.properties.NiFiRegistryPropertiesLoader; +import org.apache.nifi.registry.properties.SensitivePropertyProtectionException; +import org.apache.nifi.registry.security.crypto.BootstrapFileCryptoKeyProvider; +import org.apache.nifi.registry.security.crypto.CryptoKeyProvider; +import org.apache.nifi.registry.security.crypto.MissingCryptoKeyException; +import org.apache.nifi.registry.util.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.bridge.SLF4JBridgeHandler; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.concurrent.TimeUnit; + +/** + * Main entry point for NiFiRegistry. + */ +public class NiFiRegistry { + + private static final Logger LOGGER = LoggerFactory.getLogger(NiFiRegistry.class); + + public static final String BOOTSTRAP_PORT_PROPERTY = "nifi.registry.bootstrap.listen.port"; + + public static final String NIFI_REGISTRY_PROPERTIES_FILE_PATH_PROPERTY = "nifi.registry.properties.file.path"; + public static final String NIFI_REGISTRY_BOOTSTRAP_FILE_PATH_PROPERTY = "nifi.registry.bootstrap.config.file.path"; + public static final String NIFI_REGISTRY_BOOTSTRAP_DOCS_DIR_PROPERTY = "nifi.registry.bootstrap.config.docs.dir"; + + public static final String RELATIVE_BOOTSTRAP_FILE_LOCATION = "conf/bootstrap.conf"; + public static final String RELATIVE_PROPERTIES_FILE_LOCATION = "conf/nifi-registry.properties"; + public static final String RELATIVE_DOCS_LOCATION = "docs"; + + private final JettyServer server; + private final BootstrapListener bootstrapListener; + private volatile boolean shutdown = false; + + public NiFiRegistry(final NiFiRegistryProperties properties, CryptoKeyProvider masterKeyProvider) + throws ClassNotFoundException, IOException, NoSuchMethodException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { + + Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { + @Override + public void uncaughtException(final Thread t, final Throwable e) { + LOGGER.error("An Unknown Error Occurred in Thread {}: {}", t, e.toString()); + LOGGER.error("", e); + } + }); + + // register the shutdown hook + Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { + @Override + public void run() { + // shutdown the jetty server + shutdownHook(); + } + })); + + final String bootstrapPort = System.getProperty(BOOTSTRAP_PORT_PROPERTY); + if (bootstrapPort != null) { + try { + final int port = Integer.parseInt(bootstrapPort); + + if (port < 1 || port > 65535) { + throw new RuntimeException("Failed to start NiFi Registry because system property '" + BOOTSTRAP_PORT_PROPERTY + "' is not a valid integer in the range 1 - 65535"); + } + + bootstrapListener = new BootstrapListener(this, port); + bootstrapListener.start(); + } catch (final NumberFormatException nfe) { + throw new RuntimeException("Failed to start NiFi Registry because system property '" + BOOTSTRAP_PORT_PROPERTY + "' is not a valid integer in the range 1 - 65535"); + } + } else { + LOGGER.info("NiFi Registry started without Bootstrap Port information provided; will not listen for requests from Bootstrap"); + bootstrapListener = null; + } + + // delete the web working dir - if the application does not start successfully + // the web app directories might be in an invalid state. when this happens + // jetty will not attempt to re-extract the war into the directory. by removing + // the working directory, we can be assured that it will attempt to extract the + // war every time the application starts. + File webWorkingDir = properties.getWebWorkingDirectory(); + FileUtils.deleteFilesInDirectory(webWorkingDir, null, LOGGER, true, true); + FileUtils.deleteFile(webWorkingDir, LOGGER, 3); + + // redirect JUL log events + SLF4JBridgeHandler.removeHandlersForRootLogger(); + SLF4JBridgeHandler.install(); + + final String docsDir = System.getProperty(NIFI_REGISTRY_BOOTSTRAP_DOCS_DIR_PROPERTY, RELATIVE_DOCS_LOCATION); + + final long startTime = System.nanoTime(); + server = new JettyServer(properties, masterKeyProvider, docsDir); + + if (shutdown) { + LOGGER.info("NiFi Registry has been shutdown via NiFi Registry Bootstrap. Will not start Controller"); + } else { + server.start(); + + if (bootstrapListener != null) { + bootstrapListener.sendStartedStatus(true); + } + + final long duration = System.nanoTime() - startTime; + LOGGER.info("Registry initialization took " + duration + " nanoseconds " + + "(" + (int) TimeUnit.SECONDS.convert(duration, TimeUnit.NANOSECONDS) + " seconds)."); + } + } + + protected void shutdownHook() { + try { + this.shutdown = true; + + LOGGER.info("Initiating shutdown of Jetty web server..."); + if (server != null) { + server.stop(); + } + if (bootstrapListener != null) { + bootstrapListener.stop(); + } + LOGGER.info("Jetty web server shutdown completed (nicely or otherwise)."); + } catch (final Throwable t) { + LOGGER.warn("Problem occurred ensuring Jetty web server was properly terminated due to " + t); + } + } + + /** + * Main entry point of the application. + * + * @param args things which are ignored + */ + public static void main(String[] args) { + LOGGER.info("Launching NiFi Registry..."); + + final CryptoKeyProvider masterKeyProvider; + final NiFiRegistryProperties properties; + try { + masterKeyProvider = getMasterKeyProvider(); + properties = initializeProperties(masterKeyProvider); + } catch (final IllegalArgumentException iae) { + throw new RuntimeException("Unable to load properties: " + iae, iae); + } + + try { + new NiFiRegistry(properties, masterKeyProvider); + } catch (final Throwable t) { + LOGGER.error("Failure to launch NiFi Registry due to " + t, t); + } + } + + public static CryptoKeyProvider getMasterKeyProvider() { + final String bootstrapConfigFilePath = System.getProperty(NIFI_REGISTRY_BOOTSTRAP_FILE_PATH_PROPERTY, RELATIVE_BOOTSTRAP_FILE_LOCATION); + CryptoKeyProvider masterKeyProvider = new BootstrapFileCryptoKeyProvider(bootstrapConfigFilePath); + LOGGER.info("Read property protection key from {}", bootstrapConfigFilePath); + return masterKeyProvider; + } + + public static NiFiRegistryProperties initializeProperties(CryptoKeyProvider masterKeyProvider) { + String key = CryptoKeyProvider.EMPTY_KEY; + try { + key = masterKeyProvider.getKey(); + } catch (MissingCryptoKeyException e) { + LOGGER.debug("CryptoKeyProvider provided to initializeProperties method was empty - did not contain a key."); + // Do nothing. The key can be empty when it is passed to the loader as the loader will only use it if any properties are protected. + } + + try { + try { + // Load properties using key. If properties are protected and key missing, throw RuntimeException + final String nifiRegistryPropertiesFilePath = System.getProperty(NIFI_REGISTRY_PROPERTIES_FILE_PATH_PROPERTY, RELATIVE_PROPERTIES_FILE_LOCATION); + final NiFiRegistryProperties properties = NiFiRegistryPropertiesLoader.withKey(key).load(nifiRegistryPropertiesFilePath); + LOGGER.info("Loaded {} properties", properties.size()); + return properties; + } catch (SensitivePropertyProtectionException e) { + final String msg = "There was an issue decrypting protected properties"; + LOGGER.error(msg, e); + throw new IllegalArgumentException(msg); + } + } catch (IllegalArgumentException e) { + final String msg = "The bootstrap process did not provide a valid key and there are protected properties present in the properties file"; + LOGGER.error(msg, e); + throw new IllegalArgumentException(msg); + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-runtime/src/main/java/org/apache/nifi/registry/util/LimitingInputStream.java b/nifi-registry/nifi-registry-core/nifi-registry-runtime/src/main/java/org/apache/nifi/registry/util/LimitingInputStream.java new file mode 100644 index 0000000000..c069ec22c8 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-runtime/src/main/java/org/apache/nifi/registry/util/LimitingInputStream.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.util; + +import java.io.IOException; +import java.io.InputStream; + +public class LimitingInputStream extends InputStream { + + private final InputStream in; + private final long limit; + private long bytesRead = 0; + + public LimitingInputStream(final InputStream in, final long limit) { + this.in = in; + this.limit = limit; + } + + @Override + public int read() throws IOException { + if (bytesRead >= limit) { + return -1; + } + + final int val = in.read(); + if (val > -1) { + bytesRead++; + } + return val; + } + + @Override + public int read(final byte[] b) throws IOException { + if (bytesRead >= limit) { + return -1; + } + + final int maxToRead = (int) Math.min(b.length, limit - bytesRead); + + final int val = in.read(b, 0, maxToRead); + if (val > 0) { + bytesRead += val; + } + return val; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (bytesRead >= limit) { + return -1; + } + + final int maxToRead = (int) Math.min(len, limit - bytesRead); + + final int val = in.read(b, off, maxToRead); + if (val > 0) { + bytesRead += val; + } + return val; + } + + @Override + public long skip(final long n) throws IOException { + final long skipped = in.skip(Math.min(n, limit - bytesRead)); + bytesRead += skipped; + return skipped; + } + + @Override + public int available() throws IOException { + return in.available(); + } + + @Override + public void close() throws IOException { + in.close(); + } + + @Override + public void mark(int readlimit) { + in.mark(readlimit); + } + + @Override + public boolean markSupported() { + return in.markSupported(); + } + + @Override + public void reset() throws IOException { + in.reset(); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/pom.xml b/nifi-registry/nifi-registry-core/nifi-registry-security-api/pom.xml new file mode 100644 index 0000000000..7fd650654b --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/pom.xml @@ -0,0 +1,41 @@ + + + + + nifi-registry-core + org.apache.nifi.registry + 1.14.0-SNAPSHOT + + 4.0.0 + + nifi-registry-security-api + + + + org.apache.nifi.registry + nifi-registry-utils + + + javax.servlet + javax.servlet-api + 3.1.0 + provided + + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/AuthenticationRequest.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/AuthenticationRequest.java new file mode 100644 index 0000000000..72ae50e062 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/AuthenticationRequest.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authentication; + +import java.io.Serializable; + +public class AuthenticationRequest implements Serializable { + + private String username; + private Object credentials; + private Object details; + + public AuthenticationRequest(String username, Object credentials, Object details) { + this.username = username; + this.credentials = credentials; + this.details = details; + } + + public AuthenticationRequest() {} + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public Object getCredentials() { + return credentials; + } + + public void setCredentials(Object credentials) { + this.credentials = credentials; + } + + public Object getDetails() { + return details; + } + + public void setDetails(Object details) { + this.details = details; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + AuthenticationRequest that = (AuthenticationRequest) o; + + return username != null ? username.equals(that.username) : that.username == null; + } + + @Override + public int hashCode() { + return username != null ? username.hashCode() : 0; + } + + @Override + public String toString() { + return "AuthenticationRequest{" + + "username='" + username + '\'' + + ", credentials=[PROTECTED]" + + ", details=" + details + + '}'; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/AuthenticationResponse.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/AuthenticationResponse.java new file mode 100644 index 0000000000..b8eb721838 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/AuthenticationResponse.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authentication; + +import java.io.Serializable; + +/** + * Authentication response for a user login attempt. + */ +public class AuthenticationResponse implements Serializable { + + private final String identity; + private final String username; + private final long expiration; + private final String issuer; + + /** + * Creates an authentication response. The username and how long the authentication is valid in milliseconds + * + * @param identity The user identity + * @param username The username + * @param expiration The expiration in milliseconds + * @param issuer The issuer of the token + */ + public AuthenticationResponse(final String identity, final String username, final long expiration, final String issuer) { + this.identity = identity; + this.username = username; + this.expiration = expiration; + this.issuer = issuer; + } + + public String getIdentity() { + return identity; + } + + public String getUsername() { + return username; + } + + public String getIssuer() { + return issuer; + } + + /** + * Returns the expiration of a given authentication in milliseconds. + * + * @return The expiration in milliseconds + */ + public long getExpiration() { + return expiration; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + AuthenticationResponse that = (AuthenticationResponse) o; + + if (expiration != that.expiration) return false; + if (identity != null ? !identity.equals(that.identity) : that.identity != null) return false; + if (username != null ? !username.equals(that.username) : that.username != null) return false; + return issuer != null ? issuer.equals(that.issuer) : that.issuer == null; + } + + @Override + public int hashCode() { + int result = identity != null ? identity.hashCode() : 0; + result = 31 * result + (username != null ? username.hashCode() : 0); + result = 31 * result + (int) (expiration ^ (expiration >>> 32)); + result = 31 * result + (issuer != null ? issuer.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "AuthenticationResponse{" + + "identity='" + identity + '\'' + + ", username='" + username + '\'' + + ", expiration=" + expiration + + ", issuer='" + issuer + '\'' + + '}'; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/BasicAuthIdentityProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/BasicAuthIdentityProvider.java new file mode 100644 index 0000000000..64c8b8e70f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/BasicAuthIdentityProvider.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authentication; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.http.HttpServletRequest; +import java.nio.charset.Charset; +import java.util.Base64; + +public abstract class BasicAuthIdentityProvider implements IdentityProvider { + + public static final String AUTHORIZATION = "Authorization"; + public static final String BASIC = "Basic "; + + private static final Logger logger = LoggerFactory.getLogger(BasicAuthIdentityProvider.class); + + private static final IdentityProviderUsage usage = new IdentityProviderUsage() { + @Override + public String getText() { + return "The user credentials must be passed in standard HTTP Basic Auth format. " + + "That is: 'Authorization: Basic ', " + + "where is the base64 encoded value of ':'."; + } + + @Override + public AuthType getAuthType() { + return AuthType.BASIC; + } + }; + + @Override + public IdentityProviderUsage getUsageInstructions() { + return usage; + } + + @Override + public AuthenticationRequest extractCredentials(HttpServletRequest servletRequest) { + + if (servletRequest == null) { + logger.debug("Cannot extract user credentials from null servletRequest"); + return null; + } + + // only support this type of login when running securely + if (!servletRequest.isSecure()) { + return null; + } + + final String authorization = servletRequest.getHeader(AUTHORIZATION); + if (authorization == null || !authorization.startsWith(BASIC)) { + logger.debug("HTTP Basic Auth credentials not present. Not attempting to extract credentials for authentication."); + return null; + } + + AuthenticationRequest authenticationRequest; + + try { + + // Authorization: Basic {base64credentials} + String base64Credentials = authorization.substring(BASIC.length()).trim(); + String credentials = new String(Base64.getDecoder().decode(base64Credentials), Charset.forName("UTF-8")); + // credentials = username:password + final String[] credentialParts = credentials.split(":", 2); + String username = credentialParts[0]; + String password = credentialParts[1]; + + authenticationRequest = new UsernamePasswordAuthenticationRequest(username, password); + + } catch (IllegalArgumentException | IndexOutOfBoundsException e) { + logger.info("Failed to extract user identity credentials."); + logger.debug("", e); + return null; + } + + return authenticationRequest; + + } + + @Override + public boolean supports(Class authenticationRequestClazz) { + return UsernamePasswordAuthenticationRequest.class.isAssignableFrom(authenticationRequestClazz); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/BearerAuthIdentityProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/BearerAuthIdentityProvider.java new file mode 100644 index 0000000000..06477822ad --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/BearerAuthIdentityProvider.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authentication; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.http.HttpServletRequest; + +public abstract class BearerAuthIdentityProvider implements IdentityProvider { + + public static final String AUTHORIZATION = "Authorization"; + public static final String BEARER = "Bearer "; + + private static final Logger logger = LoggerFactory.getLogger(BearerAuthIdentityProvider.class); + + private static final IdentityProviderUsage usage = new IdentityProviderUsage() { + @Override + public String getText() { + return "The user credentials must be passed in standard HTTP Bearer Authorization format. " + + "That is: 'Authorization: Bearer ', " + + "where is a value that will be validated by this identity provider."; + } + + @Override + public AuthType getAuthType() { + return AuthType.BEARER; + } + }; + + @Override + public IdentityProviderUsage getUsageInstructions() { + return usage; + } + + @Override + public AuthenticationRequest extractCredentials(HttpServletRequest request) { + + if (request == null) { + logger.debug("Cannot extract user credentials from null servletRequest"); + return null; + } + + // only support this type of login when running securely + if (!request.isSecure()) { + return null; + } + + // get the principal out of the user token + final String authorization = request.getHeader(AUTHORIZATION); + if (authorization == null || !authorization.startsWith(BEARER)) { + logger.debug("HTTP Bearer Auth credentials not present. Not attempting to extract credentials for authentication."); + return null; + } + + // Extract the encoded token from the Authorization header + final String token = authorization.substring(BEARER.length()).trim(); + + return new AuthenticationRequest(null, token, null); + + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProvider.java new file mode 100644 index 0000000000..88488fbe63 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProvider.java @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authentication; + +import org.apache.nifi.registry.security.authentication.exception.IdentityAccessException; +import org.apache.nifi.registry.security.authentication.exception.InvalidCredentialsException; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException; + +import javax.servlet.http.HttpServletRequest; + +/** + * IdentityProvider is an interface for a class that is able to establish a client identity. + * + * Specifically, this provider can: + * - extract credentials from an HttpServletRequest (eg, parse a header, form parameter, or client certificates) + * - authenticate those credentials and map them to an authenticated identity value + * (eg, determine a username given a valid auth token) + */ +public interface IdentityProvider { + + /** + * @return an IdentityProviderUsage that describes the expectations of the inputs + * to {@link #authenticate(AuthenticationRequest)} + */ + IdentityProviderUsage getUsageInstructions(); + + /** + * Extracts credentials from an {@link HttpServletRequest}. + * + * First, a check to the HttpServletRequest should be made to determine if this IdentityProvider is + * well suited to authenticate the request. For example, if the IdentityProvider is designed to read + * a particular header field to look for a token or identity claim, the check might be that the proper + * header field exists and (if a shared header field, such as "Authorization") that the format of the + * value in the header matches the expected format for this identity provider (e.g., must start with + * a prefix such as "Bearer"). Note, the expectations of the HttpServletRequest can be described by + * the {@link #getUsageInstructions()} method. + * + * If this check fails, this method should return null. This will indicate to the framework that the + * IdentityProvider does not recognize an identity claim present in the HttpServletRequest and that + * the framework should try another IdentityProvider. + * + * If the identity claim format is recognized, it should be extracted and returned in an + * {@link AuthenticationRequest}. The types and values set in the {@link AuthenticationRequest} are + * left to the discretion of the IdentityProvider, as the intended audience of the request is the + * {@link #authenticate(AuthenticationRequest)} method, where the corresponding logic to interpret + * an {@link AuthenticationRequest} can be implemented. As a rule of thumb, any values that could be considered + * sensitive, such as a password or persistent token susceptible to replay attacks, should be stored + * in the credentials field of the {@link AuthenticationRequest} as the framework will make the most effort + * to protect that value, including obscuring it in toString() output. + * + * If the {@link AuthenticationRequest} is insufficient or too generic for this IdentityProvider implementation, + * this IdentityProvider may subclass {@link AuthenticationRequest} to create a credentials-bearing request + * object that is better suited for this IdentityProvider implementation. In that case, the implementation + * might wish to also override the {@link #supports(Class)} method to indicate what types of request + * objects it supports in the call to {@link #authenticate(AuthenticationRequest)}. + * + * If credential location is recognized in the {@link HttpServletRequest} but extraction fails, + * in most cases that exceptional case should be caught, logged, and null should be returned, as it + * is possible another IdentityProvider will be able to parse the credentials or find a separate + * set of credentials in the {@link HttpServletRequest} (e.g., a request containing an Authorization + * header and a client certificate.) + * + * @param servletRequest the {@link HttpServletRequest} request that may contain credentials + * understood by this IdentityProvider + * @return an AuthenticationRequest containing the extracted credentials in a format this + * IdentityProvider understands, or null if no credentials could be found in or extracted + * successfully from the servletRequest + */ + AuthenticationRequest extractCredentials(HttpServletRequest servletRequest); + + /** + * Authenticates the credentials passed in the {@link AuthenticationRequest}. + * + * In typical usage, the AuthenticationRequest argument is expected to originate from this + * IdentityProvider's {@link #extractCredentials} method, so the logic for interpreting the + * values in the {@link AuthenticationRequest} should correspond to how the {@link AuthenticationRequest} + * is formed there. + * + * The first step of authentication should be to check if the credentials are understandable + * by this IdentityProvider. If this check fails, this method should return null. This will + * indicate to the framework that the IdentityProvider is not able to make a judgement call + * on if the request can be authenticated, and the framework can check with another IdentityProvider + * if one is available. + * + * If this IdentityProvider is able to interpret the AuthenticationRequest, it should perform + * and authentication check. If the authentication check fails, an exception should be thrown. + * Use an {@link InvalidCredentialsException} if the authentication check completed and the + * credentials failed authentication. Use an {@link IdentityAccessException} if a dependency + * service or provider fails, such as an failure to read a persistent store of identity or + * credential data. Either exception type will indicate to the framework that this IdentityProvider's + * opinion is that the client making the request should be blocked from accessing a resource + * that requires authentication. (Versus a null return value, which is an indication that this + * IdentityProvider is not well suited to make a judgement call one way or the other.) + * + * @param authenticationRequest the request, containing identity claim credentials for the + * IdentityProvider to authenticate and determine an identity + * @return The authentication response containing a fully populated identity value, + * or null if identity cannot be determined + * @throws InvalidCredentialsException The login credentials were interpretable by this + * IdentityProvider and failed authentication + * @throws IdentityAccessException Unable to assign an identity due to an issue accessing + * underlying storage or service + */ + AuthenticationResponse authenticate(AuthenticationRequest authenticationRequest) + throws InvalidCredentialsException, IdentityAccessException; + + /** + * Allows this IdentityProvider to declare support for specific subclasses of {@link AuthenticationRequest}. + * + * In normal usage, only an AuthenticationRequest originating from this IdentityProvider's + * {@link #extractCredentials(HttpServletRequest)} method will be passed to {@link #authenticate(AuthenticationRequest)}. + * However, when IdentityProviders are used with another framework, + * another component may formulate the AuthenticationRequest to pass to the + * {@link #authenticate(AuthenticationRequest)} method. This allows a caller to + * check if the IdentityProvider can support the AuthenticationRequest class. + * If the caller knows the IdentityProvider can support the AuthenticationRequest + * (e.g., it was generated by calling {@link #extractCredentials(HttpServletRequest)}, + * this check is optional and does not need to be performed. + * + * @param authenticationRequestClazz the class the caller wants to check + * @return a boolean value indicating if this IdentityProvider supports authenticationRequestClazz + */ + default boolean supports(Class authenticationRequestClazz) { + return AuthenticationRequest.class.equals(authenticationRequestClazz); + } + + /** + * Called to configure the AuthorityProvider after instance creation. + * + * @param configurationContext at the time of configuration + * @throws SecurityProviderCreationException for any issues configuring the provider + */ + void onConfigured(IdentityProviderConfigurationContext configurationContext) throws SecurityProviderCreationException; + + /** + * Called immediately before instance destruction for implementers to release resources. + * + * @throws SecurityProviderDestructionException If pre-destruction fails. + */ + void preDestruction() throws SecurityProviderDestructionException; + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderConfigurationContext.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderConfigurationContext.java new file mode 100644 index 0000000000..6be02077da --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderConfigurationContext.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authentication; + +import java.util.Map; + +public interface IdentityProviderConfigurationContext { + + /** + * @return identifier for the authority provider + */ + String getIdentifier(); + + /** + * @return the IdentityProviderLookup from the factory context + */ + public IdentityProviderLookup getIdentityProviderLookup(); + + /** + * Retrieves all properties the component currently understands regardless + * of whether a value has been set for them or not. If no value is present + * then its value is null and thus any registered default for the property + * descriptor applies. + * + * @return Map of all properties + */ + Map getProperties(); + + /** + * @param property to lookup the descriptor and value of + * @return the value the component currently understands for the given + * PropertyDescriptor. This method does not substitute default + * PropertyDescriptor values, so the value returned will be null if not set + */ + String getProperty(String property); +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderLookup.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderLookup.java new file mode 100644 index 0000000000..dbf6d58cd0 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderLookup.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authentication; + +public interface IdentityProviderLookup { + + IdentityProvider getIdentityProvider(String identifier); + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderUsage.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderUsage.java new file mode 100644 index 0000000000..aefc97df61 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderUsage.java @@ -0,0 +1,135 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authentication; + +public interface IdentityProviderUsage { + + /** + * Provides the usage instructions for an identity provider. + * + * The instructions should target a human consumer of the + * NiFi Registry REST API that needs to know how to handle + * Authentication when using / programming an API client. + * + * @return the usage instructions for an identity provider + */ + String getText(); + + /** + * If the identity provider follows an HTTP standard auth + * scheme, this provides which scheme is being used + * (or "Other" if the identity provider follows its own scheme). + * + * In the case the scheme is well understood, such as HTTP + * "Basic" Auth, this may be sufficient. In other cases, + * {@link #getText()} should provider detailed human-readable + * instructions about how a client should interact with + * the {@link IdentityProvider}. + * + * @return an enum for the auth + */ + AuthType getAuthType(); + + /** + * Standard auth types as maintained by IANA: + * https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml + * + * Note, draft and experimental standards are not included, nor are app-specific custom schemes. + * To create an enum for such a scheme, use OTHER with a custom httpAuthScheme string, e.g.: + * + * AuthType myAuthType = AuthType.OTHER.httpAuthScheme("my-auth-scheme"); + */ + enum AuthType { + + /** + * Indicates the AuthType is unknown. Can be used in places where an AuthType is required but unknown by default. + */ + UNKNOWN(0, "Unknown"), + + /** + * HTTP Basic Auth as defined by RFC7617. + */ + BASIC(1, "Basic"), + + /** + * HTTP Bearer Auth as defined by RFC6750. + */ + BEARER(2, "Bearer"), + + /** + * HTTP Digest Auth as defined by RFC7616. + */ + DIGEST(3, "Digest"), + + /** + * HTTP Negotiate (SPNEGO) Auth as defined by RFC4559. + */ + NEGOTIATE(4, "Negotiate"), + + /** + * HTTP OAuth as defined by RFC5849 + */ + OAUTH(5, "OAuth"), + + /** + * A distinct AuthType for which there is not yet a defined enumeration value. + * If a HTTP Auth Scheme should be set (e.g., for use in a WWW-Authenticate challenge list) + * use the setter, i.e.: + * AuthType myAuthType = AuthType.OTHER.httpAuthScheme("my-auth-scheme"); + */ + OTHER(99, "Other"), + ; + + private final int code; + private String httpAuthScheme; + + private AuthType(int statusCode, String httpAuthScheme) { + this.code = statusCode; + this.httpAuthScheme = httpAuthScheme; + } + + public int getStatusCode() { + return this.code; + } + + public String getHttpAuthScheme() { + return this.toString(); + } + + public AuthType httpAuthScheme(String httpAuthScheme) { + if (httpAuthScheme != null) { + this.httpAuthScheme = httpAuthScheme; + } + return this; + } + + public String toString() { + return this.httpAuthScheme; + } + + public static AuthType fromCode(int code) { + AuthType[] enumTypes = values(); + for (AuthType s : enumTypes) { + if (s.code == code) { + return s; + } + } + return null; + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/UsernamePasswordAuthenticationRequest.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/UsernamePasswordAuthenticationRequest.java new file mode 100644 index 0000000000..3abcf94466 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/UsernamePasswordAuthenticationRequest.java @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authentication; + +public class UsernamePasswordAuthenticationRequest extends AuthenticationRequest { + + public UsernamePasswordAuthenticationRequest(String username, String password) { + super(username, password, null); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/annotation/IdentityProviderContext.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/annotation/IdentityProviderContext.java new file mode 100644 index 0000000000..8d0ddf0a12 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/annotation/IdentityProviderContext.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authentication.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Documented +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface IdentityProviderContext { +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/exception/IdentityAccessException.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/exception/IdentityAccessException.java new file mode 100644 index 0000000000..fae567a612 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/exception/IdentityAccessException.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authentication.exception; + +/** + * Represents the case when the identity could not be confirmed because it was unable + * to access the backing store. + */ +public class IdentityAccessException extends RuntimeException { + + public IdentityAccessException(String message, Throwable cause) { + super(message, cause); + } + + public IdentityAccessException(String message) { + super(message); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/exception/InvalidCredentialsException.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/exception/InvalidCredentialsException.java new file mode 100644 index 0000000000..e7c7339fcd --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/exception/InvalidCredentialsException.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authentication.exception; + +/** + * Represents the case when the identity could not be confirmed because the + * identity claim credentials were invalid. + */ +public class InvalidCredentialsException extends RuntimeException { + + public InvalidCredentialsException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidCredentialsException(String message) { + super(message); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AccessPolicy.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AccessPolicy.java new file mode 100644 index 0000000000..aa8260b9c7 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AccessPolicy.java @@ -0,0 +1,367 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; + +/** + * Defines a policy for a set of userIdentifiers to perform a set of actions on a given resource. + */ +public class AccessPolicy { + + private final String identifier; + + private final String resource; + + private final Set users; + + private final Set groups; + + private final RequestAction action; + + private AccessPolicy(final Builder builder) { + this.identifier = builder.identifier; + this.resource = builder.resource; + this.action = builder.action; + this.users = Collections.unmodifiableSet(new HashSet<>(builder.users)); + this.groups = Collections.unmodifiableSet(new HashSet<>(builder.groups)); + + if (this.identifier == null || this.identifier.trim().isEmpty()) { + throw new IllegalArgumentException("Identifier can not be null or empty"); + } + + if (this.resource == null) { + throw new IllegalArgumentException("Resource can not be null"); + } + + if (this.action == null) { + throw new IllegalArgumentException("Action can not be null"); + } + } + + /** + * @return the identifier for this policy + */ + public String getIdentifier() { + return identifier; + } + + /** + * @return the resource for this policy + */ + public String getResource() { + return resource; + } + + /** + * @return an unmodifiable set of user ids for this policy + */ + public Set getUsers() { + return users; + } + + /** + * @return an unmodifiable set of group ids for this policy + */ + public Set getGroups() { + return groups; + } + + /** + * @return the action for this policy + */ + public RequestAction getAction() { + return action; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + + final AccessPolicy other = (AccessPolicy) obj; + return Objects.equals(this.identifier, other.identifier); + } + + @Override + public int hashCode() { + return Objects.hashCode(this.identifier); + } + + @Override + public String toString() { + return String.format("identifier[%s], resource[%s], users[%s], groups[%s], action[%s]", + getIdentifier(), getResource(), getUsers(), getGroups(), getAction()); + } + + /** + * Builder for Access Policies. + */ + public static class Builder { + + private String identifier; + private String resource; + private RequestAction action; + private Set users = new HashSet<>(); + private Set groups = new HashSet<>(); + private final boolean fromPolicy; + + /** + * Default constructor for building a new AccessPolicy. + */ + public Builder() { + this.fromPolicy = false; + } + + /** + * Initializes the builder with the state of the provided policy. When using this constructor + * the identifier field of the builder can not be changed and will result in an IllegalStateException + * if attempting to do so. + * + * @param other the existing access policy to initialize from + */ + public Builder(final AccessPolicy other) { + if (other == null) { + throw new IllegalArgumentException("Can not initialize builder with a null access policy"); + } + + this.identifier = other.getIdentifier(); + this.resource = other.getResource(); + this.action = other.getAction(); + this.users.clear(); + this.users.addAll(other.getUsers()); + this.groups.clear(); + this.groups.addAll(other.getGroups()); + this.fromPolicy = true; + } + + /** + * Sets the identifier of the builder. + * + * @param identifier the identifier to set + * @return the builder + * @throws IllegalStateException if this method is called when this builder was constructed from an existing Policy + */ + public Builder identifier(final String identifier) { + if (fromPolicy) { + throw new IllegalStateException( + "Identifier can not be changed when initialized from an existing policy"); + } + + this.identifier = identifier; + return this; + } + + /** + * Sets the identifier of the builder to a random UUID. + * + * @return the builder + * @throws IllegalStateException if this method is called when this builder was constructed from an existing Policy + */ + public Builder identifierGenerateRandom() { + if (fromPolicy) { + throw new IllegalStateException( + "Identifier can not be changed when initialized from an existing policy"); + } + + this.identifier = UUID.randomUUID().toString(); + return this; + } + + /** + * Sets the identifier of the builder with a UUID generated from the specified seed string. + * + * @return the builder + * @throws IllegalStateException if this method is called when this builder was constructed from an existing Policy + */ + public Builder identifierGenerateFromSeed(final String seed) { + if (fromPolicy) { + throw new IllegalStateException( + "Identifier can not be changed when initialized from an existing policy"); + } + if (seed == null) { + throw new IllegalArgumentException("Cannot seed the policy identifier with a null value."); + } + + this.identifier = UUID.nameUUIDFromBytes(seed.getBytes(StandardCharsets.UTF_8)).toString(); + return this; + } + + /** + * Sets the resource of the builder. + * + * @param resource the resource to set + * @return the builder + */ + public Builder resource(final String resource) { + this.resource = resource; + return this; + } + + /** + * Adds all the users from the provided set to the builder's set of users. + * + * @param users the users to add + * @return the builder + */ + public Builder addUsers(final Set users) { + if (users != null) { + this.users.addAll(users); + } + return this; + } + + /** + * Adds the given user to the builder's set of users. + * + * @param user the user to add + * @return the builder + */ + public Builder addUser(final String user) { + if (user != null) { + this.users.add(user); + } + return this; + } + + /** + * Removes all users in the provided set from the builder's set of users. + * + * @param users the users to remove + * @return the builder + */ + public Builder removeUsers(final Set users) { + if (users != null) { + this.users.removeAll(users); + } + return this; + } + + /** + * Removes the provided user from the builder's set of users. + * + * @param user the user to remove + * @return the builder + */ + public Builder removeUser(final String user) { + if (user != null) { + this.users.remove(user); + } + return this; + } + + /** + * Clears the builder's set of users so that it is non-null and size == 0. + * + * @return the builder + */ + public Builder clearUsers() { + this.users.clear(); + return this; + } + + /** + * Adds all the groups from the provided set to the builder's set of groups. + * + * @param groups the groups to add + * @return the builder + */ + public Builder addGroups(final Set groups) { + if (groups != null) { + this.groups.addAll(groups); + } + return this; + } + + /** + * Adds the given group to the builder's set of groups. + * + * @param group the group to add + * @return the builder + */ + public Builder addGroup(final String group) { + if (group != null) { + this.groups.add(group); + } + return this; + } + + /** + * Removes all groups in the provided set from the builder's set of groups. + * + * @param groups the groups to remove + * @return the builder + */ + public Builder removeGroups(final Set groups) { + if (groups != null) { + this.groups.removeAll(groups); + } + return this; + } + + /** + * Removes the provided groups from the builder's set of groups. + * + * @param group the group to remove + * @return the builder + */ + public Builder removeGroup(final String group) { + if (group != null) { + this.groups.remove(group); + } + return this; + } + + /** + * Clears the builder's set of groups so that it is non-null and size == 0. + * + * @return the builder + */ + public Builder clearGroups() { + this.groups.clear(); + return this; + } + + /** + * Sets the action for this builder. + * + * @param action the action to set + * @return the builder + */ + public Builder action(final RequestAction action) { + this.action = action; + return this; + } + + /** + * @return a new AccessPolicy constructed from the state of the builder + */ + public AccessPolicy build() { + return new AccessPolicy(this); + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AccessPolicyProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AccessPolicyProvider.java new file mode 100644 index 0000000000..214787de81 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AccessPolicyProvider.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException; + +import java.util.Set; + +/** + * Provides access to AccessPolicies and the configured UserGroupProvider. + * + * NOTE: Extensions will be called often and frequently. Because of this, if the underlying implementation needs to + * make remote calls or expensive calculations those should probably be done asynchronously and/or cache the results. + * + * Additionally, extensions need to be thread safe. + */ +public interface AccessPolicyProvider { + + /** + * Retrieves all access policies. Must be non null + * + * @return a list of policies + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + Set getAccessPolicies() throws AuthorizationAccessException; + + /** + * Retrieves the policy with the given identifier. + * + * @param identifier the id of the policy to retrieve + * @return the policy with the given id, or null if no matching policy exists + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + AccessPolicy getAccessPolicy(String identifier) throws AuthorizationAccessException; + + /** + * Gets the access policies for the specified resource identifier and request action. + * + * @param resourceIdentifier the resource identifier + * @param action the request action + * @return the policy matching the resouce and action, or null if no matching policy exists + * @throws AuthorizationAccessException if there was any unexpected error performing the operation + */ + AccessPolicy getAccessPolicy(String resourceIdentifier, RequestAction action) throws AuthorizationAccessException; + + /** + * Returns the UserGroupProvider for this managed Authorizer. Must be non null + * + * @return the UserGroupProvider + */ + UserGroupProvider getUserGroupProvider(); + + /** + * Called immediately after instance creation for implementers to perform additional setup + * + * @param initializationContext in which to initialize + */ + void initialize(AccessPolicyProviderInitializationContext initializationContext) throws SecurityProviderCreationException; + + /** + * Called to configure the Authorizer. + * + * @param configurationContext at the time of configuration + * @throws SecurityProviderCreationException for any issues configuring the provider + */ + void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException; + + /** + * Called immediately before instance destruction for implementers to release resources. + * + * @throws SecurityProviderDestructionException If pre-destruction fails. + */ + void preDestruction() throws SecurityProviderDestructionException; +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AccessPolicyProviderInitializationContext.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AccessPolicyProviderInitializationContext.java new file mode 100644 index 0000000000..92792e9a18 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AccessPolicyProviderInitializationContext.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +/** + * Initialization content for AccessPolicyProviders. + */ +public interface AccessPolicyProviderInitializationContext extends UserGroupProviderInitializationContext { + + /** + * The lookup for accessing other configured AccessPolicyProviders. + * + * @return The AccessPolicyProvider lookup + */ + AccessPolicyProviderLookup getAccessPolicyProviderLookup(); +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AccessPolicyProviderLookup.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AccessPolicyProviderLookup.java new file mode 100644 index 0000000000..679072ae56 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AccessPolicyProviderLookup.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +/** + * + */ +public interface AccessPolicyProviderLookup { + + /** + * Looks up the AccessPolicyProvider with the specified identifier + * + * @param identifier The identifier of the AccessPolicyProvider + * @return The AccessPolicyProvider + */ + AccessPolicyProvider getAccessPolicyProvider(String identifier); +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizationAuditor.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizationAuditor.java new file mode 100644 index 0000000000..ae01ecec30 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizationAuditor.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +public interface AuthorizationAuditor { + + /** + * Audits an authorization request. Will be invoked for any Approved or Denied results. ResourceNotFound + * will either re-attempt authorization using a parent resource or will generate a failure result and + * audit that. + * + * @param request the request for authorization + * @param result the authorization result + */ + void auditAccessAttempt(final AuthorizationRequest request, final AuthorizationResult result); +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizationRequest.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizationRequest.java new file mode 100644 index 0000000000..3e832fabde --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizationRequest.java @@ -0,0 +1,268 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Supplier; + +/** + * Represents an authorization request for a given user/entity performing an action against a resource within some userContext. + */ +public class AuthorizationRequest { + + public static final String DEFAULT_EXPLANATION = "Unable to perform the desired action."; + + private final Resource resource; + private final Resource requestedResource; + private final String identity; + private final List proxyIdentities; + private final Set groups; + private final RequestAction action; + private final boolean isAccessAttempt; + private final boolean isAnonymous; + private final Map userContext; + private final Map resourceContext; + private final Supplier explanationSupplier; + + private AuthorizationRequest(final Builder builder) { + Objects.requireNonNull(builder.resource, "The resource is required when creating an authorization request"); + Objects.requireNonNull(builder.action, "The action is required when creating an authorization request"); + Objects.requireNonNull(builder.isAccessAttempt, "Whether this request is an access attempt is request"); + Objects.requireNonNull(builder.isAnonymous, "Whether this request is being performed by an anonymous user is required"); + + this.resource = builder.resource; + this.identity = builder.identity; + this.proxyIdentities = builder.proxyIdentities == null ? Collections.emptyList() : Collections.unmodifiableList(builder.proxyIdentities); + this.groups = builder.groups == null ? null : Collections.unmodifiableSet(builder.groups); + this.action = builder.action; + this.isAccessAttempt = builder.isAccessAttempt; + this.isAnonymous = builder.isAnonymous; + this.userContext = builder.userContext == null ? null : Collections.unmodifiableMap(builder.userContext); + this.resourceContext = builder.resourceContext == null ? null : Collections.unmodifiableMap(builder.resourceContext); + this.explanationSupplier = () -> { + final String explanation = builder.explanationSupplier.get(); + + // ensure the specified supplier returns non null + if (explanation == null) { + return DEFAULT_EXPLANATION; + } else { + return explanation; + } + }; + + if (builder.requestedResource == null) { + this.requestedResource = builder.resource; + } else { + this.requestedResource = builder.requestedResource; + } + } + + /** + * The Resource being authorized. Not null. + * + * @return The resource + */ + public Resource getResource() { + return resource; + } + + /** + * The original Resource being requested. In cases with inherited policies, this will be a ancestor resource of + * of the current resource. The initial request, and cases without inheritance, the requested resource will be + * the same as the current resource. + * + * @return The requested resource + */ + public Resource getRequestedResource() { + return requestedResource; + } + + /** + * The identity accessing the Resource. May be null if the user could not authenticate. + * + * @return The identity + */ + public String getIdentity() { + return identity; + } + + /** + * The identities in the proxy chain for the request. Will be empty if the request was not proxied. + * + * @return The identities in the proxy chain + * + * @deprecated no longer populated + */ + public List getProxyIdentities() { + return proxyIdentities; + } + + /** + * The groups the user making this request belongs to. May be null if this NiFi is not configured to load user + * groups or empty if the user has no groups + * + * @return The groups + */ + public Set getGroups() { + return groups; + } + + /** + * Whether this is a direct access attempt of the Resource if if it's being checked as part of another response. + * + * @return if this is a direct access attempt + */ + public boolean isAccessAttempt() { + return isAccessAttempt; + } + + /** + * Whether the entity accessing is anonymous. + * + * @return whether the entity is anonymous + */ + public boolean isAnonymous() { + return isAnonymous; + } + + /** + * The action being taken against the Resource. Not null. + * + * @return The action + */ + public RequestAction getAction() { + return action; + } + + /** + * The userContext of the user request to make additional access decisions. May be null. + * + * @return The userContext of the user request + */ + public Map getUserContext() { + return userContext; + } + + /** + * The event attributes to make additional access decisions for provenance events. May be null. + * + * @return The event attributes + */ + public Map getResourceContext() { + return resourceContext; + } + + /** + * A supplier for the explanation if access is denied. Non null. + * + * @return The explanation supplier if access is denied + */ + public Supplier getExplanationSupplier() { + return explanationSupplier; + } + + /** + * AuthorizationRequest builder. + */ + public static final class Builder { + + private Resource resource; + private Resource requestedResource; + private String identity; + private List proxyIdentities; + private Set groups; + private Boolean isAnonymous; + private Boolean isAccessAttempt; + private RequestAction action; + private Map userContext; + private Map resourceContext; + private Supplier explanationSupplier = () -> DEFAULT_EXPLANATION; + + public Builder resource(final Resource resource) { + this.resource = resource; + return this; + } + + public Builder requestedResource(final Resource requestedResource) { + this.requestedResource = requestedResource; + return this; + } + + public Builder identity(final String identity) { + this.identity = identity; + return this; + } + + /** + * @deprecated no longer populated by the framework + */ + public Builder proxyIdentities(final List proxyIdentities) { + this.proxyIdentities = proxyIdentities; + return this; + } + + public Builder groups(final Set groups) { + this.groups = groups; + return this; + } + + public Builder anonymous(final Boolean isAnonymous) { + this.isAnonymous = isAnonymous; + return this; + } + + public Builder accessAttempt(final Boolean isAccessAttempt) { + this.isAccessAttempt = isAccessAttempt; + return this; + } + + public Builder action(final RequestAction action) { + this.action = action; + return this; + } + + public Builder userContext(final Map userContext) { + if (userContext != null) { + this.userContext = new HashMap<>(userContext); + } + return this; + } + + public Builder resourceContext(final Map resourceContext) { + if (resourceContext != null) { + this.resourceContext = new HashMap<>(resourceContext); + } + return this; + } + + public Builder explanationSupplier(final Supplier explanationSupplier) { + if (explanationSupplier != null) { + this.explanationSupplier = explanationSupplier; + } + return this; + } + + public AuthorizationRequest build() { + return new AuthorizationRequest(this); + } + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizationResult.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizationResult.java new file mode 100644 index 0000000000..5f9b55e869 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizationResult.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +/** + * Represents a decision whether authorization is granted. + */ +public class AuthorizationResult { + + public enum Result { + Approved, + Denied, + ResourceNotFound + } + + private static final AuthorizationResult APPROVED = new AuthorizationResult(Result.Approved, null); + private static final AuthorizationResult RESOURCE_NOT_FOUND = new AuthorizationResult(Result.ResourceNotFound, "Not authorized for the requested resource."); + + private final Result result; + private final String explanation; + + /** + * Creates a new AuthorizationResult with the specified result and explanation. + * + * @param result of the authorization + * @param explanation for the authorization attempt + */ + private AuthorizationResult(Result result, String explanation) { + if (Result.Denied.equals(result) && explanation == null) { + throw new IllegalArgumentException("An explanation is required when the authorization request is denied."); + } + + if (Result.ResourceNotFound.equals(result) && explanation == null) { + throw new IllegalArgumentException("An explanation is required when the authorization request is resource not found."); + } + + this.result = result; + this.explanation = explanation; + } + + /** + * @return Whether or not the request is approved + */ + public Result getResult() { + return result; + } + + /** + * @return If the request is denied, the reason why. Null otherwise + */ + public String getExplanation() { + return explanation; + } + + /** + * @return a new approved AuthorizationResult + */ + public static AuthorizationResult approved() { + return APPROVED; + } + + /** + * Resource not found will indicate that there are no specific authorization rules for this resource. + * @return a new resource not found AuthorizationResult + */ + public static AuthorizationResult resourceNotFound() { + return RESOURCE_NOT_FOUND; + } + + /** + * Creates a new denied AuthorizationResult with a message indicating 'Access is denied'. + * + * @return a new denied AuthorizationResult + */ + public static AuthorizationResult denied() { + return denied(AuthorizationRequest.DEFAULT_EXPLANATION); + } + + /** + * Creates a new denied AuthorizationResult with the specified explanation. + * + * @param explanation for why it was denied + * @return a new denied AuthorizationResult with the specified explanation + * @throws IllegalArgumentException if explanation is null + */ + public static AuthorizationResult denied(String explanation) { + return new AuthorizationResult(Result.Denied, explanation); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/Authorizer.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/Authorizer.java new file mode 100644 index 0000000000..c8fc0f67c5 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/Authorizer.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException; + +/** + * Authorizes user requests. + */ +public interface Authorizer { + + /** + * Determines if the specified user/entity is authorized to access the specified resource within the given context. + * These details are all contained in the AuthorizationRequest. + * + * NOTE: This method will be called often and frequently. Because of this, if the underlying implementation needs to + * make remote calls or expensive calculations those should probably be done asynchronously and/or cache the results. + * + * @param request The authorization request + * @return the authorization result + * @throws AuthorizationAccessException if unable to access the policies + */ + AuthorizationResult authorize(AuthorizationRequest request) throws AuthorizationAccessException; + + /** + * Called immediately after instance creation for implementers to perform additional setup + * + * @param initializationContext in which to initialize + */ + void initialize(AuthorizerInitializationContext initializationContext) throws SecurityProviderCreationException; + + /** + * Called to configure the Authorizer. + * + * @param configurationContext at the time of configuration + * @throws SecurityProviderCreationException for any issues configuring the provider + */ + void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException; + + /** + * Called immediately before instance destruction for implementers to release resources. + * + * @throws SecurityProviderDestructionException If pre-destruction fails. + */ + void preDestruction() throws SecurityProviderDestructionException; + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerConfigurationContext.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerConfigurationContext.java new file mode 100644 index 0000000000..5e33f0c60e --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerConfigurationContext.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +import org.apache.nifi.registry.util.PropertyValue; + +import java.util.Map; + +/** + * + */ +public interface AuthorizerConfigurationContext { + + /** + * @return identifier for the authorizer + */ + String getIdentifier(); + + /** + * Retrieves all properties the component currently understands regardless + * of whether a value has been set for them or not. If no value is present + * then its value is null and thus any registered default for the property + * descriptor applies. + * + * @return Map of all properties + */ + Map getProperties(); + + /** + * @param property to lookup the descriptor and value of + * @return the value the component currently understands for the given PropertyDescriptor + */ + PropertyValue getProperty(String property); +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerInitializationContext.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerInitializationContext.java new file mode 100644 index 0000000000..55854b3ec1 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerInitializationContext.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +/** + * Initialization content for Authorizers. + */ +public interface AuthorizerInitializationContext extends AccessPolicyProviderInitializationContext { + + /** + * The lookup for accessing other configured Authorizers. + * + * @return The Authorizer lookup + */ + AuthorizerLookup getAuthorizerLookup(); +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerLookup.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerLookup.java new file mode 100644 index 0000000000..2c429d9df1 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerLookup.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +/** + * + */ +public interface AuthorizerLookup { + + /** + * Looks up the Authorizer with the specified identifier + * + * @param identifier The identifier of the Authorizer + * @return The Authorizer + */ + Authorizer getAuthorizer(String identifier); +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/ConfigurableAccessPolicyProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/ConfigurableAccessPolicyProvider.java new file mode 100644 index 0000000000..37de9fe575 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/ConfigurableAccessPolicyProvider.java @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException; +import org.apache.nifi.registry.security.authorization.exception.UninheritableAuthorizationsException; + +/** + * Provides support for configuring AccessPolicies. + * + * NOTE: Extensions will be called often and frequently. Because of this, if the underlying implementation needs to + * make remote calls or expensive calculations those should probably be done asynchronously and/or cache the results. + * + * Additionally, extensions need to be thread safe. + */ +public interface ConfigurableAccessPolicyProvider extends AccessPolicyProvider { + + /** + * Returns a fingerprint representing the authorizations managed by this authorizer. The fingerprint will be + * used for comparison to determine if two policy-based authorizers represent a compatible set of policies. + * + * @return the fingerprint for this Authorizer + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + String getFingerprint() throws AuthorizationAccessException; + + /** + * Parses the fingerprint and adds any policies to the current AccessPolicyProvider. + * + * @param fingerprint the fingerprint that was obtained from calling getFingerprint() on another Authorizer. + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + void inheritFingerprint(final String fingerprint) throws AuthorizationAccessException; + + /** + * When the fingerprints are not equal, this method will check if the proposed fingerprint is inheritable. + * If the fingerprint is an exact match, this method will not be invoked as there is nothing to inherit. + * + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + * @throws UninheritableAuthorizationsException if the proposed fingerprint was uninheritable + */ + void checkInheritability(final String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException; + + /** + * Adds the given policy ensuring that multiple policies can not be added for the same resource and action. + * + * @param accessPolicy the policy to add + * @return the policy that was added + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + AccessPolicy addAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException; + + /** + * Determines whether the specified access policy is configurable. Provides the opportunity for a ConfigurableAccessPolicyProvider to prevent + * editing of a specific access policy. By default, all known access policies are configurable. + * + * @param accessPolicy the access policy + * @return is configurable + */ + default boolean isConfigurable(AccessPolicy accessPolicy) { + if (accessPolicy == null) { + throw new IllegalArgumentException("Access policy cannot be null"); + } + + return getAccessPolicy(accessPolicy.getIdentifier()) != null; + } + + /** + * The policy represented by the provided instance will be updated based on the provided instance. + * + * @param accessPolicy an updated policy + * @return the updated policy, or null if no matching policy was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + AccessPolicy updateAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException; + + /** + * Deletes the given policy. + * + * @param accessPolicy the policy to delete + * @return the deleted policy, or null if no matching policy was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + AccessPolicy deleteAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException; + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/ConfigurableUserGroupProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/ConfigurableUserGroupProvider.java new file mode 100644 index 0000000000..8d63387e38 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/ConfigurableUserGroupProvider.java @@ -0,0 +1,146 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException; +import org.apache.nifi.registry.security.authorization.exception.UninheritableAuthorizationsException; + +/** + * Provides support for configuring Users and Groups. + * + * NOTE: Extensions will be called often and frequently. Because of this, if the underlying implementation needs to + * make remote calls or expensive calculations those should probably be done asynchronously and/or cache the results. + * + * Additionally, extensions need to be thread safe. + */ +public interface ConfigurableUserGroupProvider extends UserGroupProvider { + + /** + * Returns a fingerprint representing the authorizations managed by this authorizer. The fingerprint will be + * used for comparison to determine if two policy-based authorizers represent a compatible set of users and/or groups. + * + * @return the fingerprint for this Authorizer + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + String getFingerprint() throws AuthorizationAccessException; + + /** + * Parses the fingerprint and adds any users and groups to the current Authorizer. + * + * @param fingerprint the fingerprint that was obtained from calling getFingerprint() on another Authorizer. + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + void inheritFingerprint(final String fingerprint) throws AuthorizationAccessException; + + /** + * When the fingerprints are not equal, this method will check if the proposed fingerprint is inheritable. + * If the fingerprint is an exact match, this method will not be invoked as there is nothing to inherit. + * + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + * @throws UninheritableAuthorizationsException if the proposed fingerprint was uninheritable + */ + void checkInheritability(final String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException; + + /** + * Adds the given user. + * + * @param user the user to add + * @return the user that was added + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + * @throws IllegalStateException if there is already a user with the same identity + */ + User addUser(User user) throws AuthorizationAccessException; + + /** + * Determines whether the specified user is configurable. Provides the opportunity for a ConfigurableUserGroupProvider to prevent + * editing of a specific user. By default, all known users are configurable. + * + * @param user the user + * @return is configurable + */ + default boolean isConfigurable(User user) { + if (user == null) { + throw new IllegalArgumentException("User cannot be null"); + } + + return getUser(user.getIdentifier()) != null; + } + + /** + * The user represented by the provided instance will be updated based on the provided instance. + * + * @param user an updated user instance + * @return the updated user instance, or null if no matching user was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + * @throws IllegalStateException if there is already a user with the same identity + */ + User updateUser(final User user) throws AuthorizationAccessException; + + /** + * Deletes the given user. + * + * @param user the user to delete + * @return the user that was deleted, or null if no matching user was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + User deleteUser(User user) throws AuthorizationAccessException; + + /** + * Adds a new group. + * + * @param group the Group to add + * @return the added Group + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + * @throws IllegalStateException if a group with the same name already exists + */ + Group addGroup(Group group) throws AuthorizationAccessException; + + /** + * Determines whether the specified group is configurable. Provides the opportunity for a ConfigurableUserGroupProvider to prevent + * editing of a specific group. By default, all known groups are configurable. + * + * @param group the group + * @return is configurable + */ + default boolean isConfigurable(Group group) { + if (group == null) { + throw new IllegalArgumentException("Group cannot be null"); + } + + return getGroup(group.getIdentifier()) != null; + } + + /** + * The group represented by the provided instance will be updated based on the provided instance. + * + * @param group an updated group instance + * @return the updated group instance, or null if no matching group was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + * @throws IllegalStateException if there is already a group with the same name + */ + Group updateGroup(Group group) throws AuthorizationAccessException; + + /** + * Deletes the given group. + * + * @param group the group to delete + * @return the deleted group, or null if no matching group was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + Group deleteGroup(Group group) throws AuthorizationAccessException; + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/Group.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/Group.java new file mode 100644 index 0000000000..29006a7152 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/Group.java @@ -0,0 +1,263 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; + +/** + * A group that users can belong to. + */ +public class Group { + + private final String identifier; + + private final String name; + + private final Set users; + + private Group(final Builder builder) { + this.identifier = builder.identifier; + this.name = builder.name; + this.users = Collections.unmodifiableSet(new HashSet<>(builder.users)); + + if (this.identifier == null || this.identifier.trim().isEmpty()) { + throw new IllegalArgumentException("Identifier can not be null or empty"); + } + + if (this.name == null || this.name.trim().isEmpty()) { + throw new IllegalArgumentException("Name can not be null or empty"); + } + } + + /** + * @return the identifier of the group + */ + public String getIdentifier() { + return identifier; + } + + /** + * @return the name of the group + */ + public String getName() { + return name; + } + + /** + * @return an unmodifiable set of user identifiers that belong to this group + */ + public Set getUsers() { + return users; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + + final Group other = (Group) obj; + return Objects.equals(this.identifier, other.identifier); + } + + @Override + public int hashCode() { + return Objects.hashCode(this.identifier); + } + + @Override + public String toString() { + return String.format("identifier[%s], name[%s], users[%s]", getIdentifier(), getName(), String.join(", ", users)); + } + + + /** + * Builder for creating Groups. + */ + public static class Builder { + + private String identifier; + private String name; + private Set users = new HashSet<>(); + private final boolean fromGroup; + + public Builder() { + this.fromGroup = false; + } + + /** + * Initializes the builder with the state of the provided group. When using this constructor + * the identifier field of the builder can not be changed and will result in an IllegalStateException + * if attempting to do so. + * + * @param other the existing access policy to initialize from + */ + public Builder(final Group other) { + if (other == null) { + throw new IllegalArgumentException("Provided group can not be null"); + } + + this.identifier = other.getIdentifier(); + this.name = other.getName(); + this.users.clear(); + this.users.addAll(other.getUsers()); + this.fromGroup = true; + } + + /** + * Sets the identifier of the builder. + * + * @param identifier the identifier + * @return the builder + * @throws IllegalStateException if this method is called when this builder was constructed from an existing Group + */ + public Builder identifier(final String identifier) { + if (fromGroup) { + throw new IllegalStateException( + "Identifier can not be changed when initialized from an existing group"); + } + + this.identifier = identifier; + return this; + } + + /** + * Sets the identifier of the builder to a random UUID. + * + * @return the builder + * @throws IllegalStateException if this method is called when this builder was constructed from an existing Group + */ + public Builder identifierGenerateRandom() { + if (fromGroup) { + throw new IllegalStateException( + "Identifier can not be changed when initialized from an existing group"); + } + + this.identifier = UUID.randomUUID().toString(); + return this; + } + + /** + * Sets the identifier of the builder with a UUID generated from the specified seed string. + * + * @return the builder + * @throws IllegalStateException if this method is called when this builder was constructed from an existing Group + */ + public Builder identifierGenerateFromSeed(final String seed) { + if (fromGroup) { + throw new IllegalStateException( + "Identifier can not be changed when initialized from an existing group"); + } + if (seed == null) { + throw new IllegalArgumentException("Cannot seed the group identifier with a null value."); + } + + this.identifier = UUID.nameUUIDFromBytes(seed.getBytes(StandardCharsets.UTF_8)).toString(); + return this; + } + + /** + * Sets the name of the builder. + * + * @param name the name + * @return the builder + */ + public Builder name(final String name) { + this.name = name; + return this; + } + + /** + * Adds all users from the provided set to the builder's set of users. + * + * @param users a set of users to add + * @return the builder + */ + public Builder addUsers(final Set users) { + if (users != null) { + this.users.addAll(users); + } + return this; + } + + /** + * Adds the given user to the builder's set of users. + * + * @param user the user to add + * @return the builder + */ + public Builder addUser(final String user) { + if (user != null) { + this.users.add(user); + } + return this; + } + + /** + * Removes the given user from the builder's set of users. + * + * @param user the user to remove + * @return the builder + */ + public Builder removeUser(final String user) { + if (user != null) { + this.users.remove(user); + } + return this; + } + + /** + * Removes all users from the provided set from the builder's set of users. + * + * @param users the users to remove + * @return the builder + */ + public Builder removeUsers(final Set users) { + if (users != null) { + this.users.removeAll(users); + } + return this; + } + + /** + * Clears the builder's set of users so that users is non-null with size 0. + * + * @return the builder + */ + public Builder clearUsers() { + this.users.clear(); + return this; + } + + /** + * @return a new Group constructed from the state of the builder + */ + public Group build() { + return new Group(this); + } + + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/ManagedAuthorizer.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/ManagedAuthorizer.java new file mode 100644 index 0000000000..50b809400a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/ManagedAuthorizer.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException; +import org.apache.nifi.registry.security.authorization.exception.UninheritableAuthorizationsException; + +public interface ManagedAuthorizer extends Authorizer { + + /** + * Returns a fingerprint representing the authorizations managed by this authorizer. The fingerprint will be + * used for comparison to determine if two managed authorizers represent a compatible set of users, + * groups, and/or policies. Must be non null + * + * @return the fingerprint for this Authorizer + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + String getFingerprint() throws AuthorizationAccessException; + + /** + * Parses the fingerprint and adds any users, groups, and policies to the current Authorizer. + * + * @param fingerprint the fingerprint that was obtained from calling getFingerprint() on another Authorizer. + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + void inheritFingerprint(final String fingerprint) throws AuthorizationAccessException; + + /** + * When the fingerprints are not equal, this method will check if the proposed fingerprint is inheritable. + * If the fingerprint is an exact match, this method will not be invoked as there is nothing to inherit. + * + * @param proposedFingerprint the proposed fingerprint + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + * @throws UninheritableAuthorizationsException if the proposed fingerprint was uninheritable + */ + void checkInheritability(final String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException; + + /** + * Returns the AccessPolicy provider for this managed Authorizer. Must be non null + * + * @return the AccessPolicy provider + */ + AccessPolicyProvider getAccessPolicyProvider(); + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/RequestAction.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/RequestAction.java new file mode 100644 index 0000000000..def3de4813 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/RequestAction.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +import java.util.StringJoiner; + +/** + * Actions a user/entity can take on a resource. + */ +public enum RequestAction { + READ("read"), + WRITE("write"), + DELETE("delete"); + + private String value; + + RequestAction(String value) { + this.value = value; + } + + @Override + public String toString() { + return value.toLowerCase(); + } + + public static RequestAction valueOfValue(final String action) { + if (RequestAction.READ.toString().equalsIgnoreCase(action)) { + return RequestAction.READ; + } else if (RequestAction.WRITE.toString().equalsIgnoreCase(action)) { + return RequestAction.WRITE; + } else if (RequestAction.DELETE.toString().equalsIgnoreCase(action)) { + return RequestAction.DELETE; + } else { + StringJoiner stringJoiner = new StringJoiner(", "); + for(RequestAction ra : RequestAction.values()) { + stringJoiner.add(ra.toString()); + } + String allowableValues = stringJoiner.toString(); + throw new IllegalArgumentException("Action '" + action + "' is invalid. Must be one of [" + allowableValues + "]"); + } + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/Resource.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/Resource.java new file mode 100644 index 0000000000..eacdffe244 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/Resource.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +/** + * Resource in an authorization request. + */ +public interface Resource { + + /** + * The identifier for this resource. + * + * @return identifier for this resource + */ + String getIdentifier(); + + /** + * The name of this resource. May be null. + * + * @return name of this resource + */ + String getName(); + + /** + * The description of this resource that may be safely used in messages to the client. + * + * @return safe description + */ + String getSafeDescription(); +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/User.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/User.java new file mode 100644 index 0000000000..e118c2b62e --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/User.java @@ -0,0 +1,187 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.UUID; + +/** + * A user to create authorization policies for. + */ +public class User { + + private final String identifier; + + private final String identity; + + private User(final Builder builder) { + this.identifier = builder.identifier; + this.identity = builder.identity; + + if (identifier == null || identifier.trim().isEmpty()) { + throw new IllegalArgumentException("Identifier can not be null or empty"); + } + + if (identity == null || identity.trim().isEmpty()) { + throw new IllegalArgumentException("Identity can not be null or empty"); + } + } + + /** + * @return the identifier of the user + */ + public String getIdentifier() { + return identifier; + } + + /** + * @return the identity string of the user + */ + public String getIdentity() { + return identity; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + + final User other = (User) obj; + return Objects.equals(this.identifier, other.identifier); + } + + @Override + public int hashCode() { + return Objects.hashCode(this.identifier); + } + + @Override + public String toString() { + return String.format("identifier[%s], identity[%s]", getIdentifier(), getIdentity()); + } + + /** + * Builder for Users. + */ + public static class Builder { + + private String identifier; + private String identity; + private final boolean fromUser; + + /** + * Default constructor for building a new User. + */ + public Builder() { + this.fromUser = false; + } + + /** + * Initializes the builder with the state of the provided user. When using this constructor + * the identifier field of the builder can not be changed and will result in an IllegalStateException + * if attempting to do so. + * + * @param other the existing user to initialize from + */ + public Builder(final User other) { + if (other == null) { + throw new IllegalArgumentException("Provided user can not be null"); + } + + this.identifier = other.getIdentifier(); + this.identity = other.getIdentity(); + this.fromUser = true; + } + + /** + * Sets the identifier of the builder. + * + * @param identifier the identifier to set + * @return the builder + * @throws IllegalStateException if this method is called when this builder was constructed from an existing User + */ + public Builder identifier(final String identifier) { + if (fromUser) { + throw new IllegalStateException( + "Identifier can not be changed when initialized from an existing user"); + } + + this.identifier = identifier; + return this; + } + + /** + * Sets the identifier of the builder to a random UUID. + * + * @return the builder + * @throws IllegalStateException if this method is called when this builder was constructed from an existing User + */ + public Builder identifierGenerateRandom() { + if (fromUser) { + throw new IllegalStateException( + "Identifier can not be changed when initialized from an existing user"); + } + + this.identifier = UUID.randomUUID().toString(); + return this; + } + + /** + * Sets the identifier of the builder with a UUID generated from the specified seed string. + * + * @return the builder + * @throws IllegalStateException if this method is called when this builder was constructed from an existing User + */ + public Builder identifierGenerateFromSeed(final String seed) { + if (fromUser) { + throw new IllegalStateException( + "Identifier can not be changed when initialized from an existing user"); + } + if (seed == null) { + throw new IllegalArgumentException("Cannot seed the user identifier with a null value."); + } + + this.identifier = UUID.nameUUIDFromBytes(seed.getBytes(StandardCharsets.UTF_8)).toString(); + return this; + } + + /** + * Sets the identity of the builder. + * + * @param identity the identity to set + * @return the builder + */ + public Builder identity(final String identity) { + this.identity = identity; + return this; + } + + /** + * @return a new User constructed from the state of the builder + */ + public User build() { + return new User(this); + } + + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserAndGroups.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserAndGroups.java new file mode 100644 index 0000000000..6776592af7 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserAndGroups.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +import java.util.Set; + +/** + * A holder object to provide atomic access to a user and their groups. + */ +public interface UserAndGroups { + + /** + * A static, immutable, empty implementation. + */ + UserAndGroups EMPTY = new UserAndGroups() { + @Override + public User getUser() { + return null; + } + + @Override + public Set getGroups() { + return null; + } + }; + + /** + * Retrieves the user, or null if the user is unknown + * + * @return the user with the given identity + */ + User getUser(); + + /** + * Retrieves the groups for the user, or null if the user is unknown or has no groups. + * + * @return the set of groups for the given user identity + */ + Set getGroups(); + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserContextKeys.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserContextKeys.java new file mode 100644 index 0000000000..8db6cfccae --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserContextKeys.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +/** + * Constants for keys that can be passed in the AuthorizationRequest user context Map. + */ +public enum UserContextKeys { + + CLIENT_ADDRESS; + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserGroupProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserGroupProvider.java new file mode 100644 index 0000000000..5505e7deef --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserGroupProvider.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException; + +import java.util.Set; + +/** + * Provides access to Users and Groups. + * + * NOTE: Extensions will be called often and frequently. Because of this, if the underlying implementation needs to + * make remote calls or expensive calculations those should probably be done asynchronously and/or cache the results. + * + * Additionally, extensions need to be thread safe. + */ +public interface UserGroupProvider { + + /** + * Retrieves all users. Must be non null + * + * @return a list of users + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + Set getUsers() throws AuthorizationAccessException; + + /** + * Retrieves the user with the given identifier. + * + * @param identifier the id of the user to retrieve + * @return the user with the given id, or null if no matching user was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + User getUser(String identifier) throws AuthorizationAccessException; + + /** + * Retrieves the user with the given identity. + * + * @param identity the identity of the user to retrieve + * @return the user with the given identity, or null if no matching user was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + User getUserByIdentity(String identity) throws AuthorizationAccessException; + + /** + * Retrieves all groups. Must be non null + * + * @return a list of groups + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + Set getGroups() throws AuthorizationAccessException; + + /** + * Retrieves a Group by id. + * + * @param identifier the identifier of the Group to retrieve + * @return the Group with the given identifier, or null if no matching group was found + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + Group getGroup(String identifier) throws AuthorizationAccessException; + + /** + * Gets a user and their groups. Must be non null. If the user is not known the UserAndGroups.getUser() and + * UserAndGroups.getGroups() should return null + * + * @return the UserAndGroups for the specified identity + * @throws AuthorizationAccessException if there was an unexpected error performing the operation + */ + UserAndGroups getUserAndGroups(String identity) throws AuthorizationAccessException; + + /** + * Called immediately after instance creation for implementers to perform additional setup + * + * @param initializationContext in which to initialize + */ + void initialize(UserGroupProviderInitializationContext initializationContext) throws SecurityProviderCreationException; + + /** + * Called to configure the Authorizer. + * + * @param configurationContext at the time of configuration + * @throws SecurityProviderCreationException for any issues configuring the provider + */ + void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException; + + /** + * Called immediately before instance destruction for implementers to release resources. + * + * @throws SecurityProviderDestructionException If pre-destruction fails. + */ + void preDestruction() throws SecurityProviderDestructionException; +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserGroupProviderInitializationContext.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserGroupProviderInitializationContext.java new file mode 100644 index 0000000000..d2c471e868 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserGroupProviderInitializationContext.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +/** + * Initialization content for UserGroupProviders. + */ +public interface UserGroupProviderInitializationContext { + + /** + * The identifier of the UserGroupProvider. + * + * @return The identifier + */ + String getIdentifier(); + + /** + * The lookup for accessing other configured UserGroupProviders. + * + * @return The UserGroupProvider lookup + */ + UserGroupProviderLookup getUserGroupProviderLookup(); +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserGroupProviderLookup.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserGroupProviderLookup.java new file mode 100644 index 0000000000..df5e01c7bf --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserGroupProviderLookup.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization; + +/** + * + */ +public interface UserGroupProviderLookup { + + /** + * Looks up the UserGroupProvider with the specified identifier + * + * @param identifier The identifier of the UserGroupProvider + * @return The UserGroupProvider + */ + UserGroupProvider getUserGroupProvider(String identifier); +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/annotation/AuthorizerContext.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/annotation/AuthorizerContext.java new file mode 100644 index 0000000000..8d5136ebce --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/annotation/AuthorizerContext.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * + */ +@Documented +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface AuthorizerContext { +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/exception/AccessDeniedException.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/exception/AccessDeniedException.java new file mode 100644 index 0000000000..6ab629c303 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/exception/AccessDeniedException.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.exception; + +/** + * Represents any error that might occur while authorizing user requests. + */ +public class AccessDeniedException extends RuntimeException { + private static final long serialVersionUID = -5683444815269084134L; + + public AccessDeniedException(Throwable cause) { + super(cause); + } + + public AccessDeniedException(String message, Throwable cause) { + super(message, cause); + } + + public AccessDeniedException(String message) { + super(message); + } + + public AccessDeniedException() { + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/exception/AuthorizationAccessException.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/exception/AuthorizationAccessException.java new file mode 100644 index 0000000000..7f33430d84 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/exception/AuthorizationAccessException.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.exception; + +/** + * Represents the case when an authorization decision could not be made because the Authorizer was unable to access the underlying data store. + */ +public class AuthorizationAccessException extends RuntimeException { + + public AuthorizationAccessException(String message, Throwable cause) { + super(message, cause); + } + + public AuthorizationAccessException(String message) { + super(message); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/exception/UninheritableAuthorizationsException.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/exception/UninheritableAuthorizationsException.java new file mode 100644 index 0000000000..b3ef068cd3 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/exception/UninheritableAuthorizationsException.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization.exception; + +/** + * Represents the case when the proposed authorizations are not inheritable. + */ +public class UninheritableAuthorizationsException extends RuntimeException { + + public UninheritableAuthorizationsException(String message) { + super(message); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/exception/SecurityProviderCreationException.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/exception/SecurityProviderCreationException.java new file mode 100644 index 0000000000..01531d6c5d --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/exception/SecurityProviderCreationException.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.exception; + +/** + * Represents the exceptional case when a security api provider fails instantiation. + */ +public class SecurityProviderCreationException extends RuntimeException { + + public SecurityProviderCreationException() { + } + + public SecurityProviderCreationException(String msg) { + super(msg); + } + + public SecurityProviderCreationException(Throwable cause) { + super(cause); + } + + public SecurityProviderCreationException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/exception/SecurityProviderDestructionException.java b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/exception/SecurityProviderDestructionException.java new file mode 100644 index 0000000000..3370623635 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/exception/SecurityProviderDestructionException.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.exception; + +/** + * Represents the exceptional case when a security api provider fails destruction. + */ +public class SecurityProviderDestructionException extends RuntimeException { + + public SecurityProviderDestructionException() { + } + + public SecurityProviderDestructionException(String msg) { + super(msg); + } + + public SecurityProviderDestructionException(Throwable cause) { + super(cause); + } + + public SecurityProviderDestructionException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-utils/pom.xml b/nifi-registry/nifi-registry-core/nifi-registry-security-utils/pom.xml new file mode 100644 index 0000000000..aba0fbdc4e --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-utils/pom.xml @@ -0,0 +1,52 @@ + + + + 4.0.0 + + org.apache.nifi.registry + nifi-registry-core + 1.14.0-SNAPSHOT + + + nifi-registry-security-utils + jar + + + + + + org.bouncycastle + bcprov-jdk15on + ${bouncycastle.version} + + + org.bouncycastle + bcpkix-jdk15on + ${bouncycastle.version} + + + org.apache.commons + commons-lang3 + + + org.spockframework + spock-core + test + + + org.codehaus.groovy + groovy-test + test + + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/CertificateUtils.java b/nifi-registry/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/CertificateUtils.java new file mode 100644 index 0000000000..d766b577b8 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/CertificateUtils.java @@ -0,0 +1,671 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.util; + +import org.apache.commons.lang3.StringUtils; +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1Set; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.pkcs.Attribute; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x500.AttributeTypeAndValue; +import org.bouncycastle.asn1.x500.RDN; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.ExtendedKeyUsage; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.Extensions; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.CertIOException; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSocket; +import java.io.ByteArrayInputStream; +import java.math.BigInteger; +import java.net.Socket; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Security; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class CertificateUtils { + private static final Logger logger = LoggerFactory.getLogger(CertificateUtils.class); + private static final String PEER_NOT_AUTHENTICATED_MSG = "peer not authenticated"; + private static final Map dnOrderMap = createDnOrderMap(); + + public static final String JAVA_8_MAX_SUPPORTED_TLS_PROTOCOL_VERSION = "TLSv1.2"; + public static final String JAVA_11_MAX_SUPPORTED_TLS_PROTOCOL_VERSION = "TLSv1.3"; + public static final String[] JAVA_8_SUPPORTED_TLS_PROTOCOL_VERSIONS = new String[]{JAVA_8_MAX_SUPPORTED_TLS_PROTOCOL_VERSION}; + public static final String[] JAVA_11_SUPPORTED_TLS_PROTOCOL_VERSIONS = new String[]{JAVA_11_MAX_SUPPORTED_TLS_PROTOCOL_VERSION, JAVA_8_MAX_SUPPORTED_TLS_PROTOCOL_VERSION}; + + static { + Security.addProvider(new BouncyCastleProvider()); + } + + /** + * The time in milliseconds that the last unique serial number was generated + */ + private static long lastSerialNumberMillis = 0L; + + /** + * An incrementor to add uniqueness to serial numbers generated in the same millisecond + */ + private static int serialNumberIncrementor = 0; + + /** + * BigInteger value to use for the base of the unique serial number + */ + private static BigInteger millisecondBigInteger; + + private static Map createDnOrderMap() { + Map orderMap = new HashMap<>(); + int count = 0; + orderMap.put(BCStyle.CN, count++); + orderMap.put(BCStyle.L, count++); + orderMap.put(BCStyle.ST, count++); + orderMap.put(BCStyle.O, count++); + orderMap.put(BCStyle.OU, count++); + orderMap.put(BCStyle.C, count++); + orderMap.put(BCStyle.STREET, count++); + orderMap.put(BCStyle.DC, count++); + orderMap.put(BCStyle.UID, count++); + return Collections.unmodifiableMap(orderMap); + } + + /** + * Extracts the username from the specified DN. If the username cannot be extracted because the CN is in an unrecognized format, the entire CN is returned. If the CN cannot be extracted because + * the DN is in an unrecognized format, the entire DN is returned. + * + * @param dn the dn to extract the username from + * @return the exatracted username + */ + public static String extractUsername(String dn) { + String username = dn; + + // ensure the dn is specified + if (StringUtils.isNotBlank(dn)) { + // determine the separate + final String separator = StringUtils.indexOfIgnoreCase(dn, "/cn=") > 0 ? "/" : ","; + + // attempt to locate the cd + final String cnPattern = "cn="; + final int cnIndex = StringUtils.indexOfIgnoreCase(dn, cnPattern); + if (cnIndex >= 0) { + int separatorIndex = StringUtils.indexOf(dn, separator, cnIndex); + if (separatorIndex > 0) { + username = StringUtils.substring(dn, cnIndex + cnPattern.length(), separatorIndex); + } else { + username = StringUtils.substring(dn, cnIndex + cnPattern.length()); + } + } + } + + return username; + } + + /** + * Returns a list of subject alternative names. Any name that is represented as a String by X509Certificate.getSubjectAlternativeNames() is converted to lowercase and returned. + * + * @param certificate a certificate + * @return a list of subject alternative names; list is never null + * @throws CertificateParsingException if parsing the certificate failed + */ + public static List getSubjectAlternativeNames(final X509Certificate certificate) throws CertificateParsingException { + + final Collection> altNames = certificate.getSubjectAlternativeNames(); + if (altNames == null) { + return new ArrayList<>(); + } + + final List result = new ArrayList<>(); + for (final List generalName : altNames) { + /** + * generalName has the name type as the first element a String or byte array for the second element. We return any general names that are String types. + * + * We don't inspect the numeric name type because some certificates incorrectly put IPs and DNS names under the wrong name types. + */ + final Object value = generalName.get(1); + if (value instanceof String) { + result.add(((String) value).toLowerCase()); + } + + } + + return result; + } + + /** + * Returns the DN extracted from the peer certificate (the server DN if run on the client; the client DN (if available) if run on the server). + *

+ * If the client auth setting is WANT or NONE and a client certificate is not present, this method will return {@code null}. + * If the client auth is NEED, it will throw a {@link CertificateException}. + * + * @param socket the SSL Socket + * @return the extracted DN + * @throws CertificateException if there is a problem parsing the certificate + */ + public static String extractPeerDNFromSSLSocket(Socket socket) throws CertificateException { + String dn = null; + if (socket instanceof SSLSocket) { + final SSLSocket sslSocket = (SSLSocket) socket; + + boolean clientMode = sslSocket.getUseClientMode(); + logger.debug("SSL Socket in {} mode", clientMode ? "client" : "server"); + SslContextFactory.ClientAuth clientAuth = getClientAuthStatus(sslSocket); + logger.debug("SSL Socket client auth status: {}", clientAuth); + + if (clientMode) { + logger.debug("This socket is in client mode, so attempting to extract certificate from remote 'server' socket"); + dn = extractPeerDNFromServerSSLSocket(sslSocket); + } else { + logger.debug("This socket is in server mode, so attempting to extract certificate from remote 'client' socket"); + dn = extractPeerDNFromClientSSLSocket(sslSocket); + } + } + + return dn; + } + + /** + * Returns the DN extracted from the client certificate. + *

+ * If the client auth setting is WANT or NONE and a certificate is not present (and {@code respectClientAuth} is {@code true}), this method will return {@code null}. + * If the client auth is NEED, it will throw a {@link CertificateException}. + * + * @param sslSocket the SSL Socket + * @return the extracted DN + * @throws CertificateException if there is a problem parsing the certificate + */ + private static String extractPeerDNFromClientSSLSocket(SSLSocket sslSocket) throws CertificateException { + String dn = null; + + /** The clientAuth value can be "need", "want", or "none" + * A client must send client certificates for need, should for want, and will not for none. + * This method should throw an exception if none are provided for need, return null if none are provided for want, and return null (without checking) for none. + */ + + SslContextFactory.ClientAuth clientAuth = getClientAuthStatus(sslSocket); + logger.debug("SSL Socket client auth status: {}", clientAuth); + + if (clientAuth != SslContextFactory.ClientAuth.NONE) { + try { + final Certificate[] certChains = sslSocket.getSession().getPeerCertificates(); + if (certChains != null && certChains.length > 0) { + X509Certificate x509Certificate = convertAbstractX509Certificate(certChains[0]); + dn = x509Certificate.getSubjectDN().getName().trim(); + logger.debug("Extracted DN={} from client certificate", dn); + } + } catch (SSLPeerUnverifiedException e) { + if (e.getMessage().equals(PEER_NOT_AUTHENTICATED_MSG)) { + logger.error("The incoming request did not contain client certificates and thus the DN cannot" + + " be extracted. Check that the other endpoint is providing a complete client certificate chain"); + } + if (clientAuth == SslContextFactory.ClientAuth.WANT) { + logger.warn("Suppressing missing client certificate exception because client auth is set to 'want'"); + return dn; + } + throw new CertificateException(e); + } + } + return dn; + } + + /** + * Returns the DN extracted from the server certificate. + * + * @param socket the SSL Socket + * @return the extracted DN + * @throws CertificateException if there is a problem parsing the certificate + */ + private static String extractPeerDNFromServerSSLSocket(Socket socket) throws CertificateException { + String dn = null; + if (socket instanceof SSLSocket) { + final SSLSocket sslSocket = (SSLSocket) socket; + try { + final Certificate[] certChains = sslSocket.getSession().getPeerCertificates(); + if (certChains != null && certChains.length > 0) { + X509Certificate x509Certificate = convertAbstractX509Certificate(certChains[0]); + dn = x509Certificate.getSubjectDN().getName().trim(); + logger.debug("Extracted DN={} from server certificate", dn); + } + } catch (SSLPeerUnverifiedException e) { + if (e.getMessage().equals(PEER_NOT_AUTHENTICATED_MSG)) { + logger.error("The server did not present a certificate and thus the DN cannot" + + " be extracted. Check that the other endpoint is providing a complete certificate chain"); + } + throw new CertificateException(e); + } + } + return dn; + } + + private static SslContextFactory.ClientAuth getClientAuthStatus(SSLSocket sslSocket) { + return sslSocket.getNeedClientAuth() ? SslContextFactory.ClientAuth.REQUIRED : sslSocket.getWantClientAuth() ? SslContextFactory.ClientAuth.WANT : SslContextFactory.ClientAuth.NONE; + } + + /** + * Accepts a legacy {@link javax.security.cert.X509Certificate} and returns an {@link X509Certificate}. The {@code javax.*} package certificate classes are for legacy compatibility and should + * not be used for new development. + * + * @param legacyCertificate the {@code javax.security.cert.X509Certificate} + * @return a new {@code java.security.cert.X509Certificate} + * @throws CertificateException if there is an error generating the new certificate + */ + @SuppressWarnings("deprecation") + public static X509Certificate convertLegacyX509Certificate(javax.security.cert.X509Certificate legacyCertificate) throws CertificateException { + if (legacyCertificate == null) { + throw new IllegalArgumentException("The X.509 certificate cannot be null"); + } + + try { + return formX509Certificate(legacyCertificate.getEncoded()); + } catch (javax.security.cert.CertificateEncodingException e) { + throw new CertificateException(e); + } + } + + /** + * Accepts an abstract {@link java.security.cert.Certificate} and returns an {@link X509Certificate}. Because {@code sslSocket.getSession().getPeerCertificates()} returns an array of the + * abstract certificates, they must be translated to X.509 to replace the functionality of {@code sslSocket.getSession().getPeerCertificateChain()}. + * + * @param abstractCertificate the {@code java.security.cert.Certificate} + * @return a new {@code java.security.cert.X509Certificate} + * @throws CertificateException if there is an error generating the new certificate + */ + public static X509Certificate convertAbstractX509Certificate(java.security.cert.Certificate abstractCertificate) throws CertificateException { + if (abstractCertificate == null || !(abstractCertificate instanceof X509Certificate)) { + throw new IllegalArgumentException("The certificate cannot be null and must be an X.509 certificate"); + } + + try { + return formX509Certificate(abstractCertificate.getEncoded()); + } catch (java.security.cert.CertificateEncodingException e) { + throw new CertificateException(e); + } + } + + private static X509Certificate formX509Certificate(byte[] encodedCertificate) throws CertificateException { + try { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + ByteArrayInputStream bais = new ByteArrayInputStream(encodedCertificate); + return (X509Certificate) cf.generateCertificate(bais); + } catch (CertificateException e) { + logger.error("Error converting the certificate", e); + throw e; + } + } + + /** + * Reorders DN to the order the elements appear in the RFC 2253 table + *

+ * https://www.ietf.org/rfc/rfc2253.txt + *

+ * String X.500 AttributeType + * ------------------------------ + * CN commonName + * L localityName + * ST stateOrProvinceName + * O organizationName + * OU organizationalUnitName + * C countryName + * STREET streetAddress + * DC domainComponent + * UID userid + * + * @param dn a possibly unordered DN + * @return the ordered dn + */ + public static String reorderDn(String dn) { + RDN[] rdNs = new X500Name(dn).getRDNs(); + Arrays.sort(rdNs, new Comparator() { + @Override + public int compare(RDN o1, RDN o2) { + AttributeTypeAndValue o1First = o1.getFirst(); + AttributeTypeAndValue o2First = o2.getFirst(); + + ASN1ObjectIdentifier o1Type = o1First.getType(); + ASN1ObjectIdentifier o2Type = o2First.getType(); + + Integer o1Rank = dnOrderMap.get(o1Type); + Integer o2Rank = dnOrderMap.get(o2Type); + if (o1Rank == null) { + if (o2Rank == null) { + int idComparison = o1Type.getId().compareTo(o2Type.getId()); + if (idComparison != 0) { + return idComparison; + } + return String.valueOf(o1Type).compareTo(String.valueOf(o2Type)); + } + return 1; + } else if (o2Rank == null) { + return -1; + } + return o1Rank - o2Rank; + } + }); + return new X500Name(rdNs).toString(); + } + + /** + * Reverses the X500Name in order make the certificate be in the right order + * [see http://stackoverflow.com/questions/7567837/attributes-reversed-in-certificate-subject-and-issuer/12645265] + * + * @param x500Name the X500Name created with the intended order + * @return the X500Name reversed + */ + private static X500Name reverseX500Name(X500Name x500Name) { + List rdns = Arrays.asList(x500Name.getRDNs()); + Collections.reverse(rdns); + return new X500Name(rdns.toArray(new RDN[rdns.size()])); + } + + /** + * Generates a unique serial number by using the current time in milliseconds left shifted 32 bits (to make room for incrementor) with an incrementor added + * + * @return a unique serial number (technically unique to this classloader) + */ + protected static synchronized BigInteger getUniqueSerialNumber() { + final long currentTimeMillis = System.currentTimeMillis(); + final int incrementorValue; + + if (lastSerialNumberMillis != currentTimeMillis) { + // We can only get into this block once per millisecond + millisecondBigInteger = BigInteger.valueOf(currentTimeMillis).shiftLeft(32); + lastSerialNumberMillis = currentTimeMillis; + incrementorValue = 0; + serialNumberIncrementor = 1; + } else { + // Already created at least one serial number this millisecond + incrementorValue = serialNumberIncrementor++; + } + + return millisecondBigInteger.add(BigInteger.valueOf(incrementorValue)); + } + + /** + * Generates a self-signed {@link X509Certificate} suitable for use as a Certificate Authority. + * + * @param keyPair the {@link KeyPair} to generate the {@link X509Certificate} for + * @param dn the distinguished name to user for the {@link X509Certificate} + * @param signingAlgorithm the signing algorithm to use for the {@link X509Certificate} + * @param certificateDurationDays the duration in days for which the {@link X509Certificate} should be valid + * @return a self-signed {@link X509Certificate} suitable for use as a Certificate Authority + * @throws CertificateException if there is an generating the new certificate + */ + public static X509Certificate generateSelfSignedX509Certificate(KeyPair keyPair, String dn, String signingAlgorithm, int certificateDurationDays) + throws CertificateException { + try { + ContentSigner sigGen = new JcaContentSignerBuilder(signingAlgorithm).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate()); + SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()); + Date startDate = new Date(); + Date endDate = new Date(startDate.getTime() + TimeUnit.DAYS.toMillis(certificateDurationDays)); + + X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder( + reverseX500Name(new X500Name(dn)), + getUniqueSerialNumber(), + startDate, endDate, + reverseX500Name(new X500Name(dn)), + subPubKeyInfo); + + // Set certificate extensions + // (1) digitalSignature extension + certBuilder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment | KeyUsage.dataEncipherment + | KeyUsage.keyAgreement | KeyUsage.nonRepudiation | KeyUsage.cRLSign | KeyUsage.keyCertSign)); + + certBuilder.addExtension(Extension.basicConstraints, false, new BasicConstraints(true)); + + certBuilder.addExtension(Extension.subjectKeyIdentifier, false, new JcaX509ExtensionUtils().createSubjectKeyIdentifier(keyPair.getPublic())); + + certBuilder.addExtension(Extension.authorityKeyIdentifier, false, new JcaX509ExtensionUtils().createAuthorityKeyIdentifier(keyPair.getPublic())); + + // (2) extendedKeyUsage extension + certBuilder.addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_clientAuth, KeyPurposeId.id_kp_serverAuth})); + + // Sign the certificate + X509CertificateHolder certificateHolder = certBuilder.build(sigGen); + return new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certificateHolder); + } catch (CertIOException | NoSuchAlgorithmException | OperatorCreationException e) { + throw new CertificateException(e); + } + } + + /** + * Generates an issued {@link X509Certificate} from the given issuer certificate and {@link KeyPair} + * + * @param dn the distinguished name to use + * @param publicKey the public key to issue the certificate to + * @param issuer the issuer's certificate + * @param issuerKeyPair the issuer's keypair + * @param signingAlgorithm the signing algorithm to use + * @param days the number of days it should be valid for + * @return an issued {@link X509Certificate} from the given issuer certificate and {@link KeyPair} + * @throws CertificateException if there is an error issuing the certificate + */ + public static X509Certificate generateIssuedCertificate(String dn, PublicKey publicKey, X509Certificate issuer, KeyPair issuerKeyPair, String signingAlgorithm, int days) + throws CertificateException { + return generateIssuedCertificate(dn, publicKey, null, issuer, issuerKeyPair, signingAlgorithm, days); + } + + /** + * Generates an issued {@link X509Certificate} from the given issuer certificate and {@link KeyPair} + * + * @param dn the distinguished name to use + * @param publicKey the public key to issue the certificate to + * @param extensions extensions extracted from the CSR + * @param issuer the issuer's certificate + * @param issuerKeyPair the issuer's keypair + * @param signingAlgorithm the signing algorithm to use + * @param days the number of days it should be valid for + * @return an issued {@link X509Certificate} from the given issuer certificate and {@link KeyPair} + * @throws CertificateException if there is an error issuing the certificate + */ + public static X509Certificate generateIssuedCertificate(String dn, PublicKey publicKey, Extensions extensions, X509Certificate issuer, KeyPair issuerKeyPair, String signingAlgorithm, int days) + throws CertificateException { + try { + ContentSigner sigGen = new JcaContentSignerBuilder(signingAlgorithm).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(issuerKeyPair.getPrivate()); + SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(publicKey.getEncoded()); + Date startDate = new Date(); + Date endDate = new Date(startDate.getTime() + TimeUnit.DAYS.toMillis(days)); + + X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder( + reverseX500Name(new X500Name(issuer.getSubjectX500Principal().getName())), + getUniqueSerialNumber(), + startDate, endDate, + reverseX500Name(new X500Name(dn)), + subPubKeyInfo); + + certBuilder.addExtension(Extension.subjectKeyIdentifier, false, new JcaX509ExtensionUtils().createSubjectKeyIdentifier(publicKey)); + + certBuilder.addExtension(Extension.authorityKeyIdentifier, false, new JcaX509ExtensionUtils().createAuthorityKeyIdentifier(issuerKeyPair.getPublic())); + // Set certificate extensions + // (1) digitalSignature extension + certBuilder.addExtension(Extension.keyUsage, true, + new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment | KeyUsage.dataEncipherment | KeyUsage.keyAgreement | KeyUsage.nonRepudiation)); + + certBuilder.addExtension(Extension.basicConstraints, false, new BasicConstraints(false)); + + // (2) extendedKeyUsage extension + certBuilder.addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_clientAuth, KeyPurposeId.id_kp_serverAuth})); + + // (3) subjectAlternativeName + if (extensions != null && extensions.getExtension(Extension.subjectAlternativeName) != null) { + certBuilder.addExtension(Extension.subjectAlternativeName, false, extensions.getExtensionParsedValue(Extension.subjectAlternativeName)); + } + + X509CertificateHolder certificateHolder = certBuilder.build(sigGen); + return new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certificateHolder); + } catch (CertIOException | NoSuchAlgorithmException | OperatorCreationException e) { + throw new CertificateException(e); + } + } + + /** + * Returns true if the two provided DNs are equivalent, regardless of the order of the elements. Returns false if one or both are invalid DNs. + *

+ * Example: + *

+ * CN=test1, O=testOrg, C=US compared to CN=test1, O=testOrg, C=US -> true + * CN=test1, O=testOrg, C=US compared to O=testOrg, CN=test1, C=US -> true + * CN=test1, O=testOrg, C=US compared to CN=test2, O=testOrg, C=US -> false + * CN=test1, O=testOrg, C=US compared to O=testOrg, CN=test2, C=US -> false + * CN=test1, O=testOrg, C=US compared to -> false + * compared to -> true + * + * @param dn1 the first DN to compare + * @param dn2 the second DN to compare + * @return true if the DNs are equivalent, false otherwise + */ + public static boolean compareDNs(String dn1, String dn2) { + if (dn1 == null) { + dn1 = ""; + } + + if (dn2 == null) { + dn2 = ""; + } + + if (StringUtils.isEmpty(dn1) || StringUtils.isEmpty(dn2)) { + return dn1.equals(dn2); + } + try { + List rdn1 = new LdapName(dn1).getRdns(); + List rdn2 = new LdapName(dn2).getRdns(); + + return rdn1.size() == rdn2.size() && rdn1.containsAll(rdn2); + } catch (InvalidNameException e) { + logger.warn("Cannot compare DNs: {} and {} because one or both is not a valid DN", dn1, dn2); + return false; + } + } + + /** + * Extract extensions from CSR object + */ + public static Extensions getExtensionsFromCSR(JcaPKCS10CertificationRequest csr) { + Attribute[] attributess = csr.getAttributes(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest); + for (Attribute attribute : attributess) { + ASN1Set attValue = attribute.getAttrValues(); + if (attValue != null) { + ASN1Encodable extension = attValue.getObjectAt(0); + if (extension instanceof Extensions) { + return (Extensions) extension; + } else if (extension instanceof DERSequence) { + return Extensions.getInstance(extension); + } + } + } + return null; + } + + /** + * Returns the JVM Java major version based on the System properties (e.g. {@code JVM 1.8.0.231} -> {code 8}). + * + * @return the Java major version + */ + public static int getJavaVersion() { + String version = System.getProperty("java.version"); + return parseJavaVersion(version); + } + + /** + * Returns the major version parsed from the provided Java version string (e.g. {@code "1.8.0.231"} -> {@code 8}). + * + * @param version the Java version string + * @return the major version as an int + */ + public static int parseJavaVersion(String version) { + String majorVersion; + if (version.startsWith("1.")) { + majorVersion = version.substring(2, 3); + } else { + Pattern majorVersion9PlusPattern = Pattern.compile("(\\d+).*"); + Matcher m = majorVersion9PlusPattern.matcher(version); + if (m.find()) { + majorVersion = m.group(1); + } else { + throw new IllegalArgumentException("Could not detect major version of " + version); + } + } + return Integer.parseInt(majorVersion); + } + + /** + * Returns a {@code String[]} of supported TLS protocol versions based on the current Java platform version. + * + * @return the supported TLS protocol version(s) + */ + public static String[] getCurrentSupportedTlsProtocolVersions() { + int javaMajorVersion = getJavaVersion(); + if (javaMajorVersion < 11) { + return JAVA_8_SUPPORTED_TLS_PROTOCOL_VERSIONS; + } else { + return JAVA_11_SUPPORTED_TLS_PROTOCOL_VERSIONS; + } + } + + /** + * Returns the highest supported TLS protocol version based on the current Java platform version. + * + * @return the TLS protocol (e.g. {@code "TLSv1.2"}) + */ + public static String getHighestCurrentSupportedTlsProtocolVersion() { + int javaMajorVersion = getJavaVersion(); + if (javaMajorVersion < 11) { + return JAVA_8_MAX_SUPPORTED_TLS_PROTOCOL_VERSION; + } else { + return JAVA_11_MAX_SUPPORTED_TLS_PROTOCOL_VERSION; + } + } + + private CertificateUtils() { + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/CryptoUtils.java b/nifi-registry/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/CryptoUtils.java new file mode 100644 index 0000000000..cd2d3e3578 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/CryptoUtils.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.util; + +import javax.crypto.Cipher; +import java.security.NoSuchAlgorithmException; + +public class CryptoUtils { + + /** + * Required Cipher transformations according to Java SE 8 {@link Cipher} docs + */ + private static final String[] standardCryptoTransformations = { + "AES/CBC/NoPadding", + "AES/CBC/PKCS5Padding", + "AES/ECB/NoPadding", + "AES/ECB/PKCS5Padding", + "DES/CBC/NoPadding", + "DES/CBC/PKCS5Padding", + "DES/ECB/NoPadding", + "DES/ECB/PKCS5Padding", + "DESede/CBC/NoPadding", + "DESede/CBC/PKCS5Padding", + "DESede/ECB/NoPadding", + "DESede/ECB/PKCS5Padding", + "RSA/ECB/PKCS1Padding", + "RSA/ECB/OAEPWithSHA-1AndMGF1Padding", + "RSA/ECB/OAEPWithSHA-256AndMGF1Padding" + }; + + /** + * Check if cryptographic strength available in this Java Runtime is restricted. + * + * Not every Java Platform supports "unlimited strength encryption", + * so this convenience method provides a way to check if strength of crypto + * functions (i.e., max key length) is unlimited or restricted in the + * current Java runtime environment. + * + * @return true if it can be determined that max key lengths are less than unlimited + * false if key lengths are restricted + * null if max key length cannot be determined for any known Cipher transformations */ + public static Boolean isCryptoRestricted() { + + Boolean isCryptoRestricted = null; + + for (String transformation : standardCryptoTransformations) { + try { + return Cipher.getMaxAllowedKeyLength(transformation) < Integer.MAX_VALUE; + } catch (final NoSuchAlgorithmException e) { + // Unexpected as we are pulling from a list of transforms that every + // java platform is required to support, but try the next one + } + } + + // Tried every standard Cipher transformation and none were available, + // so crypto strength restrictions cannot be determined. + return null; + + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/KeyStoreUtils.java b/nifi-registry/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/KeyStoreUtils.java new file mode 100644 index 0000000000..952419d183 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/KeyStoreUtils.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.util; + +import org.apache.commons.lang3.StringUtils; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.Security; +import java.util.HashMap; +import java.util.Map; + +public class KeyStoreUtils { + private static final Logger logger = LoggerFactory.getLogger(KeyStoreUtils.class); + + private static final String SUN_SECURITY_PROVIDER = "SUN"; + + private static final Map KEY_STORE_TYPE_PROVIDERS = new HashMap<>(); + + static { + Security.addProvider(new BouncyCastleProvider()); + + KEY_STORE_TYPE_PROVIDERS.put(KeystoreType.JKS.toString(), SUN_SECURITY_PROVIDER); + KEY_STORE_TYPE_PROVIDERS.put(KeystoreType.PKCS12.toString(), BouncyCastleProvider.PROVIDER_NAME); + KEY_STORE_TYPE_PROVIDERS.put(KeystoreType.BCFKS.toString(), BouncyCastleProvider.PROVIDER_NAME); + } + + /** + * Returns the provider that will be used for the given keyStoreType + * + * @param keyStoreType the keyStoreType + * @return the provider that will be used + */ + public static String getKeyStoreProvider(final String keyStoreType) { + final String storeType = StringUtils.upperCase(keyStoreType); + return KEY_STORE_TYPE_PROVIDERS.get(storeType); + } + + /** + * Returns an empty KeyStore backed by the appropriate provider + * + * @param keyStoreType the keyStoreType + * @return an empty KeyStore + * @throws KeyStoreException if a KeyStore of the given type cannot be instantiated + */ + public static KeyStore getKeyStore(final String keyStoreType) throws KeyStoreException { + final String keyStoreProvider = getKeyStoreProvider(keyStoreType); + if (StringUtils.isNotEmpty(keyStoreProvider)) { + try { + return KeyStore.getInstance(keyStoreType, keyStoreProvider); + } catch (Exception e) { + logger.error("Unable to load " + keyStoreProvider + " " + keyStoreType + + " keystore. This may cause issues getting trusted CA certificates as well as Certificate Chains for use in TLS.", e); + } + } + return KeyStore.getInstance(keyStoreType); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/KeystoreType.java b/nifi-registry/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/KeystoreType.java new file mode 100644 index 0000000000..d785610d6a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/KeystoreType.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.util; + +/** + * Keystore types. + */ +public enum KeystoreType { + BCFKS, + PKCS12, + JKS +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/ProxiedEntitiesUtils.java b/nifi-registry/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/ProxiedEntitiesUtils.java new file mode 100644 index 0000000000..45a11f47e4 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/ProxiedEntitiesUtils.java @@ -0,0 +1,251 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.util; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; +import java.util.stream.Collectors; + +public class ProxiedEntitiesUtils { + private static final Logger logger = LoggerFactory.getLogger(ProxiedEntitiesUtils.class); + + public static final String PROXY_ENTITIES_CHAIN = "X-ProxiedEntitiesChain"; + public static final String PROXY_ENTITIES_ACCEPTED = "X-ProxiedEntitiesAccepted"; + public static final String PROXY_ENTITIES_DETAILS = "X-ProxiedEntitiesDetails"; + + private static final String GT = ">"; + private static final String ESCAPED_GT = "\\\\>"; + private static final String LT = "<"; + private static final String ESCAPED_LT = "\\\\<"; + + private static final String ANONYMOUS_CHAIN = "<>"; + private static final String ANONYMOUS_IDENTITY = ""; + + /** + * Formats a list of DN/usernames to be set as a HTTP header using well known conventions. + * + * @param proxiedEntities the raw identities (usernames and DNs) to be formatted as a chain + * @return the value to use in the X-ProxiedEntitiesChain header + */ + public static String getProxiedEntitiesChain(final String... proxiedEntities) { + return getProxiedEntitiesChain(Arrays.asList(proxiedEntities)); + } + + /** + * Formats a list of DN/usernames to be set as a HTTP header using well known conventions. + * + * @param proxiedEntities the raw identities (usernames and DNs) to be formatted as a chain + * @return the value to use in the X-ProxiedEntitiesChain header + */ + public static String getProxiedEntitiesChain(final List proxiedEntities) { + if (proxiedEntities == null) { + return null; + } + + final List proxiedEntityChain = proxiedEntities.stream() + .map(ProxiedEntitiesUtils::formatProxyDn) + .collect(Collectors.toList()); + return StringUtils.join(proxiedEntityChain, ""); + } + + /** + * Tokenizes the specified proxy chain. + * + * @param rawProxyChain raw chain + * @return tokenized proxy chain + */ + public static List tokenizeProxiedEntitiesChain(final String rawProxyChain) { + final List proxyChain = new ArrayList<>(); + if (!StringUtils.isEmpty(rawProxyChain)) { + + if (!isValidChainFormat(rawProxyChain)) { + throw new IllegalArgumentException("Proxy chain format is not recognized and can not safely be converted to a list."); + } + + if (rawProxyChain.equals(ANONYMOUS_CHAIN)) { + proxyChain.add(ANONYMOUS_IDENTITY); + } else { + // Split the String on the `><` token, use substring to remove leading `<` and trailing `>` + final String[] elements = StringUtils.splitByWholeSeparatorPreserveAllTokens( + rawProxyChain.substring(1, rawProxyChain.length() - 1), "><"); + // Unsanitize each DN and add it to the proxy chain list + Arrays.stream(elements) + .map(ProxiedEntitiesUtils::unsanitizeDn) + .forEach(proxyChain::add); + } + } + return proxyChain; + } + + /** + * Formats the specified DN to be set as a HTTP header using well known conventions. + * + * @param dn raw dn + * @return the dn formatted as an HTTP header + */ + public static String formatProxyDn(final String dn) { + return LT + sanitizeDn(dn) + GT; + } + + /** + * Sanitizes a DN for safe and lossless transmission. + * + * Sanitization requires: + *

    + *
  1. Encoded so that it can be sent losslessly using US-ASCII (the character set of HTTP Header values)
  2. + *
  3. Resilient to a DN with the sequence '><' to attempt to escape the tokenization process and impersonate another user.
  4. + *
+ * + *

+ * Example: + *

+ * Provided DN: {@code jdoe> {@code } would allow the user to impersonate jdoe + *

Алйс + * Provided DN: {@code Алйс} -> {@code <Алйс>} cannot be encoded/decoded as ASCII + * + * @param rawDn the unsanitized DN + * @return the sanitized DN + */ + private static String sanitizeDn(final String rawDn) { + if (StringUtils.isEmpty(rawDn)) { + return rawDn; + } else { + + // First, escape any GT [>] or LT [<] characters, which are not safe + final String escapedDn = rawDn.replaceAll(GT, ESCAPED_GT).replaceAll(LT, ESCAPED_LT); + if (!escapedDn.equals(rawDn)) { + logger.warn("The provided DN [" + rawDn + "] contained dangerous characters that were escaped to [" + escapedDn + "]"); + } + + // Second, check for characters outside US-ASCII. + // This is necessary because X509 Certs can contain international/Unicode characters, + // but this value will be passed in an HTTP Header which must be US-ASCII. + // If non-ascii characters are present, base64 encode the DN and wrap in , + // to indicate to the receiving end that the value must be decoded. + // Note: We could have decided to always base64 encode these values, + // not only to avoid the isPureAscii(...) check, but also as a + // method of sanitizing GT [>] or LT [<] chars. However, there + // are advantages to encoding only when necessary, namely: + // 1. Backwards compatibility + // 2. Debugging this X-ProxiedEntitiesChain headers is easier unencoded. + // This algorithm can be revisited as part of the next major version change. + if (isPureAscii(escapedDn)) { + return escapedDn; + } else { + final String encodedDn = base64Encode(escapedDn); + logger.debug("The provided DN [" + rawDn + "] contained non-ASCII characters and was encoded as [" + encodedDn + "]"); + return encodedDn; + } + } + } + + /** + * Reconstitutes the original DN from the sanitized version passed in the proxy chain. + *

+ * Example: + *

+ * {@code alopresto\>\ {@code alopresto> + * {@code %D0%90%D0%BB%D0%B9%D1%81} -> {@code Алйс} + * + * @param sanitizedDn the sanitized DN + * @return the original DN + */ + private static String unsanitizeDn(final String sanitizedDn) { + if (StringUtils.isEmpty(sanitizedDn)) { + return sanitizedDn; + } else { + final String decodedDn; + if (isBase64Encoded(sanitizedDn)) { + decodedDn = base64Decode(sanitizedDn); + logger.debug("The provided DN [" + sanitizedDn + "] had been encoded, and was reconstituted to the original DN [" + decodedDn + "]"); + } else { + decodedDn = sanitizedDn; + } + final String unsanitizedDn = decodedDn.replaceAll(ESCAPED_GT, GT).replaceAll(ESCAPED_LT, LT); + if (!unsanitizedDn.equals(decodedDn)) { + logger.warn("The provided DN [" + sanitizedDn + "] had been escaped, and was reconstituted to the dangerous DN [" + unsanitizedDn + "]"); + } + return unsanitizedDn; + } + } + + /** + * Base64 encodes a DN and wraps it in angled brackets to indicate the value is base64 and not a raw DN. + * + * @param rawValue The value to encode + * @return A string containing a wrapped, encoded value. + */ + private static String base64Encode(final String rawValue) { + final String base64String = Base64.getEncoder().encodeToString(rawValue.getBytes(StandardCharsets.UTF_8)); + final String wrappedEncodedValue = LT + base64String + GT; + return wrappedEncodedValue; + } + + /** + * Performs the reverse of ${@link #base64Encode(String)}. + * + * @param encodedValue the encoded value to decode. + * @return The original, decoded string. + */ + private static String base64Decode(final String encodedValue) { + final String base64String = encodedValue.substring(1, encodedValue.length() - 1); + return new String(Base64.getDecoder().decode(base64String), StandardCharsets.UTF_8); + } + + /** + * Check if a String is in the expected format and can be safely tokenized. + * + * @param rawProxiedEntitiesChain the value to check + * @return true if the value is in the valid format to tokenize, false otherwise. + */ + private static boolean isValidChainFormat(final String rawProxiedEntitiesChain) { + return isWrappedInAngleBrackets(rawProxiedEntitiesChain); + } + + /** + * Check if a value has been encoded by ${@link #base64Encode(String)}, and therefore needs to be decoded. + * + * @param token the value to check + * @return true if the value is encoded, false otherwise. + */ + private static boolean isBase64Encoded(final String token) { + return isWrappedInAngleBrackets(token); + } + + /** + * Check if a string is wrapped with <angle brackets>. + * + * @param string the value to check + * @return true if the value starts with < and ends with > - false otherwise + */ + private static boolean isWrappedInAngleBrackets(final String string) { + return string.startsWith(LT) && string.endsWith(GT); + } + + private static boolean isPureAscii(final String stringWithUnknownCharacters) { + return StandardCharsets.US_ASCII.newEncoder().canEncode(stringWithUnknownCharacters); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/SslContextFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/SslContextFactory.java new file mode 100644 index 0000000000..e10749951b --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/SslContextFactory.java @@ -0,0 +1,249 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.util; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; + +/** + * A factory for creating SSL contexts using the application's security + * properties. + * + */ +public final class SslContextFactory { + + public static enum ClientAuth { + + WANT, + REQUIRED, + NONE + } + + /** + * Creates a SSLContext instance using the given information. The password for the key is assumed to be the same + * as the password for the keystore. If this is not the case, the {@link #createSslContext(String, char[], chart[], String, String, char[], String, ClientAuth, String)} + * method should be used instead + * + * @param keystore the full path to the keystore + * @param keystorePasswd the keystore password + * @param keystoreType the type of keystore (e.g., PKCS12, JKS) + * @param truststore the full path to the truststore + * @param truststorePasswd the truststore password + * @param truststoreType the type of truststore (e.g., PKCS12, JKS) + * @param clientAuth the type of client authentication + * @param protocol the protocol to use for the SSL connection + * + * @return a SSLContext instance + * @throws KeyStoreException if any issues accessing the keystore + * @throws IOException for any problems loading the keystores + * @throws NoSuchAlgorithmException if an algorithm is found to be used but is unknown + * @throws CertificateException if there is an issue with the certificate + * @throws UnrecoverableKeyException if the key is insufficient + * @throws KeyManagementException if unable to manage the key + */ + public static SSLContext createSslContext( + final String keystore, final char[] keystorePasswd, final String keystoreType, + final String truststore, final char[] truststorePasswd, final String truststoreType, + final ClientAuth clientAuth, final String protocol) + throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, + UnrecoverableKeyException, KeyManagementException { + + // Pass the keystore password as both the keystore password and the key password. + return createSslContext(keystore, keystorePasswd, keystorePasswd, keystoreType, truststore, truststorePasswd, truststoreType, clientAuth, protocol); + } + + /** + * Creates a SSLContext instance using the given information. + * + * @param keystore the full path to the keystore + * @param keystorePasswd the keystore password + * @param keystoreType the type of keystore (e.g., PKCS12, JKS) + * @param truststore the full path to the truststore + * @param truststorePasswd the truststore password + * @param truststoreType the type of truststore (e.g., PKCS12, JKS) + * @param clientAuth the type of client authentication + * @param protocol the protocol to use for the SSL connection + * + * @return a SSLContext instance + * @throws KeyStoreException if any issues accessing the keystore + * @throws IOException for any problems loading the keystores + * @throws NoSuchAlgorithmException if an algorithm is found to be used but is unknown + * @throws CertificateException if there is an issue with the certificate + * @throws UnrecoverableKeyException if the key is insufficient + * @throws KeyManagementException if unable to manage the key + */ + public static SSLContext createSslContext( + final String keystore, final char[] keystorePasswd, final char[] keyPasswd, final String keystoreType, + final String truststore, final char[] truststorePasswd, final String truststoreType, + final ClientAuth clientAuth, final String protocol) + throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, + UnrecoverableKeyException, KeyManagementException { + + // prepare the keystore + final KeyStore keyStore = KeyStoreUtils.getKeyStore(keystoreType); + try (final InputStream keyStoreStream = new FileInputStream(keystore)) { + keyStore.load(keyStoreStream, keystorePasswd); + } + final KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + if (keyPasswd == null) { + keyManagerFactory.init(keyStore, keystorePasswd); + } else { + keyManagerFactory.init(keyStore, keyPasswd); + } + + // prepare the truststore + final KeyStore trustStore = KeyStoreUtils.getKeyStore(truststoreType); + try (final InputStream trustStoreStream = new FileInputStream(truststore)) { + trustStore.load(trustStoreStream, truststorePasswd); + } + final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(trustStore); + + // initialize the ssl context + final SSLContext sslContext = SSLContext.getInstance(protocol); + sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom()); + if (ClientAuth.REQUIRED == clientAuth) { + sslContext.getDefaultSSLParameters().setNeedClientAuth(true); + } else if (ClientAuth.WANT == clientAuth) { + sslContext.getDefaultSSLParameters().setWantClientAuth(true); + } else { + sslContext.getDefaultSSLParameters().setWantClientAuth(false); + } + + return sslContext; + + } + + /** + * Creates a SSLContext instance using the given information. This method assumes that the key password is + * the same as the keystore password. If this is not the case, use the {@link #createSslContext(String, char[], char[], String, String)} + * method instead. + * + * @param keystore the full path to the keystore + * @param keystorePasswd the keystore password + * @param keystoreType the type of keystore (e.g., PKCS12, JKS) + * @param protocol the protocol to use for the SSL connection + * + * @return a SSLContext instance + * @throws KeyStoreException if any issues accessing the keystore + * @throws IOException for any problems loading the keystores + * @throws NoSuchAlgorithmException if an algorithm is found to be used but is unknown + * @throws CertificateException if there is an issue with the certificate + * @throws UnrecoverableKeyException if the key is insufficient + * @throws KeyManagementException if unable to manage the key + */ + public static SSLContext createSslContext( + final String keystore, final char[] keystorePasswd, final String keystoreType, final String protocol) + throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, + UnrecoverableKeyException, KeyManagementException { + + // create SSL Context passing keystore password as the key password + return createSslContext(keystore, keystorePasswd, keystorePasswd, keystoreType, protocol); + } + + /** + * Creates a SSLContext instance using the given information. + * + * @param keystore the full path to the keystore + * @param keystorePasswd the keystore password + * @param keystoreType the type of keystore (e.g., PKCS12, JKS) + * @param protocol the protocol to use for the SSL connection + * + * @return a SSLContext instance + * @throws KeyStoreException if any issues accessing the keystore + * @throws IOException for any problems loading the keystores + * @throws NoSuchAlgorithmException if an algorithm is found to be used but is unknown + * @throws CertificateException if there is an issue with the certificate + * @throws UnrecoverableKeyException if the key is insufficient + * @throws KeyManagementException if unable to manage the key + */ + public static SSLContext createSslContext( + final String keystore, final char[] keystorePasswd, final char[] keyPasswd, final String keystoreType, final String protocol) + throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, + UnrecoverableKeyException, KeyManagementException { + + // prepare the keystore + final KeyStore keyStore = KeyStoreUtils.getKeyStore(keystoreType); + try (final InputStream keyStoreStream = new FileInputStream(keystore)) { + keyStore.load(keyStoreStream, keystorePasswd); + } + final KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + if (keyPasswd == null) { + keyManagerFactory.init(keyStore, keystorePasswd); + } else { + keyManagerFactory.init(keyStore, keyPasswd); + } + + // initialize the ssl context + final SSLContext ctx = SSLContext.getInstance(protocol); + ctx.init(keyManagerFactory.getKeyManagers(), new TrustManager[0], new SecureRandom()); + + return ctx; + + } + + /** + * Creates a SSLContext instance using the given information. + * + * @param truststore the full path to the truststore + * @param truststorePasswd the truststore password + * @param truststoreType the type of truststore (e.g., PKCS12, JKS) + * @param protocol the protocol to use for the SSL connection + * + * @return a SSLContext instance + * @throws KeyStoreException if any issues accessing the keystore + * @throws IOException for any problems loading the keystores + * @throws NoSuchAlgorithmException if an algorithm is found to be used but is unknown + * @throws CertificateException if there is an issue with the certificate + * @throws UnrecoverableKeyException if the key is insufficient + * @throws KeyManagementException if unable to manage the key + */ + public static SSLContext createTrustSslContext( + final String truststore, final char[] truststorePasswd, final String truststoreType, final String protocol) + throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, + UnrecoverableKeyException, KeyManagementException { + + // prepare the truststore + final KeyStore trustStore = KeyStoreUtils.getKeyStore(truststoreType); + try (final InputStream trustStoreStream = new FileInputStream(truststore)) { + trustStore.load(trustStoreStream, truststorePasswd); + } + final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(trustStore); + + // initialize the ssl context + final SSLContext ctx = SSLContext.getInstance(protocol); + ctx.init(new KeyManager[0], trustManagerFactory.getTrustManagers(), new SecureRandom()); + + return ctx; + + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/kerberos/KerberosPrincipalParser.java b/nifi-registry/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/kerberos/KerberosPrincipalParser.java new file mode 100644 index 0000000000..22328e90d7 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/kerberos/KerberosPrincipalParser.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.util.kerberos; + +import org.apache.commons.lang3.StringUtils; + +public class KerberosPrincipalParser { + + /** + *

Determines the realm specified in the given kerberos principal. + * + *

The content of the given {@code principal} after the occurrence + * of the last non-escaped realm delimiter ("@") will be considered + * the realm of the principal. + * + *

The validity of the given {@code principal} and the determined realm + * is not be verified by this method. + * + * @param principal the principal for which the realm will be determined + * @return the realm of the given principal + */ + public static String getRealm(String principal) { + if (StringUtils.isBlank(principal)) { + throw new IllegalArgumentException("principal can not be null or empty"); + } + + char previousChar = 0; + int realmDelimiterIndex = -1; + char currentChar; + boolean realmDelimiterFound = false; + int principalLength = principal.length(); + + // find the last non-escaped occurrence of the realm delimiter + for (int i = 0; i < principalLength; ++i) { + currentChar = principal.charAt(i); + if (currentChar == '@' && previousChar != '\\' ) { + realmDelimiterIndex = i; + realmDelimiterFound = true; + } + previousChar = currentChar; + } + + String principalAfterLastRealmDelimiter = principal.substring(realmDelimiterIndex + 1); + return realmDelimiterFound && realmDelimiterIndex + 1 < principalLength ? principalAfterLastRealmDelimiter : null; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-utils/src/test/groovy/org/apache/nifi/registry/security/util/ProxiedEntitiesUtilsTest.groovy b/nifi-registry/nifi-registry-core/nifi-registry-security-utils/src/test/groovy/org/apache/nifi/registry/security/util/ProxiedEntitiesUtilsTest.groovy new file mode 100644 index 0000000000..16b66e0f85 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-utils/src/test/groovy/org/apache/nifi/registry/security/util/ProxiedEntitiesUtilsTest.groovy @@ -0,0 +1,334 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.util + +import org.junit.After +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.nio.charset.StandardCharsets + +@RunWith(JUnit4.class) +class ProxiedEntitiesUtilsTest { + private static final Logger logger = LoggerFactory.getLogger(ProxiedEntitiesUtils.class) + + private static final String SAFE_USER_NAME_ANDY = "alopresto" + private static final String SAFE_USER_DN_ANDY = "CN=${SAFE_USER_NAME_ANDY}, OU=Apache NiFi" + + private static final String SAFE_USER_NAME_JOHN = "jdoe" + private static final String SAFE_USER_DN_JOHN = "CN=${SAFE_USER_NAME_JOHN}, OU=Apache NiFi" + + private static final String SAFE_USER_NAME_PROXY_1 = "proxy1.nifi.apache.org" + private static final String SAFE_USER_DN_PROXY_1 = "CN=${SAFE_USER_NAME_PROXY_1}, OU=Apache NiFi" + + private static final String SAFE_USER_NAME_PROXY_2 = "proxy2.nifi.apache.org" + private static final String SAFE_USER_DN_PROXY_2 = "CN=${SAFE_USER_NAME_PROXY_2}, OU=Apache NiFi" + + private static + final String MALICIOUS_USER_NAME_JOHN = "${SAFE_USER_NAME_JOHN}, OU=Apache NiFi>" + + private static final String UNICODE_DN_2 = "CN=Боб, OU=Apache NiFi" + private static final String UNICODE_DN_2_ENCODED = "<" + base64Encode(UNICODE_DN_2) + ">" + + @BeforeClass + static void setUpOnce() throws Exception { + logger.metaClass.methodMissing = { String name, args -> + logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}") + } + } + + @Before + void setUp() { + } + + @After + void tearDown() { + } + + private static String sanitizeDn(String dn = "") { + dn.replaceAll(/>/, '\\\\>').replaceAll('<', '\\\\<') + } + + private static String base64Encode(String dn = "") { + return Base64.getEncoder().encodeToString(dn.getBytes(StandardCharsets.UTF_8)) + } + + private static String printUnicodeString(final String raw) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < raw.size(); i++) { + int codePoint = Character.codePointAt(raw, i) + int charCount = Character.charCount(codePoint) + if (charCount > 1) { + i += charCount - 1 // 2. + if (i >= raw.length()) { + throw new IllegalArgumentException("Code point indicated more characters than available") + } + } + sb.append(String.format("\\u%04x ", codePoint)) + } + return sb.toString().trim() + } + + @Test + void testSanitizeDnShouldHandleFuzzing() throws Exception { + // Arrange + final String DESIRED_NAME = SAFE_USER_NAME_JOHN + logger.info(" Desired name: ${DESIRED_NAME} | ${printUnicodeString(DESIRED_NAME)}") + + // Contains various attempted >< escapes, trailing NULL, and BACKSPACE + 'n' + final List MALICIOUS_NAMES = [MALICIOUS_USER_NAME_JOHN, + SAFE_USER_NAME_JOHN + ">", + SAFE_USER_NAME_JOHN + "><>", + SAFE_USER_NAME_JOHN + "\\>", + SAFE_USER_NAME_JOHN + "\u003e", + SAFE_USER_NAME_JOHN + "\u005c\u005c\u003e", + SAFE_USER_NAME_JOHN + "\u0000", + SAFE_USER_NAME_JOHN + "\u0008n"] + + // Act + MALICIOUS_NAMES.each { String name -> + logger.info(" Raw name: ${name} | ${printUnicodeString(name)}") + String sanitizedName = ProxiedEntitiesUtils.sanitizeDn(name) + logger.info("Sanitized name: ${sanitizedName} | ${printUnicodeString(sanitizedName)}") + + // Assert + assert sanitizedName != DESIRED_NAME + } + } + + @Test + void testShouldFormatProxyDn() throws Exception { + // Arrange + final String DN = SAFE_USER_DN_JOHN + logger.info(" Provided proxy DN: ${DN}") + + final String EXPECTED_PROXY_DN = "<${DN}>" + logger.info(" Expected proxy DN: ${EXPECTED_PROXY_DN}") + + // Act + String forjohnedProxyDn = ProxiedEntitiesUtils.formatProxyDn(DN) + logger.info("Forjohned proxy DN: ${forjohnedProxyDn}") + + // Assert + assert forjohnedProxyDn == EXPECTED_PROXY_DN + } + + @Test + void testFormatProxyDnShouldHandleMaliciousInput() throws Exception { + // Arrange + final String DN = MALICIOUS_USER_DN_JOHN + logger.info(" Provided proxy DN: ${DN}") + + final String SANITIZED_DN = sanitizeDn(DN) + final String EXPECTED_PROXY_DN = "<${SANITIZED_DN}>" + logger.info(" Expected proxy DN: ${EXPECTED_PROXY_DN}") + + // Act + String forjohnedProxyDn = ProxiedEntitiesUtils.formatProxyDn(DN) + logger.info("Forjohned proxy DN: ${forjohnedProxyDn}") + + // Assert + assert forjohnedProxyDn == EXPECTED_PROXY_DN + } + + @Test + void testFormatProxyDnShouldEncodeNonAsciiCharacters() throws Exception { + // Arrange + logger.info(" Provided DN: ${UNICODE_DN_1}") + final String expectedFormattedDn = "<${UNICODE_DN_1_ENCODED}>" + logger.info(" Expected DN: expected") + + // Act + String formattedDn = ProxiedEntitiesUtils.formatProxyDn(UNICODE_DN_1) + logger.info("Formatted DN: ${formattedDn}") + + // Assert + assert formattedDn == expectedFormattedDn + } + + @Test + void testGetProxiedEntitiesChain() throws Exception { + // Arrange + String[] input = [SAFE_USER_NAME_JOHN, SAFE_USER_DN_PROXY_1, SAFE_USER_DN_PROXY_2] + final String expectedOutput = "<${SAFE_USER_NAME_JOHN}><${SAFE_USER_DN_PROXY_1}><${SAFE_USER_DN_PROXY_2}>" + + // Act + def output = ProxiedEntitiesUtils.getProxiedEntitiesChain(input) + + // Assert + assert output == expectedOutput + } + + @Test + void testGetProxiedEntitiesChainShouldHandleMaliciousInput() throws Exception { + // Arrange + String[] input = [MALICIOUS_USER_DN_JOHN, SAFE_USER_DN_PROXY_1, SAFE_USER_DN_PROXY_2] + final String expectedOutput = "<${sanitizeDn(MALICIOUS_USER_DN_JOHN)}><${SAFE_USER_DN_PROXY_1}><${SAFE_USER_DN_PROXY_2}>" + + // Act + def output = ProxiedEntitiesUtils.getProxiedEntitiesChain(input) + + // Assert + assert output == expectedOutput + } + + @Test + void testGetProxiedEntitiesChainShouldEncodeUnicode() throws Exception { + // Arrange + String[] input = [SAFE_USER_NAME_JOHN, UNICODE_DN_1, UNICODE_DN_2] + final String expectedOutput = "<${SAFE_USER_NAME_JOHN}><${UNICODE_DN_1_ENCODED}><${UNICODE_DN_2_ENCODED}>" + + // Act + def output = ProxiedEntitiesUtils.getProxiedEntitiesChain(input) + + // Assert + assert output == expectedOutput + } + + @Test + void testShouldTokenizeProxiedEntitiesChainWithUserNames() throws Exception { + // Arrange + final List NAMES = [SAFE_USER_NAME_JOHN, SAFE_USER_NAME_PROXY_1, SAFE_USER_NAME_PROXY_2] + final String RAW_PROXY_CHAIN = "<${NAMES.join("><")}>" + logger.info(" Provided proxy chain: ${RAW_PROXY_CHAIN}") + + // Act + def tokenizedNames = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(RAW_PROXY_CHAIN) + logger.info("Tokenized proxy chain: ${tokenizedNames}") + + // Assert + assert tokenizedNames == NAMES + } + + @Test + void testShouldTokenizeAnonymous() throws Exception { + // Arrange + final List NAMES = [""] + final String RAW_PROXY_CHAIN = "<>" + logger.info(" Provided proxy chain: ${RAW_PROXY_CHAIN}") + + // Act + def tokenizedNames = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(RAW_PROXY_CHAIN) + logger.info("Tokenized proxy chain: ${tokenizedNames}") + + // Assert + assert tokenizedNames == NAMES + } + + @Test + void testShouldTokenizeDoubleAnonymous() throws Exception { + // Arrange + final List NAMES = ["", ""] + final String RAW_PROXY_CHAIN = "<><>" + logger.info(" Provided proxy chain: ${RAW_PROXY_CHAIN}") + + // Act + def tokenizedNames = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(RAW_PROXY_CHAIN) + logger.info("Tokenized proxy chain: ${tokenizedNames}") + + // Assert + assert tokenizedNames == NAMES + } + + @Test + void testShouldTokenizeNestedAnonymous() throws Exception { + // Arrange + final List NAMES = [SAFE_USER_DN_PROXY_1, "", SAFE_USER_DN_PROXY_2] + final String RAW_PROXY_CHAIN = "<${SAFE_USER_DN_PROXY_1}><><${SAFE_USER_DN_PROXY_2}>" + logger.info(" Provided proxy chain: ${RAW_PROXY_CHAIN}") + + // Act + def tokenizedNames = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(RAW_PROXY_CHAIN) + logger.info("Tokenized proxy chain: ${tokenizedNames}") + + // Assert + assert tokenizedNames == NAMES + } + + @Test + void testShouldTokenizeProxiedEntitiesChainWithDNs() throws Exception { + // Arrange + final List DNS = [SAFE_USER_DN_JOHN, SAFE_USER_DN_PROXY_1, SAFE_USER_DN_PROXY_2] + final String RAW_PROXY_CHAIN = "<${DNS.join("><")}>" + logger.info(" Provided proxy chain: ${RAW_PROXY_CHAIN}") + + // Act + def tokenizedDns = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(RAW_PROXY_CHAIN) + logger.info("Tokenized proxy chain: ${tokenizedDns.collect { "\"${it}\"" }}") + + // Assert + assert tokenizedDns == DNS + } + + @Test + void testShouldTokenizeProxiedEntitiesChainWithAnonymousUser() throws Exception { + // Arrange + final List NAMES = ["", SAFE_USER_NAME_PROXY_1, SAFE_USER_NAME_PROXY_2] + final String RAW_PROXY_CHAIN = "<${NAMES.join("><")}>" + logger.info(" Provided proxy chain: ${RAW_PROXY_CHAIN}") + + // Act + def tokenizedNames = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(RAW_PROXY_CHAIN) + logger.info("Tokenized proxy chain: ${tokenizedNames}") + + // Assert + assert tokenizedNames == NAMES + } + + @Test + void testTokenizeProxiedEntitiesChainShouldHandleMaliciousUser() throws Exception { + // Arrange + final List NAMES = [MALICIOUS_USER_NAME_JOHN, SAFE_USER_NAME_PROXY_1, SAFE_USER_NAME_PROXY_2] + final String RAW_PROXY_CHAIN = "<${NAMES.collect { sanitizeDn(it) }.join("><")}>" + logger.info(" Provided proxy chain: ${RAW_PROXY_CHAIN}") + + // Act + def tokenizedNames = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(RAW_PROXY_CHAIN) + logger.info("Tokenized proxy chain: ${tokenizedNames.collect { "\"${it}\"" }}") + + // Assert + assert tokenizedNames == NAMES + assert tokenizedNames.size() == NAMES.size() + assert !tokenizedNames.contains(SAFE_USER_NAME_JOHN) + } + + @Test + void testTokenizeProxiedEntitiesChainShouldDecodeNonAsciiValues() throws Exception { + // Arrange + final String RAW_PROXY_CHAIN = "<${SAFE_USER_NAME_JOHN}><${UNICODE_DN_1_ENCODED}><${UNICODE_DN_2_ENCODED}>" + final List TOKENIZED_NAMES = [SAFE_USER_NAME_JOHN, UNICODE_DN_1, UNICODE_DN_2] + logger.info(" Provided proxy chain: ${RAW_PROXY_CHAIN}") + + // Act + def tokenizedNames = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(RAW_PROXY_CHAIN) + logger.info("Tokenized proxy chain: ${tokenizedNames.collect { "\"${it}\"" }}") + + // Assert + assert tokenizedNames == TOKENIZED_NAMES + assert tokenizedNames.size() == TOKENIZED_NAMES.size() + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-utils/src/test/groovy/org/apache/nifi/registry/security/util/kerberos/KerberosPrincipalParserSpec.groovy b/nifi-registry/nifi-registry-core/nifi-registry-security-utils/src/test/groovy/org/apache/nifi/registry/security/util/kerberos/KerberosPrincipalParserSpec.groovy new file mode 100644 index 0000000000..e96307b927 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-utils/src/test/groovy/org/apache/nifi/registry/security/util/kerberos/KerberosPrincipalParserSpec.groovy @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.util.kerberos + +import spock.lang.Specification +import spock.lang.Unroll + +class KerberosPrincipalParserSpec extends Specification { + + @Unroll + def "Verify parsed realm from '#testPrincipal' == '#expectedRealm'"() { + expect: + KerberosPrincipalParser.getRealm(testPrincipal) == expectedRealm + + where: + testPrincipal || expectedRealm + "user" || null + "user@" || null + "user@EXAMPLE.COM" || "EXAMPLE.COM" + "user@name@EXAMPLE.COM" || "EXAMPLE.COM" + "user\\@" || null + "user\\@name" || null + "user\\@name@EXAMPLE.COM" || "EXAMPLE.COM" + "user@EXAMPLE.COM\\@" || "EXAMPLE.COM\\@" + "user@@name@\\@@\\@" || "\\@" + "user@@name@\\@@\\@@EXAMPLE.COM" || "EXAMPLE.COM" + "user@@name@\\@@\\@@EXAMPLE.COM@" || null + "user\\@\\@name@EXAMPLE.COM" || "EXAMPLE.COM" + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-security-utils/src/test/java/org/apache/nifi/registry/security/util/KeyStoreUtilsTest.java b/nifi-registry/nifi-registry-core/nifi-registry-security-utils/src/test/java/org/apache/nifi/registry/security/util/KeyStoreUtilsTest.java new file mode 100644 index 0000000000..c0fb63c3be --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-security-utils/src/test/java/org/apache/nifi/registry/security/util/KeyStoreUtilsTest.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.util; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.Assert; +import org.junit.Test; + +import java.security.KeyStore; +import java.security.KeyStoreException; + +public class KeyStoreUtilsTest { + + @Test + public void testGetKeyStore() throws KeyStoreException { + for (final KeystoreType keystoreType : KeystoreType.values()) { + final KeyStore keyStore = KeyStoreUtils.getKeyStore(keystoreType.toString()); + Assert.assertNotNull(String.format("KeyStore not found for Keystore Type [%s]", keystoreType), keyStore); + Assert.assertEquals(keystoreType.name(), keyStore.getType()); + } + } + + @Test + public void testGetKeyStoreProviderNullType() { + final String keyStoreProvider = KeyStoreUtils.getKeyStoreProvider(null); + Assert.assertNull(keyStoreProvider); + } + + @Test + public void testGetKeyStoreProviderBouncyCastleProvider() { + final String keyStoreProvider = KeyStoreUtils.getKeyStoreProvider(KeystoreType.PKCS12.name()); + Assert.assertEquals(BouncyCastleProvider.PROVIDER_NAME, keyStoreProvider); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-test/pom.xml b/nifi-registry/nifi-registry-core/nifi-registry-test/pom.xml new file mode 100644 index 0000000000..3a6c015018 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-test/pom.xml @@ -0,0 +1,78 @@ + + + + 4.0.0 + + org.apache.nifi.registry + nifi-registry-core + 1.14.0-SNAPSHOT + + + nifi-registry-test + jar + + + + org.springframework.boot + spring-boot-starter-test + ${spring.boot.version} + + + org.testcontainers + testcontainers + ${testcontainers.version} + compile + + + org.testcontainers + mysql + ${testcontainers.version} + + + org.slf4j + jcl-over-slf4j + + + + + org.testcontainers + mariadb + ${testcontainers.version} + + + org.testcontainers + postgresql + ${testcontainers.version} + + + mysql + mysql-connector-java + 8.0.15 + + + org.mariadb.jdbc + mariadb-java-client + 2.4.1 + + + org.postgresql + postgresql + 42.2.19 + + + junit + junit + 4.13.1 + + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/DatabaseProfileValueSource.java b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/DatabaseProfileValueSource.java new file mode 100644 index 0000000000..1a9e5d9ba9 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/DatabaseProfileValueSource.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db; + +import org.springframework.test.annotation.ProfileValueSource; + +/** + * This {@link ProfileValueSource} offers a set of keys {@code current.database.is.} and + * {@code current.database.is.not.} where {@code } is a database as used in active profiles + * to enable integration tests to run with a certain database. The value returned for these keys is + * {@code "true"} or {@code "false"} depending on if the database is actually the one currently used by integration tests. + * + * @author Jens Schauder + */ +public class DatabaseProfileValueSource implements ProfileValueSource { + + private static final String MYSQL = "mysql"; + private static final String MARIADB = "mariadb"; + private static final String POSTGRES = "postgres"; + private static final String H2 = "h2"; + + private String currentDatabase; + + DatabaseProfileValueSource() { + final String activeProfiles = System.getProperty("spring.profiles.active", H2); + + if (activeProfiles.contains(H2)) { + currentDatabase = H2; + } else if (activeProfiles.contains(MYSQL)) { + currentDatabase = MYSQL; + } else if (activeProfiles.contains(MARIADB)) { + currentDatabase = MARIADB; + } else if (activeProfiles.contains(POSTGRES)) { + currentDatabase = POSTGRES; + } + } + + @Override + public String get(String key) { + if (!key.startsWith("current.database.is.")) { + return null; + } + if (key.startsWith("current.database.is.not.")) { + return Boolean.toString(!key.endsWith(currentDatabase)).toLowerCase(); + } + return Boolean.toString(key.endsWith(currentDatabase)).toLowerCase(); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/MariaDB10DataSourceFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/MariaDB10DataSourceFactory.java new file mode 100644 index 0000000000..140af67843 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/MariaDB10DataSourceFactory.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.testcontainers.containers.MariaDBContainer; + +@Configuration +@Profile("mariadb-10") +public class MariaDB10DataSourceFactory extends MariaDBDataSourceFactory { + + private static final MariaDBContainer MARIA_DB_CONTAINER = new MariaDBCustomContainer("mariadb:10.0"); + + static { + MARIA_DB_CONTAINER.start(); + } + + @Override + protected MariaDBContainer mariaDBContainer() { + return MARIA_DB_CONTAINER; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/MariaDB10_2DataSourceFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/MariaDB10_2DataSourceFactory.java new file mode 100644 index 0000000000..34a5bc2690 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/MariaDB10_2DataSourceFactory.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.testcontainers.containers.MariaDBContainer; + +@Configuration +@Profile("mariadb-10-2") +public class MariaDB10_2DataSourceFactory extends MariaDBDataSourceFactory { + + private static final MariaDBContainer MARIA_DB_CONTAINER = new MariaDBCustomContainer("mariadb:10.2"); + + static { + MARIA_DB_CONTAINER.start(); + } + + @Override + protected MariaDBContainer mariaDBContainer() { + return MARIA_DB_CONTAINER; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/MariaDB10_3DataSourceFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/MariaDB10_3DataSourceFactory.java new file mode 100644 index 0000000000..92389f1abc --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/MariaDB10_3DataSourceFactory.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.testcontainers.containers.MariaDBContainer; + +@Configuration +@Profile("mariadb-10-3") +public class MariaDB10_3DataSourceFactory extends MariaDBDataSourceFactory { + + private static final MariaDBContainer MARIA_DB_CONTAINER = new MariaDBCustomContainer("mariadb:10.3"); + + static { + MARIA_DB_CONTAINER.start(); + } + + @Override + protected MariaDBContainer mariaDBContainer() { + return MARIA_DB_CONTAINER; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/MariaDBCustomContainer.java b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/MariaDBCustomContainer.java new file mode 100644 index 0000000000..3a99312817 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/MariaDBCustomContainer.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db; + +import org.testcontainers.containers.MariaDBContainer; + +/** + * Custom container to override the JDBC URL and add additional query parameters. + * + * NOTE: At the time of implementing this, Flyway does not support some versions of MariaDB because the driver returns an unexpected DB name: + * + * https://github.com/flyway/flyway/issues/2339 + * + * The work around is to add the useMysqlMetadata=true to the URL. + */ +public class MariaDBCustomContainer extends MariaDBContainer { + + public MariaDBCustomContainer(String dockerImageName) { + super(dockerImageName); + } + + @Override + public String getJdbcUrl() { + return super.getJdbcUrl() + "?useMysqlMetadata=true"; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/MariaDBDataSourceFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/MariaDBDataSourceFactory.java new file mode 100644 index 0000000000..913394765a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/MariaDBDataSourceFactory.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db; + +import org.mariadb.jdbc.MariaDbDataSource; +import org.testcontainers.containers.MariaDBContainer; +import org.testcontainers.delegate.DatabaseDelegate; +import org.testcontainers.jdbc.JdbcDatabaseDelegate; + +import javax.annotation.PostConstruct; +import javax.script.ScriptException; +import javax.sql.DataSource; +import java.sql.SQLException; + +public abstract class MariaDBDataSourceFactory extends TestDataSourceFactory { + + protected abstract MariaDBContainer mariaDBContainer(); + + @Override + protected DataSource createDataSource() { + try { + final MariaDBContainer container = mariaDBContainer(); + final MariaDbDataSource dataSource = new MariaDbDataSource(); + dataSource.setUrl(container.getJdbcUrl()); + dataSource.setUser(container.getUsername()); + dataSource.setPassword(container.getPassword()); + dataSource.setDatabaseName(container.getDatabaseName()); + return dataSource; + } catch (SQLException e) { + throw new RuntimeException("Unable to create MariaDB DataSource", e); + } + } + + @PostConstruct + public void initDatabase() throws SQLException, ScriptException { + DatabaseDelegate databaseDelegate = new JdbcDatabaseDelegate(mariaDBContainer(), ""); + databaseDelegate.execute("DROP DATABASE test; CREATE DATABASE test;", "", 0, false, true); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/MySql6DataSourceFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/MySql6DataSourceFactory.java new file mode 100644 index 0000000000..69fc9875d1 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/MySql6DataSourceFactory.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.testcontainers.containers.MySQLContainer; + +@Configuration +@Profile("mysql-56") +public class MySql6DataSourceFactory extends MySqlDataSourceFactory { + + private static final MySQLContainer MYSQL_CONTAINER = new MySqlCustomContainer("mysql:5.6"); + + static { + MYSQL_CONTAINER.start(); + } + + @Override + protected MySQLContainer mysqlContainer() { + return MYSQL_CONTAINER; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/MySql7DataSourceFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/MySql7DataSourceFactory.java new file mode 100644 index 0000000000..d0a1fdeaf6 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/MySql7DataSourceFactory.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.testcontainers.containers.MySQLContainer; + +@Configuration +@Profile({"mysql", "mysql-57"}) +public class MySql7DataSourceFactory extends MySqlDataSourceFactory { + + private static final MySQLContainer MYSQL_CONTAINER = new MySqlCustomContainer("mysql:5.7"); + + static { + MYSQL_CONTAINER.start(); + } + + @Override + protected MySQLContainer mysqlContainer() { + return MYSQL_CONTAINER; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/MySql8DataSourceFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/MySql8DataSourceFactory.java new file mode 100644 index 0000000000..b069438cf0 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/MySql8DataSourceFactory.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.testcontainers.containers.MySQLContainer; + +@Configuration +@Profile({"mysql", "mysql-8"}) +public class MySql8DataSourceFactory extends MySqlDataSourceFactory { + + private static final MySQLContainer MYSQL_CONTAINER = new MySqlCustomContainer("mysql:8.0"); + + static { + MYSQL_CONTAINER.start(); + } + + @Override + protected MySQLContainer mysqlContainer() { + return MYSQL_CONTAINER; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/MySqlCustomContainer.java b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/MySqlCustomContainer.java new file mode 100644 index 0000000000..202a030e39 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/MySqlCustomContainer.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db; + +import org.testcontainers.containers.MySQLContainer; + +/** + * Custom container to override the JDBC URL and add additional query parameters. + * + * NOTE: At the time of implementing this, testcontainers could not start a container for MySQL 8, see this issue: + * + * https://github.com/testcontainers/testcontainers-java/issues/736 + * + * The work around is to add the allowPublicKeyRetrieval=true to the URL. + */ +public class MySqlCustomContainer extends MySQLContainer { + + public MySqlCustomContainer(String dockerImageName) { + super(dockerImageName); + } + + @Override + public String getJdbcUrl() { + return super.getJdbcUrl() + "?useSSL=false&allowPublicKeyRetrieval=true"; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/MySqlDataSourceFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/MySqlDataSourceFactory.java new file mode 100644 index 0000000000..3d1833fbe0 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/MySqlDataSourceFactory.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db; + +import com.mysql.cj.jdbc.MysqlDataSource; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.delegate.DatabaseDelegate; +import org.testcontainers.jdbc.JdbcDatabaseDelegate; + +import javax.annotation.PostConstruct; +import javax.script.ScriptException; +import javax.sql.DataSource; +import java.sql.SQLException; + +public abstract class MySqlDataSourceFactory extends TestDataSourceFactory { + + protected abstract MySQLContainer mysqlContainer(); + + @Override + protected DataSource createDataSource() { + final MySQLContainer container = mysqlContainer(); + final MysqlDataSource dataSource = new MysqlDataSource(); + dataSource.setUrl(container.getJdbcUrl()); + dataSource.setUser(container.getUsername()); + dataSource.setPassword(container.getPassword()); + dataSource.setDatabaseName(container.getDatabaseName()); + return dataSource; + } + + @PostConstruct + public void initDatabase() throws SQLException, ScriptException { + DatabaseDelegate databaseDelegate = new JdbcDatabaseDelegate(mysqlContainer(), ""); + databaseDelegate.execute("DROP DATABASE test; CREATE DATABASE test;", "", 0, false, true); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/Postgres10DataSourceFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/Postgres10DataSourceFactory.java new file mode 100644 index 0000000000..23d2f1d0eb --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/Postgres10DataSourceFactory.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db; + +import org.postgresql.ds.PGSimpleDataSource; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.delegate.DatabaseDelegate; +import org.testcontainers.jdbc.JdbcDatabaseDelegate; + +import javax.annotation.PostConstruct; +import javax.script.ScriptException; +import javax.sql.DataSource; +import java.sql.SQLException; + +@Configuration +@Profile("postgres-10") +public class Postgres10DataSourceFactory extends TestDataSourceFactory { + + private static final PostgreSQLContainer POSTGRESQL_CONTAINER = new PostgreSQLContainer("postgres:10"); + + static { + POSTGRESQL_CONTAINER.start(); + } + + @Override + protected DataSource createDataSource() { + PGSimpleDataSource dataSource = new PGSimpleDataSource(); + dataSource.setUrl(POSTGRESQL_CONTAINER.getJdbcUrl()); + dataSource.setUser(POSTGRESQL_CONTAINER.getUsername()); + dataSource.setPassword(POSTGRESQL_CONTAINER.getPassword()); + return dataSource; + } + + @PostConstruct + public void initDatabase() throws SQLException, ScriptException { + DatabaseDelegate databaseDelegate = new JdbcDatabaseDelegate(POSTGRESQL_CONTAINER, ""); + databaseDelegate.execute("DROP DATABASE test; CREATE DATABASE test;", "", 0, false, true); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/Postgres11DataSourceFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/Postgres11DataSourceFactory.java new file mode 100644 index 0000000000..ff66240983 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/Postgres11DataSourceFactory.java @@ -0,0 +1,54 @@ +package org.apache.nifi.registry.db;/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +import org.postgresql.ds.PGSimpleDataSource; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.delegate.DatabaseDelegate; +import org.testcontainers.jdbc.JdbcDatabaseDelegate; + +import javax.annotation.PostConstruct; +import javax.script.ScriptException; +import javax.sql.DataSource; +import java.sql.SQLException; + +@Configuration +@Profile("postgres-11") +public class Postgres11DataSourceFactory extends TestDataSourceFactory { + + private static final PostgreSQLContainer POSTGRESQL_CONTAINER = new PostgreSQLContainer("postgres:11"); + + static { + POSTGRESQL_CONTAINER.start(); + } + + @Override + protected DataSource createDataSource() { + PGSimpleDataSource dataSource = new PGSimpleDataSource(); + dataSource.setUrl(POSTGRESQL_CONTAINER.getJdbcUrl()); + dataSource.setUser(POSTGRESQL_CONTAINER.getUsername()); + dataSource.setPassword(POSTGRESQL_CONTAINER.getPassword()); + return dataSource; + } + + @PostConstruct + public void initDatabase() throws SQLException, ScriptException { + DatabaseDelegate databaseDelegate = new JdbcDatabaseDelegate(POSTGRESQL_CONTAINER, ""); + databaseDelegate.execute("DROP DATABASE test; CREATE DATABASE test;", "", 0, false, true); + } +} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/Postgres12DataSourceFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/Postgres12DataSourceFactory.java new file mode 100644 index 0000000000..c96c604137 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/Postgres12DataSourceFactory.java @@ -0,0 +1,54 @@ +package org.apache.nifi.registry.db;/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +import org.postgresql.ds.PGSimpleDataSource; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.delegate.DatabaseDelegate; +import org.testcontainers.jdbc.JdbcDatabaseDelegate; + +import javax.annotation.PostConstruct; +import javax.script.ScriptException; +import javax.sql.DataSource; +import java.sql.SQLException; + +@Configuration +@Profile("postgres-12") +public class Postgres12DataSourceFactory extends TestDataSourceFactory { + + private static final PostgreSQLContainer POSTGRESQL_CONTAINER = new PostgreSQLContainer("postgres:12"); + + static { + POSTGRESQL_CONTAINER.start(); + } + + @Override + protected DataSource createDataSource() { + PGSimpleDataSource dataSource = new PGSimpleDataSource(); + dataSource.setUrl(POSTGRESQL_CONTAINER.getJdbcUrl()); + dataSource.setUser(POSTGRESQL_CONTAINER.getUsername()); + dataSource.setPassword(POSTGRESQL_CONTAINER.getPassword()); + return dataSource; + } + + @PostConstruct + public void initDatabase() throws SQLException, ScriptException { + DatabaseDelegate databaseDelegate = new JdbcDatabaseDelegate(POSTGRESQL_CONTAINER, ""); + databaseDelegate.execute("DROP DATABASE test; CREATE DATABASE test;", "", 0, false, true); + } +} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/Postgres13DataSourceFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/Postgres13DataSourceFactory.java new file mode 100644 index 0000000000..77aef2f4b5 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/Postgres13DataSourceFactory.java @@ -0,0 +1,54 @@ +package org.apache.nifi.registry.db;/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +import org.postgresql.ds.PGSimpleDataSource; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.delegate.DatabaseDelegate; +import org.testcontainers.jdbc.JdbcDatabaseDelegate; + +import javax.annotation.PostConstruct; +import javax.script.ScriptException; +import javax.sql.DataSource; +import java.sql.SQLException; + +@Configuration +@Profile("postgres-13") +public class Postgres13DataSourceFactory extends TestDataSourceFactory { + + private static final PostgreSQLContainer POSTGRESQL_CONTAINER = new PostgreSQLContainer("postgres:13"); + + static { + POSTGRESQL_CONTAINER.start(); + } + + @Override + protected DataSource createDataSource() { + PGSimpleDataSource dataSource = new PGSimpleDataSource(); + dataSource.setUrl(POSTGRESQL_CONTAINER.getJdbcUrl()); + dataSource.setUser(POSTGRESQL_CONTAINER.getUsername()); + dataSource.setPassword(POSTGRESQL_CONTAINER.getPassword()); + return dataSource; + } + + @PostConstruct + public void initDatabase() throws SQLException, ScriptException { + DatabaseDelegate databaseDelegate = new JdbcDatabaseDelegate(POSTGRESQL_CONTAINER, ""); + databaseDelegate.execute("DROP DATABASE test; CREATE DATABASE test;", "", 0, false, true); + } +} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/PostgresDataSourceFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/PostgresDataSourceFactory.java new file mode 100644 index 0000000000..01fe05d789 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/PostgresDataSourceFactory.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db; + +import org.postgresql.ds.PGSimpleDataSource; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.delegate.DatabaseDelegate; +import org.testcontainers.jdbc.JdbcDatabaseDelegate; + +import javax.annotation.PostConstruct; +import javax.script.ScriptException; +import javax.sql.DataSource; +import java.sql.SQLException; + +@Configuration +@Profile("postgres") +public class PostgresDataSourceFactory extends TestDataSourceFactory { + + private static final PostgreSQLContainer POSTGRESQL_CONTAINER = new PostgreSQLContainer(); + + static { + POSTGRESQL_CONTAINER.start(); + } + + @Override + protected DataSource createDataSource() { + PGSimpleDataSource dataSource = new PGSimpleDataSource(); + dataSource.setUrl(POSTGRESQL_CONTAINER.getJdbcUrl()); + dataSource.setUser(POSTGRESQL_CONTAINER.getUsername()); + dataSource.setPassword(POSTGRESQL_CONTAINER.getPassword()); + return dataSource; + } + + @PostConstruct + public void initDatabase() throws SQLException, ScriptException { + DatabaseDelegate databaseDelegate = new JdbcDatabaseDelegate(POSTGRESQL_CONTAINER, ""); + databaseDelegate.execute("DROP DATABASE test; CREATE DATABASE test;", "", 0, false, true); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/TestDataSourceFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/TestDataSourceFactory.java new file mode 100644 index 0000000000..773c74bf9e --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-test/src/main/java/org/apache/nifi/registry/db/TestDataSourceFactory.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.db; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +import javax.sql.DataSource; + +@Configuration +public abstract class TestDataSourceFactory { + + @Bean + @Primary + public DataSource dataSource() { + return createDataSource(); + } + + protected abstract DataSource createDataSource(); + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-utils/pom.xml b/nifi-registry/nifi-registry-core/nifi-registry-utils/pom.xml new file mode 100644 index 0000000000..c5d8ca9629 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-utils/pom.xml @@ -0,0 +1,26 @@ + + + + 4.0.0 + + org.apache.nifi.registry + nifi-registry-core + 1.14.0-SNAPSHOT + + + nifi-registry-utils + jar + + + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/DataUnit.java b/nifi-registry/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/DataUnit.java new file mode 100644 index 0000000000..21aa9a7231 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/DataUnit.java @@ -0,0 +1,245 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.util; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public enum DataUnit { + + /** + * Bytes + */ + B { + @Override + public double toB(double value) { + return value; + } + + @Override + public double toKB(double value) { + return value / POWERS[1]; + } + + @Override + public double toMB(double value) { + return value / POWERS[2]; + } + + @Override + public double toGB(double value) { + return value / POWERS[3]; + } + + @Override + public double toTB(double value) { + return value / POWERS[4]; + } + + @Override + public double convert(double sourceSize, DataUnit sourceUnit) { + return sourceUnit.toB(sourceSize); + } + }, + /** + * Kilobytes + */ + KB { + @Override + public double toB(double value) { + return value * POWERS[1]; + } + + @Override + public double toKB(double value) { + return value; + } + + @Override + public double toMB(double value) { + return value / POWERS[1]; + } + + @Override + public double toGB(double value) { + return value / POWERS[2]; + } + + @Override + public double toTB(double value) { + return value / POWERS[3]; + } + + @Override + public double convert(double sourceSize, DataUnit sourceUnit) { + return sourceUnit.toKB(sourceSize); + } + }, + /** + * Megabytes + */ + MB { + @Override + public double toB(double value) { + return value * POWERS[2]; + } + + @Override + public double toKB(double value) { + return value * POWERS[1]; + } + + @Override + public double toMB(double value) { + return value; + } + + @Override + public double toGB(double value) { + return value / POWERS[1]; + } + + @Override + public double toTB(double value) { + return value / POWERS[2]; + } + + @Override + public double convert(double sourceSize, DataUnit sourceUnit) { + return sourceUnit.toMB(sourceSize); + } + }, + /** + * Gigabytes + */ + GB { + @Override + public double toB(double value) { + return value * POWERS[3]; + } + + @Override + public double toKB(double value) { + return value * POWERS[2]; + } + + @Override + public double toMB(double value) { + return value * POWERS[1]; + } + + @Override + public double toGB(double value) { + return value; + } + + @Override + public double toTB(double value) { + return value / POWERS[1]; + } + + @Override + public double convert(double sourceSize, DataUnit sourceUnit) { + return sourceUnit.toGB(sourceSize); + } + }, + /** + * Terabytes + */ + TB { + @Override + public double toB(double value) { + return value * POWERS[4]; + } + + @Override + public double toKB(double value) { + return value * POWERS[3]; + } + + @Override + public double toMB(double value) { + return value * POWERS[2]; + } + + @Override + public double toGB(double value) { + return value * POWERS[1]; + } + + @Override + public double toTB(double value) { + return value; + } + + @Override + public double convert(double sourceSize, DataUnit sourceUnit) { + return sourceUnit.toTB(sourceSize); + } + }; + + public double convert(final double sourceSize, final DataUnit sourceUnit) { + throw new AbstractMethodError(); + } + + public double toB(double size) { + throw new AbstractMethodError(); + } + + public double toKB(double size) { + throw new AbstractMethodError(); + } + + public double toMB(double size) { + throw new AbstractMethodError(); + } + + public double toGB(double size) { + throw new AbstractMethodError(); + } + + public double toTB(double size) { + throw new AbstractMethodError(); + } + + public static final double[] POWERS = {1, + 1024D, + 1024 * 1024D, + 1024 * 1024 * 1024D, + 1024 * 1024 * 1024 * 1024D}; + + public static final String DATA_SIZE_REGEX = "(\\d+(?:\\.\\d+)?)\\s*(B|KB|MB|GB|TB)"; + public static final Pattern DATA_SIZE_PATTERN = Pattern.compile(DATA_SIZE_REGEX); + + public static Double parseDataSize(final String value, final DataUnit units) { + if (value == null) { + return null; + } + + final Matcher matcher = DATA_SIZE_PATTERN.matcher(value.toUpperCase()); + if (!matcher.find()) { + throw new IllegalArgumentException("Invalid data size: " + value); + } + + final String sizeValue = matcher.group(1); + final String unitValue = matcher.group(2); + + final DataUnit sourceUnit = DataUnit.valueOf(unitValue); + final double size = Double.parseDouble(sizeValue); + return units.convert(size, sourceUnit); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/EscapeUtils.java b/nifi-registry/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/EscapeUtils.java new file mode 100644 index 0000000000..b42538ae3e --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/EscapeUtils.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.util; + +public class EscapeUtils { + + /** + * Escapes the specified html by replacing &, <, >, ", ', / with their corresponding html entity. If html is null, null is returned. + * + * @param html to escape + * @return escaped html + */ + public static String escapeHtml(String html) { + if (html == null) { + return null; + } + + html = html.replace("&", "&"); + html = html.replace("<", "<"); + html = html.replace(">", ">"); + html = html.replace("\"", """); + html = html.replace("'", "'"); + html = html.replace("/", "/"); + + return html; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/FileUtils.java b/nifi-registry/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/FileUtils.java new file mode 100644 index 0000000000..5abaf7e66b --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/FileUtils.java @@ -0,0 +1,426 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.util; + +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collection; + +import org.slf4j.Logger; + +/** + * A utility class containing a few useful static methods to do typical IO + * operations. + * + */ +public class FileUtils { + + public static final long TRANSFER_CHUNK_SIZE_BYTES = 1024 * 1024 * 8; //8 MB chunks + public static final long MILLIS_BETWEEN_ATTEMPTS = 50L; + + /** + * Closes the given closeable quietly - no logging, no exceptions... + * + * @param closeable the thing to close + */ + public static void closeQuietly(final Closeable closeable) { + if (null != closeable) { + try { + closeable.close(); + } catch (final IOException io) {/*IGNORE*/ + + } + } + } + + /** + * Releases the given lock quietly no logging, no exception + * + * @param lock the lock to release + */ + public static void releaseQuietly(final FileLock lock) { + if (null != lock) { + try { + lock.release(); + } catch (final IOException io) { + /*IGNORE*/ + } + } + } + + /* Superseded by renamed class bellow */ + @Deprecated + public static void ensureDirectoryExistAndCanAccess(final File dir) throws IOException { + ensureDirectoryExistAndCanReadAndWrite(dir); + } + + public static void ensureDirectoryExistAndCanReadAndWrite(final File dir) throws IOException { + if (dir.exists() && !dir.isDirectory()) { + throw new IOException(dir.getAbsolutePath() + " is not a directory"); + } else if (!dir.exists()) { + final boolean made = dir.mkdirs(); + if (!made) { + throw new IOException(dir.getAbsolutePath() + " could not be created"); + } + } + if (!(dir.canRead() && dir.canWrite())) { + throw new IOException(dir.getAbsolutePath() + " directory does not have read/write privilege"); + } + } + + public static void ensureDirectoryExistAndCanRead(final File dir) throws IOException { + if (dir.exists() && !dir.isDirectory()) { + throw new IOException(dir.getAbsolutePath() + " is not a directory"); + } else if (!dir.exists()) { + final boolean made = dir.mkdirs(); + if (!made) { + throw new IOException(dir.getAbsolutePath() + " could not be created"); + } + } + if (!dir.canRead()) { + throw new IOException(dir.getAbsolutePath() + " directory does not have read privilege"); + } + } + + /** + * Copies the given source file to the given destination file. The given destination will be overwritten if it already exists. + * + * @param source the file to copy + * @param destination the file to copy to + * @param lockInputFile if true will lock input file during copy; if false will not + * @param lockOutputFile if true will lock output file during copy; if false will not + * @param move if true will perform what is effectively a move operation rather than a pure copy. This allows for potentially highly efficient movement of the file but if not possible this will + * revert to a copy then delete behavior. If false, then the file is copied and the source file is retained. If a true rename/move occurs then no lock is held during that time. + * @param logger if failures occur, they will be logged to this logger if possible. If this logger is null, an IOException will instead be thrown, indicating the problem. + * @return long number of bytes copied + * @throws FileNotFoundException if the source file could not be found + * @throws IOException if unable to read or write the underlying streams + * @throws SecurityException if a security manager denies the needed file operations + */ + public static long copyFile(final File source, final File destination, final boolean lockInputFile, final boolean lockOutputFile, final boolean move, final Logger logger) + throws FileNotFoundException, IOException { + + FileInputStream fis = null; + FileOutputStream fos = null; + FileLock inLock = null; + FileLock outLock = null; + long fileSize = 0L; + if (!source.canRead()) { + throw new IOException("Must at least have read permission"); + + } + if (move && source.renameTo(destination)) { + fileSize = destination.length(); + } else { + try { + fis = new FileInputStream(source); + fos = new FileOutputStream(destination); + final FileChannel in = fis.getChannel(); + final FileChannel out = fos.getChannel(); + if (lockInputFile) { + inLock = in.tryLock(0, Long.MAX_VALUE, true); + if (null == inLock) { + throw new IOException("Unable to obtain shared file lock for: " + source.getAbsolutePath()); + } + } + if (lockOutputFile) { + outLock = out.tryLock(0, Long.MAX_VALUE, false); + if (null == outLock) { + throw new IOException("Unable to obtain exclusive file lock for: " + destination.getAbsolutePath()); + } + } + long bytesWritten = 0; + do { + bytesWritten += out.transferFrom(in, bytesWritten, TRANSFER_CHUNK_SIZE_BYTES); + fileSize = in.size(); + } while (bytesWritten < fileSize); + out.force(false); + FileUtils.closeQuietly(fos); + FileUtils.closeQuietly(fis); + fos = null; + fis = null; + if (move && !FileUtils.deleteFile(source, null, 5)) { + if (logger == null) { + FileUtils.deleteFile(destination, null, 5); + throw new IOException("Could not remove file " + source.getAbsolutePath()); + } else { + logger.warn("Configured to delete source file when renaming/move not successful. However, unable to delete file at: " + source.getAbsolutePath()); + } + } + } finally { + FileUtils.releaseQuietly(inLock); + FileUtils.releaseQuietly(outLock); + FileUtils.closeQuietly(fos); + FileUtils.closeQuietly(fis); + } + } + return fileSize; + } + + /** + * Copies the given source file to the given destination file. The given destination will be overwritten if it already exists. + * + * @param source the file to copy from + * @param destination the file to copy to + * @param lockInputFile if true will lock input file during copy; if false will not + * @param lockOutputFile if true will lock output file during copy; if false will not + * @param logger the logger to use + * @return long number of bytes copied + * @throws FileNotFoundException if the source file could not be found + * @throws IOException if unable to read or write to file + * @throws SecurityException if a security manager denies the needed file operations + */ + public static long copyFile(final File source, final File destination, final boolean lockInputFile, final boolean lockOutputFile, final Logger logger) throws FileNotFoundException, IOException { + return FileUtils.copyFile(source, destination, lockInputFile, lockOutputFile, false, logger); + } + + public static long copyFile(final File source, final OutputStream stream, final boolean closeOutputStream, final boolean lockInputFile) throws FileNotFoundException, IOException { + FileInputStream fis = null; + FileLock inLock = null; + long fileSize = 0L; + try { + fis = new FileInputStream(source); + final FileChannel in = fis.getChannel(); + if (lockInputFile) { + inLock = in.tryLock(0, Long.MAX_VALUE, true); + if (inLock == null) { + throw new IOException("Unable to obtain exclusive file lock for: " + source.getAbsolutePath()); + } + + } + + byte[] buffer = new byte[1 << 18]; //256 KB + int bytesRead = -1; + while ((bytesRead = fis.read(buffer)) != -1) { + stream.write(buffer, 0, bytesRead); + } + in.force(false); + stream.flush(); + fileSize = in.size(); + } finally { + FileUtils.releaseQuietly(inLock); + FileUtils.closeQuietly(fis); + if (closeOutputStream) { + FileUtils.closeQuietly(stream); + } + } + return fileSize; + } + + public static long copyFile(final InputStream stream, final File destination, final boolean closeInputStream, final boolean lockOutputFile) throws FileNotFoundException, IOException { + final Path destPath = destination.toPath(); + final long size = Files.copy(stream, destPath); + if (closeInputStream) { + stream.close(); + } + return size; + } + + /** + * Deletes the given file. If the given file exists but could not be deleted + * this will be printed as a warning to the given logger + * + * @param file to delete + * @param logger to notify + * @return true if deleted + */ + public static boolean deleteFile(final File file, final Logger logger) { + return FileUtils.deleteFile(file, logger, 1); + } + + /** + * Deletes the given file. If the given file exists but could not be deleted + * this will be printed as a warning to the given logger + * + * @param file to delete + * @param logger to notify + * @param attempts indicates how many times an attempt to delete should be + * made + * @return true if given file no longer exists + */ + public static boolean deleteFile(final File file, final Logger logger, final int attempts) { + if (file == null) { + return false; + } + boolean isGone = false; + try { + if (file.exists()) { + final int effectiveAttempts = Math.max(1, attempts); + for (int i = 0; i < effectiveAttempts && !isGone; i++) { + isGone = file.delete() || !file.exists(); + if (!isGone && (effectiveAttempts - i) > 1) { + FileUtils.sleepQuietly(MILLIS_BETWEEN_ATTEMPTS); + } + } + if (!isGone && logger != null) { + logger.warn("File appears to exist but unable to delete file: " + file.getAbsolutePath()); + } + } + } catch (final Throwable t) { + if (logger != null) { + logger.warn("Unable to delete file: '" + file.getAbsolutePath() + "' due to " + t); + } + } + return isGone; + } + + /** + * Deletes all files (not directories..) in the given directory (non + * recursive) that match the given filename filter. If any file cannot be + * deleted then this is printed at warn to the given logger. + * + * @param directory to delete contents of + * @param filter if null then no filter is used + * @param logger to notify + * @throws IOException if abstract pathname does not denote a directory, or + * if an I/O error occurs + */ + public static void deleteFilesInDirectory(final File directory, final FilenameFilter filter, final Logger logger) throws IOException { + FileUtils.deleteFilesInDirectory(directory, filter, logger, false); + } + + /** + * Deletes all files (not directories) in the given directory (recursive) + * that match the given filename filter. If any file cannot be deleted then + * this is printed at warn to the given logger. + * + * @param directory to delete contents of + * @param filter if null then no filter is used + * @param logger to notify + * @param recurse true if should recurse + * @throws IOException if abstract pathname does not denote a directory, or + * if an I/O error occurs + */ + public static void deleteFilesInDirectory(final File directory, final FilenameFilter filter, final Logger logger, final boolean recurse) throws IOException { + FileUtils.deleteFilesInDirectory(directory, filter, logger, recurse, false); + } + + /** + * Deletes all files (not directories) in the given directory (recursive) + * that match the given filename filter. If any file cannot be deleted then + * this is printed at warn to the given logger. + * + * @param directory to delete contents of + * @param filter if null then no filter is used + * @param logger to notify + * @param recurse will look for contents of sub directories. + * @param deleteEmptyDirectories default is false; if true will delete + * directories found that are empty + * @throws IOException if abstract pathname does not denote a directory, or + * if an I/O error occurs + */ + public static void deleteFilesInDirectory(final File directory, final FilenameFilter filter, final Logger logger, final boolean recurse, final boolean deleteEmptyDirectories) throws IOException { + // ensure the specified directory is actually a directory and that it exists + if (null != directory && directory.isDirectory()) { + final File ingestFiles[] = directory.listFiles(); + if (ingestFiles == null) { + // null if abstract pathname does not denote a directory, or if an I/O error occurs + throw new IOException("Unable to list directory content in: " + directory.getAbsolutePath()); + } + for (File ingestFile : ingestFiles) { + boolean process = (filter == null) ? true : filter.accept(directory, ingestFile.getName()); + if (ingestFile.isFile() && process) { + FileUtils.deleteFile(ingestFile, logger, 3); + } + if (ingestFile.isDirectory() && recurse) { + FileUtils.deleteFilesInDirectory(ingestFile, filter, logger, recurse, deleteEmptyDirectories); + if (deleteEmptyDirectories && ingestFile.list().length == 0) { + FileUtils.deleteFile(ingestFile, logger, 3); + } + } + } + } + } + + /** + * Deletes given files. + * + * @param files to delete + * @param recurse will recurse + * @throws IOException if issues deleting files + */ + public static void deleteFiles(final Collection files, final boolean recurse) throws IOException { + for (final File file : files) { + FileUtils.deleteFile(file, recurse); + } + } + + public static void deleteFile(final File file, final boolean recurse) throws IOException { + final File[] list = file.listFiles(); + if (file.isDirectory() && recurse && list != null) { + FileUtils.deleteFiles(Arrays.asList(list), recurse); + } + //now delete the file itself regardless of whether it is plain file or a directory + if (!FileUtils.deleteFile(file, null, 5)) { + throw new IOException("Unable to delete " + file.getAbsolutePath()); + } + } + + public static void sleepQuietly(final long millis) { + try { + Thread.sleep(millis); + } catch (final InterruptedException ex) { + /* do nothing */ + } + } + + + // The invalid character list is derived from this Stackoverflow page. + // https://stackoverflow.com/questions/1155107/is-there-a-cross-platform-java-method-to-remove-filename-special-chars + private final static int[] INVALID_CHARS = {34, 60, 62, 124, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 58, 42, 63, 92, 47, 32}; + + static { + Arrays.sort(INVALID_CHARS); + } + + /** + * Replaces invalid characters for a file system name within a given filename string to underscore '_'. + * Be careful not to pass a file path as this method replaces path delimiter characters (i.e forward/back slashes). + * @param filename The filename to clean + * @return sanitized filename + */ + public static String sanitizeFilename(String filename) { + if (filename == null || filename.isEmpty()) { + return filename; + } + int codePointCount = filename.codePointCount(0, filename.length()); + + final StringBuilder cleanName = new StringBuilder(); + for (int i = 0; i < codePointCount; i++) { + int c = filename.codePointAt(i); + if (Arrays.binarySearch(INVALID_CHARS, c) < 0) { + cleanName.appendCodePoint(c); + } else { + cleanName.append('_'); + } + } + return cleanName.toString(); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/FormatUtils.java b/nifi-registry/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/FormatUtils.java new file mode 100644 index 0000000000..aa207c5a04 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/FormatUtils.java @@ -0,0 +1,429 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.util; + +import java.text.NumberFormat; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class FormatUtils { + private static final String UNION = "|"; + + // for Data Sizes + private static final double BYTES_IN_KILOBYTE = 1024; + private static final double BYTES_IN_MEGABYTE = BYTES_IN_KILOBYTE * 1024; + private static final double BYTES_IN_GIGABYTE = BYTES_IN_MEGABYTE * 1024; + private static final double BYTES_IN_TERABYTE = BYTES_IN_GIGABYTE * 1024; + + // for Time Durations + private static final String NANOS = join(UNION, "ns", "nano", "nanos", "nanosecond", "nanoseconds"); + private static final String MILLIS = join(UNION, "ms", "milli", "millis", "millisecond", "milliseconds"); + private static final String SECS = join(UNION, "s", "sec", "secs", "second", "seconds"); + private static final String MINS = join(UNION, "m", "min", "mins", "minute", "minutes"); + private static final String HOURS = join(UNION, "h", "hr", "hrs", "hour", "hours"); + private static final String DAYS = join(UNION, "d", "day", "days"); + private static final String WEEKS = join(UNION, "w", "wk", "wks", "week", "weeks"); + + private static final String VALID_TIME_UNITS = join(UNION, NANOS, MILLIS, SECS, MINS, HOURS, DAYS, WEEKS); + public static final String TIME_DURATION_REGEX = "([\\d.]+)\\s*(" + VALID_TIME_UNITS + ")"; + public static final Pattern TIME_DURATION_PATTERN = Pattern.compile(TIME_DURATION_REGEX); + private static final List TIME_UNIT_MULTIPLIERS = Arrays.asList(1000L, 1000L, 1000L, 60L, 60L, 24L); + + /** + * Formats the specified count by adding commas. + * + * @param count the value to add commas to + * @return the string representation of the given value with commas included + */ + public static String formatCount(final long count) { + return NumberFormat.getIntegerInstance().format(count); + } + + /** + * Formats the specified duration in 'mm:ss.SSS' format. + * + * @param sourceDuration the duration to format + * @param sourceUnit the unit to interpret the duration + * @return representation of the given time data in minutes/seconds + */ + public static String formatMinutesSeconds(final long sourceDuration, final TimeUnit sourceUnit) { + final long millis = TimeUnit.MILLISECONDS.convert(sourceDuration, sourceUnit); + + final long millisInMinute = TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES); + final int minutes = (int) (millis / millisInMinute); + final long secondsMillisLeft = millis - minutes * millisInMinute; + + final long millisInSecond = TimeUnit.MILLISECONDS.convert(1, TimeUnit.SECONDS); + final int seconds = (int) (secondsMillisLeft / millisInSecond); + final long millisLeft = secondsMillisLeft - seconds * millisInSecond; + + return pad2Places(minutes) + ":" + pad2Places(seconds) + "." + pad3Places(millisLeft); + } + + /** + * Formats the specified duration in 'HH:mm:ss.SSS' format. + * + * @param sourceDuration the duration to format + * @param sourceUnit the unit to interpret the duration + * @return representation of the given time data in hours/minutes/seconds + */ + public static String formatHoursMinutesSeconds(final long sourceDuration, final TimeUnit sourceUnit) { + final long millis = TimeUnit.MILLISECONDS.convert(sourceDuration, sourceUnit); + + final long millisInHour = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS); + final int hours = (int) (millis / millisInHour); + final long minutesSecondsMillisLeft = millis - hours * millisInHour; + + return pad2Places(hours) + ":" + formatMinutesSeconds(minutesSecondsMillisLeft, TimeUnit.MILLISECONDS); + } + + private static String pad2Places(final long val) { + return (val < 10) ? "0" + val : String.valueOf(val); + } + + private static String pad3Places(final long val) { + return (val < 100) ? "0" + pad2Places(val) : String.valueOf(val); + } + + /** + * Formats the specified data size in human readable format. + * + * @param dataSize Data size in bytes + * @return Human readable format + */ + public static String formatDataSize(final double dataSize) { + // initialize the formatter + final NumberFormat format = NumberFormat.getNumberInstance(); + format.setMaximumFractionDigits(2); + + // check terabytes + double dataSizeToFormat = dataSize / BYTES_IN_TERABYTE; + if (dataSizeToFormat > 1) { + return format.format(dataSizeToFormat) + " TB"; + } + + // check gigabytes + dataSizeToFormat = dataSize / BYTES_IN_GIGABYTE; + if (dataSizeToFormat > 1) { + return format.format(dataSizeToFormat) + " GB"; + } + + // check megabytes + dataSizeToFormat = dataSize / BYTES_IN_MEGABYTE; + if (dataSizeToFormat > 1) { + return format.format(dataSizeToFormat) + " MB"; + } + + // check kilobytes + dataSizeToFormat = dataSize / BYTES_IN_KILOBYTE; + if (dataSizeToFormat > 1) { + return format.format(dataSizeToFormat) + " KB"; + } + + // default to bytes + return format.format(dataSize) + " bytes"; + } + + /** + * Returns a time duration in the requested {@link TimeUnit} after parsing the {@code String} + * input. If the resulting value is a decimal (i.e. + * {@code 25 hours -> TimeUnit.DAYS = 1.04}), the value is rounded. + * + * @param value the raw String input (i.e. "28 minutes") + * @param desiredUnit the requested output {@link TimeUnit} + * @return the whole number value of this duration in the requested units + * @deprecated As of Apache NiFi 1.9.0, because this method only returns whole numbers, use {@link #getPreciseTimeDuration(String, TimeUnit)} when possible. + */ + @Deprecated + public static long getTimeDuration(final String value, final TimeUnit desiredUnit) { + return Math.round(getPreciseTimeDuration(value, desiredUnit)); + } + + /** + * Returns the parsed and converted input in the requested units. + *

+ * If the value is {@code 0 <= x < 1} in the provided units, the units will first be converted to a smaller unit to get a value >= 1 (i.e. 0.5 seconds -> 500 milliseconds). + * This is because the underlying unit conversion cannot handle decimal values. + *

+ * If the value is {@code x >= 1} but x is not a whole number, the units will first be converted to a smaller unit to attempt to get a whole number value (i.e. 1.5 seconds -> 1500 milliseconds). + *

+ * If the value is {@code x < 1000} and the units are {@code TimeUnit.NANOSECONDS}, the result will be a whole number of nanoseconds, rounded (i.e. 123.4 ns -> 123 ns). + *

+ * This method handles decimal values over {@code 1 ns}, but {@code < 1 ns} will return {@code 0} in any other unit. + *

+ * Examples: + *

+ * "10 seconds", {@code TimeUnit.MILLISECONDS} -> 10_000.0 + * "0.010 s", {@code TimeUnit.MILLISECONDS} -> 10.0 + * "0.010 s", {@code TimeUnit.SECONDS} -> 0.010 + * "0.010 ns", {@code TimeUnit.NANOSECONDS} -> 1 + * "0.010 ns", {@code TimeUnit.MICROSECONDS} -> 0 + * + * @param value the {@code String} input + * @param desiredUnit the desired output {@link TimeUnit} + * @return the parsed and converted amount (without a unit) + */ + public static double getPreciseTimeDuration(final String value, final TimeUnit desiredUnit) { + final Matcher matcher = TIME_DURATION_PATTERN.matcher(value.toLowerCase()); + if (!matcher.matches()) { + throw new IllegalArgumentException("Value '" + value + "' is not a valid time duration"); + } + + final String duration = matcher.group(1); + final String units = matcher.group(2); + + double durationVal = Double.parseDouble(duration); + TimeUnit specifiedTimeUnit; + + // The TimeUnit enum doesn't have a value for WEEKS, so handle this case independently + if (isWeek(units)) { + specifiedTimeUnit = TimeUnit.DAYS; + durationVal *= 7; + } else { + specifiedTimeUnit = determineTimeUnit(units); + } + + // The units are now guaranteed to be in DAYS or smaller + long durationLong; + if (durationVal == Math.rint(durationVal)) { + durationLong = Math.round(durationVal); + } else { + // Try reducing the size of the units to make the input a long + List wholeResults = makeWholeNumberTime(durationVal, specifiedTimeUnit); + durationLong = (long) wholeResults.get(0); + specifiedTimeUnit = (TimeUnit) wholeResults.get(1); + } + + return desiredUnit.convert(durationLong, specifiedTimeUnit); + } + + /** + * Converts the provided time duration value to one that can be represented as a whole number. + * Returns a {@code List} containing the new value as a {@code long} at index 0 and the + * {@link TimeUnit} at index 1. If the incoming value is already whole, it is returned as is. + * If the incoming value cannot be made whole, a whole approximation is returned. For values + * {@code >= 1 TimeUnit.NANOSECONDS}, the value is rounded (i.e. 123.4 ns -> 123 ns). + * For values {@code < 1 TimeUnit.NANOSECONDS}, the constant [1L, {@code TimeUnit.NANOSECONDS}] is returned as the smallest measurable unit of time. + *

+ * Examples: + *

+ * 1, {@code TimeUnit.SECONDS} -> [1, {@code TimeUnit.SECONDS}] + * 1.1, {@code TimeUnit.SECONDS} -> [1100, {@code TimeUnit.MILLISECONDS}] + * 0.1, {@code TimeUnit.SECONDS} -> [100, {@code TimeUnit.MILLISECONDS}] + * 0.1, {@code TimeUnit.NANOSECONDS} -> [1, {@code TimeUnit.NANOSECONDS}] + * + * @param decimal the time duration as a decimal + * @param timeUnit the current time unit + * @return the time duration as a whole number ({@code long}) and the smaller time unit used + */ + protected static List makeWholeNumberTime(double decimal, TimeUnit timeUnit) { + // If the value is already a whole number, return it and the current time unit + if (decimal == Math.rint(decimal)) { + return Arrays.asList(new Object[]{(long) decimal, timeUnit}); + } else if (TimeUnit.NANOSECONDS == timeUnit) { + // The time unit is as small as possible + if (decimal < 1.0) { + decimal = 1; + } else { + decimal = Math.rint(decimal); + } + return Arrays.asList(new Object[]{(long) decimal, timeUnit}); + } else { + // Determine the next time unit and the respective multiplier + TimeUnit smallerTimeUnit = getSmallerTimeUnit(timeUnit); + long multiplier = calculateMultiplier(timeUnit, smallerTimeUnit); + + // Recurse with the original number converted to the smaller unit + return makeWholeNumberTime(decimal * multiplier, smallerTimeUnit); + } + } + + /** + * Returns the numerical multiplier to convert a value from {@code originalTimeUnit} to + * {@code newTimeUnit} (i.e. for {@code TimeUnit.DAYS -> TimeUnit.MINUTES} would return + * 24 * 60 = 1440). If the original and new units are the same, returns 1. If the new unit + * is larger than the original (i.e. the result would be less than 1), throws an + * {@link IllegalArgumentException}. + * + * @param originalTimeUnit the source time unit + * @param newTimeUnit the destination time unit + * @return the numerical multiplier between the units + */ + protected static long calculateMultiplier(TimeUnit originalTimeUnit, TimeUnit newTimeUnit) { + if (originalTimeUnit == newTimeUnit) { + return 1; + } else if (originalTimeUnit.ordinal() < newTimeUnit.ordinal()) { + throw new IllegalArgumentException("The original time unit '" + originalTimeUnit + "' must be larger than the new time unit '" + newTimeUnit + "'"); + } else { + int originalOrd = originalTimeUnit.ordinal(); + int newOrd = newTimeUnit.ordinal(); + + List unitMultipliers = TIME_UNIT_MULTIPLIERS.subList(newOrd, originalOrd); + return unitMultipliers.stream().reduce(1L, (a, b) -> (long) a * b); + } + } + + /** + * Returns the next smallest {@link TimeUnit} (i.e. {@code TimeUnit.DAYS -> TimeUnit.HOURS}). + * If the parameter is {@code null} or {@code TimeUnit.NANOSECONDS}, an + * {@link IllegalArgumentException} is thrown because there is no valid smaller TimeUnit. + * + * @param originalUnit the TimeUnit + * @return the next smaller TimeUnit + */ + protected static TimeUnit getSmallerTimeUnit(TimeUnit originalUnit) { + if (originalUnit == null || TimeUnit.NANOSECONDS == originalUnit) { + throw new IllegalArgumentException("Cannot determine a smaller time unit than '" + originalUnit + "'"); + } else { + return TimeUnit.values()[originalUnit.ordinal() - 1]; + } + } + + /** + * Returns {@code true} if this raw unit {@code String} is parsed as representing "weeks", which does not have a value in the {@link TimeUnit} enum. + * + * @param rawUnit the String containing the desired unit + * @return true if the unit is "weeks"; false otherwise + */ + protected static boolean isWeek(final String rawUnit) { + switch (rawUnit) { + case "w": + case "wk": + case "wks": + case "week": + case "weeks": + return true; + default: + return false; + } + } + + /** + * Returns the {@link TimeUnit} enum that maps to the provided raw {@code String} input. The + * highest time unit is {@code TimeUnit.DAYS}. Any input that cannot be parsed will result in + * an {@link IllegalArgumentException}. + * + * @param rawUnit the String to parse + * @return the TimeUnit + */ + protected static TimeUnit determineTimeUnit(String rawUnit) { + switch (rawUnit.toLowerCase()) { + case "ns": + case "nano": + case "nanos": + case "nanoseconds": + return TimeUnit.NANOSECONDS; + case "µs": + case "micro": + case "micros": + case "microseconds": + return TimeUnit.MICROSECONDS; + case "ms": + case "milli": + case "millis": + case "milliseconds": + return TimeUnit.MILLISECONDS; + case "s": + case "sec": + case "secs": + case "second": + case "seconds": + return TimeUnit.SECONDS; + case "m": + case "min": + case "mins": + case "minute": + case "minutes": + return TimeUnit.MINUTES; + case "h": + case "hr": + case "hrs": + case "hour": + case "hours": + return TimeUnit.HOURS; + case "d": + case "day": + case "days": + return TimeUnit.DAYS; + default: + throw new IllegalArgumentException("Could not parse '" + rawUnit + "' to TimeUnit"); + } + } + + public static String formatUtilization(final double utilization) { + return utilization + "%"; + } + + private static String join(final String delimiter, final String... values) { + if (values.length == 0) { + return ""; + } else if (values.length == 1) { + return values[0]; + } + + final StringBuilder sb = new StringBuilder(); + sb.append(values[0]); + for (int i = 1; i < values.length; i++) { + sb.append(delimiter).append(values[i]); + } + + return sb.toString(); + } + + /** + * Formats nanoseconds in the format: + * 3 seconds, 8 millis, 3 nanos - if includeTotalNanos = false, + * 3 seconds, 8 millis, 3 nanos (3008000003 nanos) - if includeTotalNanos = true + * + * @param nanos the number of nanoseconds to format + * @param includeTotalNanos whether or not to include the total number of nanoseconds in parentheses in the returned value + * @return a human-readable String that is a formatted representation of the given number of nanoseconds. + */ + public static String formatNanos(final long nanos, final boolean includeTotalNanos) { + final StringBuilder sb = new StringBuilder(); + + final long seconds = nanos >= 1000000000L ? nanos / 1000000000L : 0L; + long millis = nanos >= 1000000L ? nanos / 1000000L : 0L; + final long nanosLeft = nanos % 1000000L; + + if (seconds > 0) { + sb.append(seconds).append(" seconds"); + } + if (millis > 0) { + if (seconds > 0) { + sb.append(", "); + millis -= seconds * 1000L; + } + + sb.append(millis).append(" millis"); + } + if (seconds > 0 || millis > 0) { + sb.append(", "); + } + sb.append(nanosLeft).append(" nanos"); + + if (includeTotalNanos) { + sb.append(" (").append(nanos).append(" nanos)"); + } + + return sb.toString(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/PropertyValue.java b/nifi-registry/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/PropertyValue.java new file mode 100644 index 0000000000..49507728bf --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/PropertyValue.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.util; + +import java.util.concurrent.TimeUnit; + +/** + *

+ * A PropertyValue provides a mechanism whereby the currently configured value + * can be obtained in different forms. + *

+ */ +public interface PropertyValue { + + /** + * @return the raw property value as a string + */ + String getValue(); + + /** + * @return an integer representation of the property value, or + * null if not set + * @throws NumberFormatException if not able to parse + */ + Integer asInteger(); + + /** + * @return a Long representation of the property value, or null + * if not set + * @throws NumberFormatException if not able to parse + */ + Long asLong(); + + /** + * @return a Boolean representation of the property value, or + * null if not set + */ + Boolean asBoolean(); + + /** + * @return a Float representation of the property value, or + * null if not set + * @throws NumberFormatException if not able to parse + */ + Float asFloat(); + + /** + * @return a Double representation of the property value, of + * null if not set + * @throws NumberFormatException if not able to parse + */ + Double asDouble(); + + /** + * @param timeUnit specifies the TimeUnit to convert the time duration into + * @return a Long value representing the value of the configured time period + * in terms of the specified TimeUnit; if the property is not set, returns + * null + */ + Long asTimePeriod(TimeUnit timeUnit); + + /** + * + * @param dataUnit specifies the DataUnit to convert the data size into + * @return a Long value representing the value of the configured data size + * in terms of the specified DataUnit; if hte property is not set, returns + * null + */ + Double asDataSize(DataUnit dataUnit); + + /** + * @return true if the user has configured a value, or if the + * PropertyDescriptor for the associated property has a default + * value, false otherwise + */ + boolean isSet(); +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/StandardPropertyValue.java b/nifi-registry/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/StandardPropertyValue.java new file mode 100644 index 0000000000..b185fadf92 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/StandardPropertyValue.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.util; + +import java.util.concurrent.TimeUnit; + +public class StandardPropertyValue implements PropertyValue { + + private final String rawValue; + + public StandardPropertyValue(final String rawValue) { + this.rawValue = rawValue; + } + + @Override + public String getValue() { + return rawValue; + } + + @Override + public Integer asInteger() { + return (rawValue == null) ? null : Integer.parseInt(rawValue.trim()); + } + + @Override + public Long asLong() { + return (rawValue == null) ? null : Long.parseLong(rawValue.trim()); + } + + @Override + public Boolean asBoolean() { + return (rawValue == null) ? null : Boolean.parseBoolean(rawValue.trim()); + } + + @Override + public Float asFloat() { + return (rawValue == null) ? null : Float.parseFloat(rawValue.trim()); + } + + @Override + public Double asDouble() { + return (rawValue == null) ? null : Double.parseDouble(rawValue.trim()); + } + + @Override + public Long asTimePeriod(final TimeUnit timeUnit) { + return (rawValue == null) ? null : FormatUtils.getTimeDuration(rawValue.trim(), timeUnit); + } + + @Override + public Double asDataSize(final DataUnit dataUnit) { + return rawValue == null ? null : DataUnit.parseDataSize(rawValue.trim(), dataUnit); + } + + @Override + public boolean isSet() { + return rawValue != null; + } + + @Override + public String toString() { + return rawValue; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-utils/src/test/java/org/apache/nifi/registry/util/TestFileUtils.java b/nifi-registry/nifi-registry-core/nifi-registry-utils/src/test/java/org/apache/nifi/registry/util/TestFileUtils.java new file mode 100644 index 0000000000..d4bc9631be --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-utils/src/test/java/org/apache/nifi/registry/util/TestFileUtils.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.util; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class TestFileUtils { + @Test + public void testSanitizeFilename() { + String filename = "This / is / a test"; + final String sanitizedFilename = FileUtils.sanitizeFilename(filename); + assertEquals("This___is___a_test", sanitizedFilename); + } +} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/pom.xml b/nifi-registry/nifi-registry-core/nifi-registry-web-api/pom.xml new file mode 100644 index 0000000000..132ad93323 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/pom.xml @@ -0,0 +1,515 @@ + + + + + 4.0.0 + + org.apache.nifi.registry + nifi-registry-core + 1.14.0-SNAPSHOT + + nifi-registry-web-api + 1.14.0-SNAPSHOT + war + + + ${project.basedir}/src/main/resources/swagger + ${project.build.directory}/swagger + ${project.basedir}/src/main/asciidoc + ${project.build.directory}/asciidoc + ${project.build.directory}/${project.artifactId}-${project.version}/docs/ + + + + + + src/main/resources + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + maven-war-plugin + + false + + + + com.github.kongchen + swagger-maven-plugin + 3.1.6 + + + compile + + generate + + + + + + org.apache.nifi.registry.web.api + + + http + https + + /nifi-registry-api + + Apache NiFi Registry REST API + ${project.version} + + The REST API provides an interface to a registry with operations for saving, versioning, reading NiFi flows and components. + + + Apache NiFi Registry + dev@nifi.apache.org + https://nifi.apache.org + + + https://www.apache.org/licenses/LICENSE-2.0.html + Apache 2.0 License + + As described in the license + + + + ${swagger.source.dir}/security-definitions.json + + + classpath:/templates/index.html.hbs + ${docs.dir}/rest-api/index.html + ${swagger.generated.dir} + + + + + + + + maven-resources-plugin + + + copy-resources + validate + + copy-resources + + + ${docs.dir}/rest-api/images + + + src/main/resources/images + + + + + + + + com.googlecode.maven-download-plugin + download-maven-plugin + 1.2.1 + + + download-swagger-ui + + + wget + + + https://github.com/swagger-api/swagger-ui/archive/v${swagger.ui.version}.tar.gz + true + ${project.build.directory} + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.8 + + + bundle-swagger-ui + prepare-package + + run + + + + + Copy static Swagger UI files to target + + + + + + Disable schema validation by removing validatorUrl + + + + + Rename 'index.html' to 'ui.html' + + Replace default swagger.json location + + + + Copy swagger.json into static assets folder + + + + + + + + + + + + + + io.github.swagger2markup + swagger2markup-maven-plugin + 1.3.3 + + + compile + + convertSwagger2markup + + + ${swagger.generated.dir}/swagger.json + ${asciidoc.generated.dir} + + ASCIIDOC + TAGS + true + + + + + + + + org.asciidoctor + asciidoctor-maven-plugin + 1.5.7.1 + + ${asciidoc.source.dir} + rest-api.adoc + + article + + 3 + + + + + ${project.version} + Apache NiFi + ${asciidoc.generated.dir} + + + + + output-html + compile + + process-asciidoc + + + html5 + ${docs.dir}/rest-api + rest-api.html + + + + + + + + + + no-swagger-ui + + + + + com.googlecode.maven-download-plugin + download-maven-plugin + 1.2.1 + + + download-swagger-ui + none + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.8 + + + bundle-swagger-ui + none + + + + + + + + + jigsaw + + (1.8,) + + + + + jakarta.xml.bind + jakarta.xml.bind-api + provided + + + org.glassfish.jaxb + jaxb-runtime + provided + + + + + + + + org.springframework.boot + spring-boot-starter-web + ${spring.boot.version} + + + org.springframework.boot + spring-boot-starter-jersey + ${spring.boot.version} + + + + org.springframework + spring-aop + + + + javax.xml.bind + jaxb-api + + + + + + org.springframework.boot + spring-boot-starter-actuator + ${spring.boot.version} + + + io.micrometer + micrometer-core + + + + + org.springframework.security.kerberos + spring-security-kerberos-core + 1.0.1.RELEASE + + + org.springframework + spring-core + + + org.springframework.security + spring-security-core + + + + + + org.springframework.boot + spring-boot-starter-tomcat + ${spring.boot.version} + provided + + + org.apache.nifi.registry + nifi-registry-framework + 1.14.0-SNAPSHOT + + + org.apache.nifi.registry + nifi-registry-revision-spring-jdbc + 1.14.0-SNAPSHOT + + + org.apache.nifi.registry + nifi-registry-revision-entity-service + 1.14.0-SNAPSHOT + + + org.apache.nifi.registry + nifi-registry-properties + 1.14.0-SNAPSHOT + provided + + + org.apache.nifi.registry + nifi-registry-security-api + 1.14.0-SNAPSHOT + provided + + + org.apache.nifi.registry + nifi-registry-provider-api + 1.14.0-SNAPSHOT + provided + + + org.apache.nifi.registry + nifi-registry-security-utils + 1.14.0-SNAPSHOT + + + org.apache.commons + commons-lang3 + + + javax.servlet + javax.servlet-api + provided + + + io.swagger + swagger-annotations + + + org.glassfish.jersey.media + jersey-media-multipart + + + io.jsonwebtoken + jjwt + 0.7.0 + + + + org.apache.nifi.registry + nifi-registry-test + 1.14.0-SNAPSHOT + test + + + org.apache.nifi.registry + nifi-registry-client + 1.14.0-SNAPSHOT + test + + + org.springframework.boot + spring-boot-starter-jetty + ${spring.boot.version} + test + + + org.eclipse.jetty.websocket + websocket-server + + + org.eclipse.jetty.websocket + javax-websocket-server-impl + + + + + com.unboundid + unboundid-ldapsdk + 3.2.1 + test + + + org.spockframework + spock-core + test + + + org.codehaus.groovy + groovy-test + test + + + org.spockframework + spock-core + test + + + org.codehaus.groovy + * + + + + + cglib + cglib-nodep + 2.2.2 + test + + + org.eclipse.jetty + jetty-util + ${jetty.version} + test + + + com.nimbusds + oauth2-oidc-sdk + + + com.google.guava + guava + + + org.codehaus.groovy + groovy-json + 2.5.4 + test + + + org.codehaus.groovy + groovy + 2.5.4 + test + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/asciidoc/rest-api.adoc b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/asciidoc/rest-api.adoc new file mode 100644 index 0000000000..4430d836a8 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/asciidoc/rest-api.adoc @@ -0,0 +1,20 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You 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. + +include::{generated}/overview.adoc[] +include::{generated}/security.adoc[] +include::{generated}/paths.adoc[] +include::{generated}/definitions.adoc[] \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/NiFiRegistryApiApplication.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/NiFiRegistryApiApplication.java new file mode 100644 index 0000000000..2ffefbb5a9 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/NiFiRegistryApiApplication.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry; + +import org.apache.nifi.registry.event.EventService; +import org.apache.nifi.registry.event.StandardEvent; +import org.apache.nifi.registry.hook.Event; +import org.apache.nifi.registry.hook.EventType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +import java.util.Properties; + +/** + * Main class for starting the NiFi Registry Web API as a Spring Boot application. + * + * This class is purposely in the org.apache.nifi.registry package since that is the common base + * package across other modules. This is done because spring-boot will use the package of this + * class to automatically scan for beans/config/entities/etc. and would otherwise require + * configuring custom packages to scan in several different places. + * + * WebMvcAutoConfiguration is excluded because our web app is using Jersey in place of SpringMVC + */ +@SpringBootApplication +public class NiFiRegistryApiApplication extends SpringBootServletInitializer { + + public static final String NIFI_REGISTRY_PROPERTIES_ATTRIBUTE = "nifi-registry.properties"; + public static final String NIFI_REGISTRY_MASTER_KEY_ATTRIBUTE = "nifi-registry.key"; + + @Autowired + private EventService eventService; + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + final Properties defaultProperties = new Properties(); + + // Spring Boot 2.1.0 disabled bean overriding so this re-enables it + defaultProperties.setProperty("spring.main.allow-bean-definition-overriding", "true"); + + // Disable unnecessary Spring MVC filters that cause problems with Jersey + defaultProperties.setProperty("spring.mvc.hiddenmethod.filter.enabled", "false"); + defaultProperties.setProperty("spring.mvc.formcontent.filter.enabled", "false"); + + // Enable Actuator Endpoints + defaultProperties.setProperty("management.endpoints.web.expose", "*"); + + // Run Jersey as a filter instead of a servlet so that requests can be forwarded to other handlers (e.g., actuator) + defaultProperties.setProperty("spring.jersey.type", "filter"); + + return application + .sources(NiFiRegistryApiApplication.class) + .properties(defaultProperties); + } + + @Component + private class OnApplicationReadyEventing + implements ApplicationListener { + + @Override + public void onApplicationEvent(final ApplicationReadyEvent event) { + Event registryStartEvent = new StandardEvent.Builder() + .eventType(EventType.REGISTRY_START) + .build(); + eventService.publish(registryStartEvent); + } + } + + public static void main(String[] args) { + SpringApplication.run(NiFiRegistryApiApplication.class, args); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/NiFiRegistryPropertiesFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/NiFiRegistryPropertiesFactory.java new file mode 100644 index 0000000000..b7865bb76b --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/NiFiRegistryPropertiesFactory.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry; + +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.context.ServletContextAware; + +import javax.servlet.ServletContext; + +/** + * The JettyServer puts an instance of NiFiRegistryProperties into the ServletContext, this class + * obtains that instance and makes it available to inject to all other places. + * + */ +@Configuration +public class NiFiRegistryPropertiesFactory implements ServletContextAware { + + private NiFiRegistryProperties properties; + + @Override + public void setServletContext(ServletContext servletContext) { + properties = (NiFiRegistryProperties) servletContext.getAttribute( + NiFiRegistryApiApplication.NIFI_REGISTRY_PROPERTIES_ATTRIBUTE); + } + + @Bean + public NiFiRegistryProperties getNiFiRegistryProperties() { + return properties; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistryResourceConfig.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistryResourceConfig.java new file mode 100644 index 0000000000..d033ef286a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistryResourceConfig.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web; + +import org.apache.nifi.registry.web.api.AccessPolicyResource; +import org.apache.nifi.registry.web.api.AccessResource; +import org.apache.nifi.registry.web.api.BucketBundleResource; +import org.apache.nifi.registry.web.api.BucketFlowResource; +import org.apache.nifi.registry.web.api.BucketResource; +import org.apache.nifi.registry.web.api.ConfigResource; +import org.apache.nifi.registry.web.api.ExtensionRepoResource; +import org.apache.nifi.registry.web.api.BundleResource; +import org.apache.nifi.registry.web.api.ExtensionResource; +import org.apache.nifi.registry.web.api.FlowResource; +import org.apache.nifi.registry.web.api.ItemResource; +import org.apache.nifi.registry.web.api.TenantResource; +import org.glassfish.jersey.media.multipart.MultiPartFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.ServerProperties; +import org.glassfish.jersey.server.filter.HttpMethodOverrideFilter; +import org.glassfish.jersey.servlet.ServletProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Configuration; + +import javax.servlet.ServletContext; +import javax.ws.rs.core.Context; + +/** + * This is the main Jersey configuration for the application. + * + * NOTE: Don't set @ApplicationPath here because it has already been set to 'nifi-registry-api' in JettyServer + */ +@Configuration +public class NiFiRegistryResourceConfig extends ResourceConfig { + + private static final Logger logger = LoggerFactory.getLogger(NiFiRegistryResourceConfig.class); + + public NiFiRegistryResourceConfig(@Context ServletContext servletContext) { + // register filters + register(HttpMethodOverrideFilter.class); + + // register the exception mappers & jackson object mapper resolver + packages("org.apache.nifi.registry.web.mapper"); + + // register endpoints + register(AccessPolicyResource.class); + register(AccessResource.class); + register(BucketResource.class); + register(BucketFlowResource.class); + register(BucketBundleResource.class); + register(BundleResource.class); + register(ExtensionResource.class); + register(ExtensionRepoResource.class); + register(FlowResource.class); + register(ItemResource.class); + register(TenantResource.class); + register(ConfigResource.class); + + // register multipart feature + register(MultiPartFeature.class); + + // include bean validation errors in response + property(ServerProperties.BV_SEND_ERROR_IN_RESPONSE, true); + + // this is necessary for the /access/token/kerberos endpoint to work correctly + // when sending 401 Unauthorized with a WWW-Authenticate: Negotiate header. + // if this value needs to be changed, kerberos authentication needs to move to filter chain + // so it can directly set the HttpServletResponse instead of indirectly through a JAX-RS Response + property(ServerProperties.RESPONSE_SET_STATUS_OVER_SEND_ERROR, true); + + // configure jersey to ignore resource paths for actuator and swagger-ui + property(ServletProperties.FILTER_STATIC_CONTENT_REGEX, "/(actuator|swagger/).*"); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessPolicyResource.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessPolicyResource.java new file mode 100644 index 0000000000..32508071f9 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessPolicyResource.java @@ -0,0 +1,341 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.api; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import io.swagger.annotations.Authorization; +import io.swagger.annotations.Extension; +import io.swagger.annotations.ExtensionProperty; +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.authorization.AccessPolicy; +import org.apache.nifi.registry.authorization.AccessPolicySummary; +import org.apache.nifi.registry.authorization.Resource; +import org.apache.nifi.registry.event.EventService; +import org.apache.nifi.registry.revision.entity.RevisionInfo; +import org.apache.nifi.registry.revision.web.ClientIdParameter; +import org.apache.nifi.registry.revision.web.LongParameter; +import org.apache.nifi.registry.security.authorization.RequestAction; +import org.apache.nifi.registry.web.service.ServiceFacade; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.net.URI; +import java.util.Collections; +import java.util.List; + +/** + * RESTful endpoint for managing access policies. + */ +@Component +@Path("/policies") +@Api( + value = "policies", + description = "Endpoint for managing access policies.", + authorizations = { @Authorization("Authorization") } +) +public class AccessPolicyResource extends ApplicationResource { + + @Autowired + public AccessPolicyResource(final ServiceFacade serviceFacade, final EventService eventService) { + super(serviceFacade, eventService); + } + + /** + * Create a new access policy. + * + * @param httpServletRequest request + * @param requestAccessPolicy the access policy to create. + * @return The created access policy. + */ + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Create access policy", + response = AccessPolicy.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "write"), + @ExtensionProperty(name = "resource", value = "/policies") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry might not be configured to use a ConfigurableAccessPolicyProvider.") }) + public Response createAccessPolicy( + @Context final HttpServletRequest httpServletRequest, + @ApiParam(value = "The access policy configuration details.", required = true) + final AccessPolicy requestAccessPolicy) { + + AccessPolicy createdPolicy = serviceFacade.createAccessPolicy(requestAccessPolicy); + String locationUri = generateAccessPolicyUri(createdPolicy); + return generateCreatedResponse(URI.create(locationUri), createdPolicy).build(); + } + + /** + * Retrieves all access policies + * + * @return A list of access policies + */ + @GET + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get all access policies", + response = AccessPolicy.class, + responseContainer = "List", + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/policies") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getAccessPolicies() { + List accessPolicies = serviceFacade.getAccessPolicies(); + if (accessPolicies == null) { + accessPolicies = Collections.emptyList(); + } + + return generateOkResponse(accessPolicies).build(); + } + + /** + * Retrieves the specified access policy. + * + * @param identifier The id of the access policy to retrieve + * @return An accessPolicyEntity. + */ + @GET + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @Path("{id}") + @ApiOperation( + value = "Get access policy", + response = AccessPolicy.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/policies") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getAccessPolicy( + @ApiParam(value = "The access policy id.", required = true) + @PathParam("id") final String identifier) { + final AccessPolicy accessPolicy = serviceFacade.getAccessPolicy(identifier); + return generateOkResponse(accessPolicy).build(); + } + + + /** + * Retrieve a specified access policy for a given (action, resource) pair. + * + * @param action the action, i.e. "read", "write" + * @param rawResource the name of the resource as a raw string + * @return An access policy. + */ + @GET + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @Path("{action}/{resource: .+}") + @ApiOperation( + value = "Get access policy for resource", + notes = "Gets an access policy for the specified action and resource", + response = AccessPolicy.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/policies") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getAccessPolicyForResource( + @ApiParam(value = "The request action.", allowableValues = "read, write, delete", required = true) + @PathParam("action") + final String action, + @ApiParam(value = "The resource of the policy.", required = true) + @PathParam("resource") + final String rawResource) { + + // parse the action and resource type + final RequestAction requestAction = RequestAction.valueOfValue(action); + final String resource = "/" + rawResource; + + final AccessPolicy accessPolicy = serviceFacade.getAccessPolicy(resource, requestAction); + return generateOkResponse(accessPolicy).build(); + } + + + /** + * Update an access policy. + * + * @param httpServletRequest request + * @param identifier The id of the access policy to update. + * @param requestAccessPolicy An access policy. + * @return the updated access policy. + */ + @PUT + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Path("{id}") + @ApiOperation( + value = "Update access policy", + response = AccessPolicy.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "write"), + @ExtensionProperty(name = "resource", value = "/policies") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry might not be configured to use a ConfigurableAccessPolicyProvider.") }) + public Response updateAccessPolicy( + @Context + final HttpServletRequest httpServletRequest, + @ApiParam(value = "The access policy id.", required = true) + @PathParam("id") + final String identifier, + @ApiParam(value = "The access policy configuration details.", required = true) + final AccessPolicy requestAccessPolicy) { + + if (requestAccessPolicy == null) { + throw new IllegalArgumentException("Access policy details must be specified when updating a policy."); + } + if (!identifier.equals(requestAccessPolicy.getIdentifier())) { + throw new IllegalArgumentException(String.format("The policy id in the request body (%s) does not equal the " + + "policy id of the requested resource (%s).", requestAccessPolicy.getIdentifier(), identifier)); + } + + final AccessPolicy createdPolicy = serviceFacade.updateAccessPolicy(requestAccessPolicy); + return generateOkResponse(createdPolicy).build(); + } + + + /** + * Remove a specified access policy. + * + * @param httpServletRequest request + * @param identifier The id of the access policy to remove. + * @return The deleted access policy + */ + @DELETE + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @Path("{id}") + @ApiOperation( + value = "Delete access policy", + response = AccessPolicy.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "delete"), + @ExtensionProperty(name = "resource", value = "/policies") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry might not be configured to use a ConfigurableAccessPolicyProvider.") }) + public Response removeAccessPolicy( + @Context + final HttpServletRequest httpServletRequest, + @ApiParam(value = "The version is used to verify the client is working with the latest version of the entity.", required = true) + @QueryParam(VERSION) + final LongParameter version, + @ApiParam(value = "If the client id is not specified, new one will be generated. This value (whether specified or generated) is included in the response.") + @QueryParam(CLIENT_ID) + @DefaultValue(StringUtils.EMPTY) + final ClientIdParameter clientId, + @ApiParam(value = "The access policy id.", required = true) + @PathParam("id") + final String identifier) { + + final RevisionInfo revisionInfo = getRevisionInfo(version, clientId); + final AccessPolicy deletedPolicy = serviceFacade.deleteAccessPolicy(identifier, revisionInfo); + return generateOkResponse(deletedPolicy).build(); + } + + /** + * Gets the available resources that support access/authorization policies. + * + * @return A resourcesEntity. + */ + @GET + @Path("/resources") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get available resources", + notes = "Gets the available resources that support access/authorization policies", + response = Resource.class, + responseContainer = "List", + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/policies") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403) }) + public Response getResources() { + final List resources = serviceFacade.getResources(); + return generateOkResponse(resources).build(); + } + + private String generateAccessPolicyUri(final AccessPolicySummary accessPolicy) { + return generateResourceUri("policies", accessPolicy.getIdentifier()); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessResource.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessResource.java new file mode 100644 index 0000000000..3c5db2670a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessResource.java @@ -0,0 +1,835 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.api; + +import com.nimbusds.oauth2.sdk.AuthorizationCode; +import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant; +import com.nimbusds.oauth2.sdk.AuthorizationGrant; +import com.nimbusds.oauth2.sdk.ParseException; +import com.nimbusds.oauth2.sdk.id.State; +import com.nimbusds.openid.connect.sdk.AuthenticationErrorResponse; +import com.nimbusds.openid.connect.sdk.AuthenticationResponseParser; +import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse; +import io.jsonwebtoken.JwtException; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import io.swagger.annotations.Authorization; +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.authorization.CurrentUser; +import org.apache.nifi.registry.event.EventService; +import org.apache.nifi.registry.exception.AdministrationException; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.apache.nifi.registry.security.authentication.AuthenticationRequest; +import org.apache.nifi.registry.security.authentication.AuthenticationResponse; +import org.apache.nifi.registry.security.authentication.BasicAuthIdentityProvider; +import org.apache.nifi.registry.security.authentication.IdentityProvider; +import org.apache.nifi.registry.security.authentication.IdentityProviderUsage; +import org.apache.nifi.registry.security.authentication.exception.IdentityAccessException; +import org.apache.nifi.registry.security.authentication.exception.InvalidCredentialsException; +import org.apache.nifi.registry.security.authorization.user.NiFiUser; +import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils; +import org.apache.nifi.registry.web.exception.UnauthorizedException; +import org.apache.nifi.registry.web.security.authentication.jwt.JwtService; +import org.apache.nifi.registry.web.security.authentication.kerberos.KerberosSpnegoIdentityProvider; +import org.apache.nifi.registry.web.security.authentication.oidc.OidcService; +import org.apache.nifi.registry.web.security.authentication.x509.X509IdentityProvider; +import org.apache.nifi.registry.web.service.ServiceFacade; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; + +import javax.servlet.ServletContext; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Collectors; + +@Component +@Path("/access") +@Api( + value = "access", + description = "Endpoints for obtaining an access token or checking access status." +) +public class AccessResource extends ApplicationResource { + + private static final Logger logger = LoggerFactory.getLogger(AccessResource.class); + + private static final String OIDC_REQUEST_IDENTIFIER = "oidc-request-identifier"; + private static final String OIDC_ERROR_TITLE = "Unable to continue login sequence"; + + private NiFiRegistryProperties properties; + private JwtService jwtService; + private OidcService oidcService; + private X509IdentityProvider x509IdentityProvider; + private KerberosSpnegoIdentityProvider kerberosSpnegoIdentityProvider; + private IdentityProvider identityProvider; + + @Context + protected UriInfo uriInfo; + + @Autowired + public AccessResource( + NiFiRegistryProperties properties, + JwtService jwtService, + X509IdentityProvider x509IdentityProvider, + OidcService oidcService, + @Nullable KerberosSpnegoIdentityProvider kerberosSpnegoIdentityProvider, + @Nullable IdentityProvider identityProvider, + ServiceFacade serviceFacade, + EventService eventService) { + super(serviceFacade, eventService); + this.properties = properties; + this.jwtService = jwtService; + this.x509IdentityProvider = x509IdentityProvider; + this.oidcService = oidcService; + this.kerberosSpnegoIdentityProvider = kerberosSpnegoIdentityProvider; + this.identityProvider = identityProvider; + } + + /** + * Gets the current client's identity and authorized permissions. + * + * @param httpServletRequest the servlet request + * @return An object describing the current client identity, as determined by the server, and it's permissions. + */ + @GET + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get access status", + notes = "Returns the current client's authenticated identity and permissions to top-level resources", + response = CurrentUser.class, + authorizations = {@Authorization(value = "Authorization")} + ) + @ApiResponses({ + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry might be running unsecured.") }) + public Response getAccessStatus(@Context HttpServletRequest httpServletRequest) { + + final NiFiUser user = NiFiUserUtils.getNiFiUser(); + if (user == null) { + // Not expected to happen unless the nifi registry server has been seriously misconfigured. + throw new WebApplicationException(new Throwable("Unable to access details for current user.")); + } + + final CurrentUser currentUser = serviceFacade.getCurrentUser(); + currentUser.setLoginSupported(isBasicLoginSupported(httpServletRequest)); + currentUser.setOIDCLoginSupported(isOIDCLoginSupported(httpServletRequest)); + + return generateOkResponse(currentUser).build(); + } + + /** + * Creates a token for accessing the REST API. + * + * @param httpServletRequest the servlet request + * @return A JWT (string) + */ + @POST + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.TEXT_PLAIN) + @Path("/token") + @ApiOperation( + value = "Create token trying all providers", + notes = "Creates a token for accessing the REST API via auto-detected method of verifying client identity claim credentials. " + + "The token returned is formatted as a JSON Web Token (JWT). The token is base64 encoded and comprised of three parts. The header, " + + "the body, and the signature. The expiration of the token is a contained within the body. The token can be used in the Authorization header " + + "in the format 'Authorization: Bearer '.", + response = String.class + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry may not be configured to support login with username/password."), + @ApiResponse(code = 500, message = HttpStatusMessages.MESSAGE_500) }) + public Response createAccessTokenByTryingAllProviders(@Context HttpServletRequest httpServletRequest) { + + // only support access tokens when communicating over HTTPS + if (!httpServletRequest.isSecure()) { + throw new IllegalStateException("Access tokens are only issued over HTTPS"); + } + + List identityProviderWaterfall = generateIdentityProviderWaterfall(); + + String token = null; + for (IdentityProvider provider : identityProviderWaterfall) { + + AuthenticationRequest authenticationRequest = provider.extractCredentials(httpServletRequest); + if (authenticationRequest == null) { + continue; + } + try { + token = createAccessToken(provider, authenticationRequest); + break; + } catch (final InvalidCredentialsException ice){ + logger.debug("{}: the supplied client credentials are invalid.", provider.getClass().getSimpleName()); + logger.debug("", ice); + } + + } + + if (StringUtils.isEmpty(token)) { + List acceptableAuthTypes = identityProviderWaterfall.stream() + .map(IdentityProvider::getUsageInstructions) + .map(IdentityProviderUsage::getAuthType) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + + throw new UnauthorizedException("Client credentials are missing or invalid according to all configured identity providers.") + .withAuthenticateChallenge(acceptableAuthTypes); + } + + // build the response + final URI uri = URI.create(generateResourceUri("access", "token")); + return generateCreatedResponse(uri, token).build(); + } + + /** + * Creates a token for accessing the REST API. + * + * @param httpServletRequest the servlet request + * @return A JWT (string) + */ + @POST + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.TEXT_PLAIN) + @Path("/token/login") + @ApiOperation( + value = "Create token using basic auth", + notes = "Creates a token for accessing the REST API via username/password. The user credentials must be passed in standard HTTP Basic Auth format. " + + "That is: 'Authorization: Basic ', where is the base64 encoded value of ':'. " + + "The token returned is formatted as a JSON Web Token (JWT). The token is base64 encoded and comprised of three parts. The header, " + + "the body, and the signature. The expiration of the token is a contained within the body. The token can be used in the Authorization header " + + "in the format 'Authorization: Bearer '.", + response = String.class, + authorizations = { @Authorization("BasicAuth") } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry may not be configured to support login with username/password."), + @ApiResponse(code = 500, message = HttpStatusMessages.MESSAGE_500) }) + public Response createAccessTokenUsingBasicAuthCredentials(@Context HttpServletRequest httpServletRequest) { + + // only support access tokens when communicating over HTTPS + if (!httpServletRequest.isSecure()) { + throw new IllegalStateException("Access tokens are only issued over HTTPS"); + } + + // if not configured with custom identity provider, or if provider doesn't support HTTP Basic Auth, don't consider credentials + if (identityProvider == null) { + logger.debug("An Identity Provider must be configured to use this endpoint. Please consult the administration guide."); + throw new IllegalStateException("Username/Password login not supported by this NiFi. Contact System Administrator."); + } + if (!(identityProvider instanceof BasicAuthIdentityProvider)) { + logger.debug("An Identity Provider is configured, but it does not support HTTP Basic Auth authentication. " + + "The configured Identity Provider must extend {}", BasicAuthIdentityProvider.class); + throw new IllegalStateException("Username/Password login not supported by this NiFi. Contact System Administrator."); + } + + // generate JWT for response + AuthenticationRequest authenticationRequest = identityProvider.extractCredentials(httpServletRequest); + + if (authenticationRequest == null) { + throw new UnauthorizedException("The client credentials are missing from the request.") + .withAuthenticateChallenge(IdentityProviderUsage.AuthType.OTHER); + } + + final String token; + try { + token = createAccessToken(identityProvider, authenticationRequest); + } catch (final InvalidCredentialsException ice){ + throw new UnauthorizedException("The supplied client credentials are not valid.", ice) + .withAuthenticateChallenge(IdentityProviderUsage.AuthType.OTHER); + } + + // form the response + final URI uri = URI.create(generateResourceUri("access", "token")); + return generateCreatedResponse(uri, token).build(); + } + + @DELETE + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.WILDCARD) + @Path("/logout") + @ApiOperation( + value = "Performs a logout for other providers that have been issued a JWT.", + notes = NON_GUARANTEED_ENDPOINT + ) + @ApiResponses( + value = { + @ApiResponse(code = 200, message = "User was logged out successfully."), + @ApiResponse(code = 401, message = "Authentication token provided was empty or not in the correct JWT format."), + @ApiResponse(code = 500, message = "Client failed to log out."), + } + ) + public Response logOut(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) { + if (!httpServletRequest.isSecure()) { + throw new IllegalStateException("User authentication/authorization is only supported when running over HTTPS."); + } + + String userIdentity = NiFiUserUtils.getNiFiUserIdentity(); + + if(userIdentity != null && !userIdentity.isEmpty()) { + try { + logger.info("Logging out user " + userIdentity); + jwtService.logOut(userIdentity); + return generateOkResponse().build(); + } catch (final JwtException e) { + logger.error("Logout of user " + userIdentity + " failed due to: " + e.getMessage()); + return Response.serverError().build(); + } + } else { + return Response.status(401, "Authentication token provided was empty or not in the correct JWT format.").build(); + } + } + + @POST + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.TEXT_PLAIN) + @Path("/token/kerberos") + @ApiOperation( + value = "Create token using kerberos", + notes = "Creates a token for accessing the REST API via Kerberos Service Tickets or SPNEGO Tokens (which includes Kerberos Service Tickets). " + + "The token returned is formatted as a JSON Web Token (JWT). The token is base64 encoded and comprised of three parts. The header, " + + "the body, and the signature. The expiration of the token is a contained within the body. The token can be used in the Authorization header " + + "in the format 'Authorization: Bearer '.", + response = String.class + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry may not be configured to support login Kerberos credentials."), + @ApiResponse(code = 500, message = HttpStatusMessages.MESSAGE_500) }) + public Response createAccessTokenUsingKerberosTicket(@Context HttpServletRequest httpServletRequest) { + + // only support access tokens when communicating over HTTPS + if (!httpServletRequest.isSecure()) { + throw new IllegalStateException("Access tokens are only issued over HTTPS"); + } + + // if not configured with custom identity provider, don't consider credentials + if (!properties.isKerberosSpnegoSupportEnabled() || kerberosSpnegoIdentityProvider == null) { + throw new IllegalStateException("Kerberos service ticket login not supported by this NiFi Registry"); + } + + AuthenticationRequest authenticationRequest = kerberosSpnegoIdentityProvider.extractCredentials(httpServletRequest); + + if (authenticationRequest == null) { + throw new UnauthorizedException("The client credentials are missing from the request.") + .withAuthenticateChallenge(kerberosSpnegoIdentityProvider.getUsageInstructions().getAuthType()); + } + + final String token; + try { + token = createAccessToken(kerberosSpnegoIdentityProvider, authenticationRequest); + } catch (final InvalidCredentialsException ice){ + throw new UnauthorizedException("The supplied client credentials are not valid.", ice) + .withAuthenticateChallenge(kerberosSpnegoIdentityProvider.getUsageInstructions().getAuthType()); + } + + // build the response + final URI uri = URI.create(generateResourceUri("access", "token")); + return generateCreatedResponse(uri, token).build(); + + } + + /** + * Creates a token for accessing the REST API using a custom identity provider configured using NiFi Registry extensions. + * + * @param httpServletRequest the servlet request + * @return A JWT (string) + */ + @POST + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.TEXT_PLAIN) + @Path("/token/identity-provider") + @ApiOperation( + value = "Create token using identity provider", + notes = "Creates a token for accessing the REST API via a custom identity provider. " + + "The user credentials must be passed in a format understood by the custom identity provider, e.g., a third-party auth token in an HTTP header. " + + "The exact format of the user credentials expected by the custom identity provider can be discovered by 'GET /access/token/identity-provider/usage'. " + + "The token returned is formatted as a JSON Web Token (JWT). The token is base64 encoded and comprised of three parts. The header, " + + "the body, and the signature. The expiration of the token is a contained within the body. The token can be used in the Authorization header " + + "in the format 'Authorization: Bearer '.", + response = String.class + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry may not be configured to support login with customized credentials."), + @ApiResponse(code = 500, message = HttpStatusMessages.MESSAGE_500) }) + public Response createAccessTokenUsingIdentityProviderCredentials(@Context HttpServletRequest httpServletRequest) { + + // only support access tokens when communicating over HTTPS + if (!httpServletRequest.isSecure()) { + throw new IllegalStateException("Access tokens are only issued over HTTPS"); + } + + // if not configured with custom identity provider, don't consider credentials + if (identityProvider == null) { + throw new IllegalStateException("Custom login not supported by this NiFi Registry"); + } + + AuthenticationRequest authenticationRequest = identityProvider.extractCredentials(httpServletRequest); + + if (authenticationRequest == null) { + throw new UnauthorizedException("The client credentials are missing from the request.") + .withAuthenticateChallenge(identityProvider.getUsageInstructions().getAuthType()); + } + + final String token; + try { + token = createAccessToken(identityProvider, authenticationRequest); + } catch (InvalidCredentialsException ice) { + throw new UnauthorizedException("The supplied client credentials are not valid.", ice) + .withAuthenticateChallenge(identityProvider.getUsageInstructions().getAuthType()); + } + + // build the response + final URI uri = URI.create(generateResourceUri("access", "token")); + return generateCreatedResponse(uri, token).build(); + + } + + /** + * Creates a token for accessing the REST API using a custom identity provider configured using NiFi Registry extensions. + * + * @param httpServletRequest the servlet request + * @return A JWT (string) + */ + @GET + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.TEXT_PLAIN) + @Path("/token/identity-provider/usage") + @ApiOperation( + value = "Get identity provider usage", + notes = "Provides a description of how the currently configured identity provider expects credentials to be passed to POST /access/token/identity-provider", + response = String.class + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry may not be configured to support login with customized credentials."), + @ApiResponse(code = 500, message = HttpStatusMessages.MESSAGE_500) }) + public Response getIdentityProviderUsageInstructions(@Context HttpServletRequest httpServletRequest) { + + // if not configuration for login, don't consider credentials + if (identityProvider == null) { + throw new IllegalStateException("Custom login not supported by this NiFi Registry"); + } + + Class ipClazz = identityProvider.getClass(); + String identityProviderName = StringUtils.isNotEmpty(ipClazz.getSimpleName()) ? ipClazz.getSimpleName() : ipClazz.getName(); + + try { + String usageInstructions = "Usage Instructions for '" + identityProviderName + "': "; + usageInstructions += identityProvider.getUsageInstructions().getText(); + return generateOkResponse(usageInstructions).build(); + + } catch (Exception e) { + // If, for any reason, this identity provider does not support getUsageInstructions(), e.g., returns null or throws NotImplementedException. + return Response.status(Response.Status.NOT_IMPLEMENTED) + .entity("The currently configured identity provider, '" + identityProvider.getClass().getName() + "' does not provide usage instructions.") + .build(); + } + + } + + /** + * Creates a token for accessing the REST API using a custom identity provider configured using NiFi Registry extensions. + * + * @param httpServletRequest the servlet request + * @return A JWT (string) + */ + @POST + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.TEXT_PLAIN) + @Path("/token/identity-provider/test") + @ApiOperation( + value = "Test identity provider", + notes = "Tests the format of the credentials against this identity provider without preforming authentication on the credentials to validate them. " + + "The user credentials should be passed in a format understood by the custom identity provider as defined by 'GET /access/token/identity-provider/usage'.", + response = String.class + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = "The format of the credentials were not recognized by the currently configured identity provider."), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry may not be configured to support login with customized credentials."), + @ApiResponse(code = 500, message = HttpStatusMessages.MESSAGE_500) }) + public Response testIdentityProviderRecognizesCredentialsFormat(@Context HttpServletRequest httpServletRequest) { + + // only support access tokens when communicating over HTTPS + if (!httpServletRequest.isSecure()) { + throw new IllegalStateException("Access tokens are only issued over HTTPS"); + } + + // if not configured with custom identity provider, don't consider credentials + if (identityProvider == null) { + throw new IllegalStateException("Custom login not supported by this NiFi Registry"); + } + + final Class ipClazz = identityProvider.getClass(); + final String identityProviderName = StringUtils.isNotEmpty(ipClazz.getSimpleName()) ? ipClazz.getSimpleName() : ipClazz.getName(); + + // attempt to extract client credentials without authenticating them + AuthenticationRequest authenticationRequest = identityProvider.extractCredentials(httpServletRequest); + + if (authenticationRequest == null) { + throw new UnauthorizedException("The format of the credentials were not recognized by the currently configured identity provider " + + "'" + identityProviderName + "'. " + identityProvider.getUsageInstructions().getText()) + .withAuthenticateChallenge(identityProvider.getUsageInstructions().getAuthType()); + } + + + final String successMessage = identityProviderName + " recognized the format of the credentials in the HTTP request."; + return generateOkResponse(successMessage).build(); + + } + + @GET + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.WILDCARD) + @Path("/oidc/request") + @ApiOperation( + value = "Initiates a request to authenticate through the configured OpenId Connect provider.", + notes = NON_GUARANTEED_ENDPOINT + ) + public void oidcRequest(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) throws Exception { + // only consider user specific access over https + if (!httpServletRequest.isSecure()) { + //forwardToMessagePage(httpServletRequest, httpServletResponse, "User authentication/authorization is only supported when running over HTTPS."); + throw new IllegalStateException("User authentication/authorization is only supported when running over HTTPS."); + } + + // ensure oidc is enabled + if (!oidcService.isOidcEnabled()) { + //forwardToMessagePage(httpServletRequest, httpServletResponse, "OpenId Connect is not configured."); + throw new IllegalStateException("OpenId Connect is not configured."); + } + + final String oidcRequestIdentifier = UUID.randomUUID().toString(); + + // generate a cookie to associate this login sequence + final Cookie cookie = new Cookie(OIDC_REQUEST_IDENTIFIER, oidcRequestIdentifier); + cookie.setPath("/"); + cookie.setHttpOnly(true); + cookie.setMaxAge(60); + cookie.setSecure(true); + httpServletResponse.addCookie(cookie); + + // get the state for this request + final State state = oidcService.createState(oidcRequestIdentifier); + + // build the authorization uri + final URI authorizationUri = UriBuilder.fromUri(oidcService.getAuthorizationEndpoint()) + .queryParam("client_id", oidcService.getClientId()) + .queryParam("response_type", "code") + .queryParam("scope", oidcService.getScope().toString()) + .queryParam("state", state.getValue()) + .queryParam("redirect_uri", getOidcCallback()) + .build(); + + // generate the response + httpServletResponse.sendRedirect(authorizationUri.toString()); + } + + @GET + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.WILDCARD) + @Path("/oidc/callback") + @ApiOperation( + value = "Redirect/callback URI for processing the result of the OpenId Connect login sequence.", + notes = NON_GUARANTEED_ENDPOINT + ) + public void oidcCallback(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) throws Exception { + // only consider user specific access over https + if (!httpServletRequest.isSecure()) { + //forwardToMessagePage(httpServletRequest, httpServletResponse, "User authentication/authorization is only supported when running over HTTPS."); + throw new IllegalStateException("User authentication/authorization is only supported when running over HTTPS."); + } + + // ensure oidc is enabled + if (!oidcService.isOidcEnabled()) { + //forwardToMessagePage(httpServletRequest, httpServletResponse, "OpenId Connect is not configured."); + throw new IllegalStateException("OpenId Connect is not configured."); + } + + final String oidcRequestIdentifier = getCookieValue(httpServletRequest.getCookies(), OIDC_REQUEST_IDENTIFIER); + if (oidcRequestIdentifier == null) { + throw new IllegalStateException("The login request identifier was not found in the request. Unable to continue."); + } + + final com.nimbusds.openid.connect.sdk.AuthenticationResponse oidcResponse; + try { + oidcResponse = AuthenticationResponseParser.parse(getRequestUri()); + } catch (final ParseException e) { + logger.error("Unable to parse the redirect URI from the OpenId Connect Provider. Unable to continue login process."); + + // remove the oidc request cookie + removeOidcRequestCookie(httpServletResponse); + + // forward to the error page + throw new IllegalStateException("Unable to parse the redirect URI from the OpenId Connect Provider. Unable to continue login process."); + } + + if (oidcResponse.indicatesSuccess()) { + final AuthenticationSuccessResponse successfulOidcResponse = (AuthenticationSuccessResponse) oidcResponse; + + // confirm state + final State state = successfulOidcResponse.getState(); + if (state == null || !oidcService.isStateValid(oidcRequestIdentifier, state)) { + logger.error("The state value returned by the OpenId Connect Provider does not match the stored state. Unable to continue login process."); + + // remove the oidc request cookie + removeOidcRequestCookie(httpServletResponse); + + // forward to the error page + throw new IllegalStateException("Purposed state does not match the stored state. Unable to continue login process."); + } + + try { + // exchange authorization code for id token + final AuthorizationCode authorizationCode = successfulOidcResponse.getAuthorizationCode(); + final AuthorizationGrant authorizationGrant = new AuthorizationCodeGrant(authorizationCode, URI.create(getOidcCallback())); + oidcService.exchangeAuthorizationCode(oidcRequestIdentifier, authorizationGrant); + } catch (final Exception e) { + logger.error("Unable to exchange authorization for ID token: " + e.getMessage(), e); + + // remove the oidc request cookie + removeOidcRequestCookie(httpServletResponse); + + // forward to the error page + throw new IllegalStateException("Unable to exchange authorization for ID token: " + e.getMessage()); + } + + // redirect to the name page + httpServletResponse.sendRedirect(getNiFiRegistryUri()); + } else { + // remove the oidc request cookie + removeOidcRequestCookie(httpServletResponse); + + // report the unsuccessful login + final AuthenticationErrorResponse errorOidcResponse = (AuthenticationErrorResponse) oidcResponse; + throw new IllegalStateException("Unsuccessful login attempt: " + errorOidcResponse.getErrorObject().getDescription()); + } + } + + @POST + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.TEXT_PLAIN) + @Path("/oidc/exchange") + @ApiOperation( + value = "Retrieves a JWT following a successful login sequence using the configured OpenId Connect provider.", + response = String.class, + notes = NON_GUARANTEED_ENDPOINT + ) + public Response oidcExchange(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) throws Exception { + // only consider user specific access over https + if (!httpServletRequest.isSecure()) { + throw new IllegalStateException("User authentication/authorization is only supported when running over HTTPS."); + } + + // ensure oidc is enabled + if (!oidcService.isOidcEnabled()) { + throw new IllegalStateException("OpenId Connect is not configured."); + } + + final String oidcRequestIdentifier = getCookieValue(httpServletRequest.getCookies(), OIDC_REQUEST_IDENTIFIER); + if (oidcRequestIdentifier == null) { + throw new IllegalArgumentException("The login request identifier was not found in the request. Unable to continue."); + } + + // remove the oidc request cookie + removeOidcRequestCookie(httpServletResponse); + + // get the jwt + final String jwt = oidcService.getJwt(oidcRequestIdentifier); + if (jwt == null) { + throw new IllegalArgumentException("A JWT for this login request identifier could not be found. Unable to continue."); + } + + // generate the response + return generateOkResponse(jwt).build(); + } + + @DELETE + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.WILDCARD) + @Path("/oidc/logout") + @ApiOperation( + value = "Performs a logout in the OpenId Provider.", + notes = NON_GUARANTEED_ENDPOINT + ) + public void oidcLogout(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) throws Exception { + if (!httpServletRequest.isSecure()) { + throw new IllegalStateException("User authentication/authorization is only supported when running over HTTPS."); + } + + if (!oidcService.isOidcEnabled()) { + throw new IllegalStateException("OpenId Connect is not configured."); + } + + final String tokenHeader = httpServletRequest.getHeader(JwtService.AUTHORIZATION); + jwtService.logOutUsingAuthHeader(tokenHeader); + + URI endSessionEndpoint = oidcService.getEndSessionEndpoint(); + String postLogoutRedirectUri = generateResourceUri("..", "nifi-registry"); + + if (endSessionEndpoint == null) { + // handle the case, where the OpenID Provider does not have an end session endpoint + //httpServletResponse.sendRedirect(postLogoutRedirectUri); + } else { + URI logoutUri = UriBuilder.fromUri(endSessionEndpoint) + .queryParam("post_logout_redirect_uri", postLogoutRedirectUri) + .build(); + httpServletResponse.sendRedirect(logoutUri.toString()); + } + } + + /** + * Gets the value of a cookie matching the specified name. If no cookie with that name exists, null is returned. + * + * @param cookies the cookies + * @param name the name of the cookie + * @return the value of the corresponding cookie, or null if the cookie does not exist + */ + private String getCookieValue(final Cookie[] cookies, final String name) { + if (cookies != null) { + for (final Cookie cookie : cookies) { + if (name.equals(cookie.getName())) { + return cookie.getValue(); + } + } + } + + return null; + } + + public void setOidcService(OidcService oidcService) { + this.oidcService = oidcService; + } + + private String getOidcCallback() { + return generateResourceUri("access", "oidc", "callback"); + } + + private void removeOidcRequestCookie(final HttpServletResponse httpServletResponse) { + final Cookie cookie = new Cookie(OIDC_REQUEST_IDENTIFIER, null); + cookie.setPath("/"); + cookie.setHttpOnly(true); + cookie.setMaxAge(0); + cookie.setSecure(true); + httpServletResponse.addCookie(cookie); + } + + protected URI getRequestUri() { + return uriInfo.getRequestUri(); + } + + private String getNiFiRegistryUri() { + final String nifiRegistryApiUrl = generateResourceUri(); + final String baseUrl = StringUtils.substringBeforeLast(nifiRegistryApiUrl, "/nifi-registry-api"); + return baseUrl + "/nifi-registry"; + } + + private void forwardToMessagePage(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse, final String message) throws Exception { + httpServletRequest.setAttribute("title", OIDC_ERROR_TITLE); + httpServletRequest.setAttribute("messages", message); + + final ServletContext uiContext = httpServletRequest.getServletContext().getContext("/nifi-registry"); + uiContext.getRequestDispatcher("/WEB-INF/pages/message-page.jsp").forward(httpServletRequest, httpServletResponse); + } + + private String createAccessToken(IdentityProvider identityProvider, AuthenticationRequest authenticationRequest) + throws InvalidCredentialsException, AdministrationException { + + final AuthenticationResponse authenticationResponse; + + try { + authenticationResponse = identityProvider.authenticate(authenticationRequest); + final String token = jwtService.generateSignedToken(authenticationResponse); + return token; + } catch (final IdentityAccessException | JwtException e) { + throw new AdministrationException(e.getMessage()); + } + + } + + /** + * A helper function that generates a prioritized list of IdentityProviders to use to + * attempt client authentication. + * + * Note: This is currently a hard-coded list order consisting of: + * + * - X509IdentityProvider (if available) + * - KerberosProvider (if available) + * - User-defined IdentityProvider (if available) + * + * However, in the future it could be entirely user-configurable + * + * @return a list of providers to use in order to authenticate the client. + */ + private List generateIdentityProviderWaterfall() { + List identityProviderWaterfall = new ArrayList<>(); + + // if configured with an X509IdentityProvider, add it to the list of providers to try + if (x509IdentityProvider != null) { + identityProviderWaterfall.add(x509IdentityProvider); + } + + // if configured with an KerberosSpnegoIdentityProvider, add it to the end of the list of providers to try + if (kerberosSpnegoIdentityProvider != null) { + identityProviderWaterfall.add(kerberosSpnegoIdentityProvider); + } + + // if configured with custom identity provider, add it to the end of the list of providers to try + if (identityProvider != null) { + identityProviderWaterfall.add(identityProvider); + } + + return identityProviderWaterfall; + } + + private boolean isBasicLoginSupported(HttpServletRequest request) { + return request.isSecure() && identityProvider != null; + } + + private boolean isOIDCLoginSupported(HttpServletRequest request) { + return request.isSecure() && oidcService != null && oidcService.isOidcEnabled(); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ApplicationResource.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ApplicationResource.java new file mode 100644 index 0000000000..bce1e3911c --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ApplicationResource.java @@ -0,0 +1,233 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.api; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.nifi.registry.event.EventService; +import org.apache.nifi.registry.hook.Event; +import org.apache.nifi.registry.revision.entity.RevisionInfo; +import org.apache.nifi.registry.revision.web.ClientIdParameter; +import org.apache.nifi.registry.revision.web.LongParameter; +import org.apache.nifi.registry.web.service.ServiceFacade; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.CacheControl; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriBuilderException; +import javax.ws.rs.core.UriInfo; +import java.net.URI; +import java.net.URISyntaxException; + +public class ApplicationResource { + + public static final String CLIENT_ID = "clientId"; + public static final String VERSION = "version"; + + public static final String PROXY_SCHEME_HTTP_HEADER = "X-ProxyScheme"; + public static final String PROXY_HOST_HTTP_HEADER = "X-ProxyHost"; + public static final String PROXY_PORT_HTTP_HEADER = "X-ProxyPort"; + public static final String PROXY_CONTEXT_PATH_HTTP_HEADER = "X-ProxyContextPath"; + + public static final String FORWARDED_PROTO_HTTP_HEADER = "X-Forwarded-Proto"; + public static final String FORWARDED_HOST_HTTP_HEADER = "X-Forwarded-Server"; + public static final String FORWARDED_PORT_HTTP_HEADER = "X-Forwarded-Port"; + public static final String FORWARDED_CONTEXT_HTTP_HEADER = "X-Forwarded-Context"; + + public static final String NON_GUARANTEED_ENDPOINT = "\n\nNOTE: This endpoint is subject to change as NiFi Registry and its REST API evolve."; + + private static final Logger logger = LoggerFactory.getLogger(ApplicationResource.class); + + @Context + private HttpServletRequest httpServletRequest; + + @Context + private UriInfo uriInfo; + + protected final ServiceFacade serviceFacade; + private final EventService eventService; + + public ApplicationResource(final ServiceFacade serviceFacade, + final EventService eventService) { + this.serviceFacade = serviceFacade; + this.eventService = eventService; + Validate.notNull(this.serviceFacade); + Validate.notNull(this.eventService); + } + + // We don't want an error creating/publishing an event to cause the overall request to fail, so catch all throwables here + protected void publish(final Event event) { + try { + eventService.publish(event); + } catch (Throwable t) { + logger.error("Unable to publish event: " + t.getMessage(), t); + } + } + + protected URI getBaseUri() { + final UriBuilder uriBuilder = uriInfo.getBaseUriBuilder(); + URI uri = uriBuilder.build(); + try { + + // check for proxy settings + final String scheme = getFirstHeaderValue(PROXY_SCHEME_HTTP_HEADER, FORWARDED_PROTO_HTTP_HEADER); + final String host = getFirstHeaderValue(PROXY_HOST_HTTP_HEADER, FORWARDED_HOST_HTTP_HEADER); + final String port = getFirstHeaderValue(PROXY_PORT_HTTP_HEADER, FORWARDED_PORT_HTTP_HEADER); + String baseContextPath = getFirstHeaderValue(PROXY_CONTEXT_PATH_HTTP_HEADER, FORWARDED_CONTEXT_HTTP_HEADER); + + // if necessary, prepend the context path + String resourcePath = uri.getPath(); + if (baseContextPath != null) { + // normalize context path + if (!baseContextPath.startsWith("/")) { + baseContextPath = "/" + baseContextPath; + } + + if (baseContextPath.endsWith("/")) { + baseContextPath = StringUtils.substringBeforeLast(baseContextPath, "/"); + } + + // determine the complete resource path + resourcePath = baseContextPath + resourcePath; + } + + // determine the port uri + int uriPort = uri.getPort(); + if (port != null) { + if (StringUtils.isWhitespace(port)) { + uriPort = -1; + } else { + try { + uriPort = Integer.parseInt(port); + } catch (final NumberFormatException nfe) { + logger.warn(String.format("Unable to parse proxy port HTTP header '%s'. Using port from request URI '%s'.", port, uriPort)); + } + } + } + + // construct the URI + uri = new URI( + (StringUtils.isBlank(scheme)) ? uri.getScheme() : scheme, + uri.getUserInfo(), + (StringUtils.isBlank(host)) ? uri.getHost() : host, + uriPort, + resourcePath, + uri.getQuery(), + uri.getFragment()); + + } catch (final URISyntaxException use) { + throw new UriBuilderException(use); + } + return uri; + } + + protected String generateResourceUri(final String... path) { + final URI baseUri = getBaseUri(); + final URI fullUri = UriBuilder.fromUri(baseUri).segment(path).build(); + return fullUri.toString(); + } + + /** + * Edit the response headers to indicating no caching. + * + * @param response response + * @return builder + */ + protected Response.ResponseBuilder noCache(final Response.ResponseBuilder response) { + final CacheControl cacheControl = new CacheControl(); + cacheControl.setPrivate(true); + cacheControl.setNoCache(true); + cacheControl.setNoStore(true); + return response.cacheControl(cacheControl); + } + + /** + * Generates an OK response with the specified content. + * + * @param entity The entity + * @return The response to be built + */ + protected Response.ResponseBuilder generateOkResponse(final Object entity) { + final Response.ResponseBuilder response = Response.ok(entity); + return noCache(response); + } + + /** + * Generates an Ok response with no content. + * + * @return an Ok response with no content + */ + protected Response.ResponseBuilder generateOkResponse() { + return noCache(Response.ok()); + } + + /** + * Generates a 201 Created response with the specified content. + * + * @param uri The URI + * @param entity entity + * @return The response to be built + */ + protected Response.ResponseBuilder generateCreatedResponse(final URI uri, final Object entity) { + // generate the response builder + return Response.created(uri).entity(entity); + } + + /** + * Returns the value for the first key discovered when inspecting the current request. Will + * return null if there are no keys specified or if none of the specified keys are found. + * + * @param keys http header keys + * @return the value for the first key found + */ + private String getFirstHeaderValue(final String... keys) { + if (keys == null) { + return null; + } + + for (final String key : keys) { + final String value = httpServletRequest.getHeader(key); + + // if we found an entry for this key, return the value + if (value != null) { + return value; + } + } + + // unable to find any matching keys + return null; + } + + /** + * Creates a RevisionInfo from the version and clientId parameters. + * + * @param version the version + * @param clientId the client id + * @return the RevisionInfo + */ + protected RevisionInfo getRevisionInfo(final LongParameter version, final ClientIdParameter clientId) { + final RevisionInfo revisionInfo = new RevisionInfo(); + revisionInfo.setVersion(version == null ? null : version.getLong()); + revisionInfo.setClientId(clientId.getClientId()); + return revisionInfo; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketBundleResource.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketBundleResource.java new file mode 100644 index 0000000000..60d8df556d --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketBundleResource.java @@ -0,0 +1,151 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.api; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import io.swagger.annotations.Authorization; +import io.swagger.annotations.Extension; +import io.swagger.annotations.ExtensionProperty; +import org.apache.nifi.registry.event.EventFactory; +import org.apache.nifi.registry.event.EventService; +import org.apache.nifi.registry.extension.bundle.Bundle; +import org.apache.nifi.registry.extension.bundle.BundleType; +import org.apache.nifi.registry.extension.bundle.BundleTypeValues; +import org.apache.nifi.registry.extension.bundle.BundleVersion; +import org.apache.nifi.registry.web.service.ServiceFacade; +import org.glassfish.jersey.media.multipart.FormDataContentDisposition; +import org.glassfish.jersey.media.multipart.FormDataParam; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +@Component +@Path("/buckets/{bucketId}/bundles") +@Api( + value = "bucket bundles", + description = "Create extension bundles scoped to an existing bucket in the registry. ", + authorizations = { @Authorization("Authorization") } +) +public class BucketBundleResource extends ApplicationResource { + + private static final Logger LOGGER = LoggerFactory.getLogger(BucketBundleResource.class); + + @Autowired + public BucketBundleResource(final ServiceFacade serviceFacade, final EventService eventService) { + super(serviceFacade, eventService); + } + + @POST + @Path("{bundleType}") + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Create extension bundle version", + notes = "Creates a version of an extension bundle by uploading a binary artifact. " + + "If an extension bundle already exists in the given bucket with the same group id and artifact id " + + "as that of the bundle being uploaded, then it will be added as a new version to the existing bundle. " + + "If an extension bundle does not already exist in the given bucket with the same group id and artifact id, " + + "then a new extension bundle will be created and this version will be added to the new bundle. " + + "Client's may optionally supply a SHA-256 in hex format through the multi-part form field 'sha256'. " + + "If supplied, then this value will be compared against the SHA-256 computed by the server, and the bundle " + + "will be rejected if the values do not match. If not supplied, the bundle will be accepted, but will be marked " + + "to indicate that the client did not supply a SHA-256 during creation. " + NON_GUARANTEED_ENDPOINT, + response = BundleVersion.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "write"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response createExtensionBundleVersion( + @PathParam("bucketId") + @ApiParam(value = "The bucket identifier", required = true) + final String bucketId, + @PathParam("bundleType") + @ApiParam(value = "The type of the bundle", required = true, allowableValues = BundleTypeValues.ALL_VALUES) + final BundleType bundleType, + @FormDataParam("file") + final InputStream fileInputStream, + @FormDataParam("file") + final FormDataContentDisposition fileMetaData, + @FormDataParam("sha256") + final String clientSha256) throws IOException { + + LOGGER.debug("Creating extension bundle version for bundle type {}", new Object[]{bundleType}); + + final BundleVersion createdBundleVersion = serviceFacade.createBundleVersion( + bucketId, bundleType, fileInputStream, clientSha256); + + publish(EventFactory.extensionBundleCreated(createdBundleVersion.getBundle())); + publish(EventFactory.extensionBundleVersionCreated(createdBundleVersion)); + + return Response.status(Response.Status.OK).entity(createdBundleVersion).build(); + } + + @GET + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get extension bundles by bucket", + notes = NON_GUARANTEED_ENDPOINT, + response = Bundle.class, + responseContainer = "List", + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getExtensionBundles( + @PathParam("bucketId") + @ApiParam(value = "The bucket identifier", required = true) + final String bucketId) { + + final List bundles = serviceFacade.getBundlesByBucket(bucketId); + return Response.status(Response.Status.OK).entity(bundles).build(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java new file mode 100644 index 0000000000..8dd7290432 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java @@ -0,0 +1,542 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.api; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import io.swagger.annotations.Authorization; +import io.swagger.annotations.Extension; +import io.swagger.annotations.ExtensionProperty; +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.bucket.BucketItem; +import org.apache.nifi.registry.diff.VersionedFlowDifference; +import org.apache.nifi.registry.event.EventFactory; +import org.apache.nifi.registry.event.EventService; +import org.apache.nifi.registry.flow.VersionedFlow; +import org.apache.nifi.registry.flow.VersionedFlowSnapshot; +import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata; +import org.apache.nifi.registry.revision.entity.RevisionInfo; +import org.apache.nifi.registry.revision.web.ClientIdParameter; +import org.apache.nifi.registry.revision.web.LongParameter; +import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils; +import org.apache.nifi.registry.web.service.ServiceFacade; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.validation.constraints.NotNull; +import javax.ws.rs.BadRequestException; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +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; +import java.util.SortedSet; + +@Component +@Path("/buckets/{bucketId}/flows") +@Api( + value = "bucket flows", + description = "Create flows scoped to an existing bucket in the registry.", + authorizations = { @Authorization("Authorization") } +) +public class BucketFlowResource extends ApplicationResource { + + @Autowired + public BucketFlowResource(final ServiceFacade serviceFacade, final EventService eventService) { + super(serviceFacade, eventService); + } + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Create flow", + notes = "Creates a flow in the given bucket. The flow id is created by the server and populated in the returned entity.", + response = VersionedFlow.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "write"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response createFlow( + @PathParam("bucketId") + @ApiParam("The bucket identifier") + final String bucketId, + @ApiParam(value = "The details of the flow to create.", required = true) + final VersionedFlow flow) { + + verifyPathParamsMatchBody(bucketId, flow); + + final VersionedFlow createdFlow = serviceFacade.createFlow(bucketId, flow); + publish(EventFactory.flowCreated(createdFlow)); + return Response.status(Response.Status.OK).entity(createdFlow).build(); + } + + @GET + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get bucket flows", + notes = "Retrieves all flows in the given bucket.", + response = VersionedFlow.class, + responseContainer = "List", + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getFlows( + @PathParam("bucketId") + @ApiParam("The bucket identifier") + final String bucketId) { + + final List flows = serviceFacade.getFlows(bucketId); + return Response.status(Response.Status.OK).entity(flows).build(); + } + + @GET + @Path("{flowId}") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get bucket flow", + notes = "Retrieves the flow with the given id in the given bucket.", + response = VersionedFlow.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getFlow( + @PathParam("bucketId") + @ApiParam("The bucket identifier") + final String bucketId, + @PathParam("flowId") + @ApiParam("The flow identifier") + final String flowId) { + + final VersionedFlow flow = serviceFacade.getFlow(bucketId, flowId); + return Response.status(Response.Status.OK).entity(flow).build(); + } + + @PUT + @Path("{flowId}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Update bucket flow", + notes = "Updates the flow with the given id in the given bucket.", + response = VersionedFlow.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "write"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response updateFlow( + @PathParam("bucketId") + @ApiParam("The bucket identifier") + final String bucketId, + @PathParam("flowId") + @ApiParam("The flow identifier") + final String flowId, + @ApiParam(value = "The updated flow", required = true) + final VersionedFlow flow) { + + verifyPathParamsMatchBody(bucketId, flowId, flow); + + // bucketId and flowId fields are optional in the body parameter, but required before calling the service layer + setBucketItemMetadataIfMissing(bucketId, flowId, flow); + + final VersionedFlow updatedFlow = serviceFacade.updateFlow(flow); + publish(EventFactory.flowUpdated(updatedFlow)); + return Response.status(Response.Status.OK).entity(updatedFlow).build(); + } + + @DELETE + @Path("{flowId}") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Delete bucket flow", + notes = "Deletes a flow, including all saved versions of that flow.", + response = VersionedFlow.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "delete"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response deleteFlow( + @ApiParam(value = "The version is used to verify the client is working with the latest version of the entity.", required = true) + @QueryParam(VERSION) + final LongParameter version, + @ApiParam(value = "If the client id is not specified, new one will be generated. This value (whether specified or generated) is included in the response.") + @QueryParam(CLIENT_ID) + @DefaultValue(StringUtils.EMPTY) + final ClientIdParameter clientId, + @PathParam("bucketId") + @ApiParam("The bucket identifier") + final String bucketId, + @PathParam("flowId") + @ApiParam("The flow identifier") + final String flowId) { + + final RevisionInfo revisionInfo = getRevisionInfo(version, clientId); + final VersionedFlow deletedFlow = serviceFacade.deleteFlow(bucketId, flowId, revisionInfo); + publish(EventFactory.flowDeleted(deletedFlow)); + + return Response.status(Response.Status.OK).entity(deletedFlow).build(); + } + + @POST + @Path("{flowId}/versions") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Create flow version", + notes = "Creates the next version of a flow. The version number of the object being created must be the " + + "next available version integer. Flow versions are immutable after they are created.", + response = VersionedFlowSnapshot.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "write"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response createFlowVersion( + @PathParam("bucketId") + @ApiParam("The bucket identifier") + final String bucketId, + @PathParam("flowId") + @ApiParam(value = "The flow identifier") + final String flowId, + @ApiParam(value = "The new versioned flow snapshot.", required = true) + final VersionedFlowSnapshot snapshot) { + + verifyPathParamsMatchBody(bucketId, flowId, snapshot); + + // bucketId and flowId fields are optional in the body parameter, but required before calling the service layer + setSnaphotMetadataIfMissing(bucketId, flowId, snapshot); + + final String userIdentity = NiFiUserUtils.getNiFiUserIdentity(); + snapshot.getSnapshotMetadata().setAuthor(userIdentity); + + final VersionedFlowSnapshot createdSnapshot = serviceFacade.createFlowSnapshot(snapshot); + publish(EventFactory.flowVersionCreated(createdSnapshot)); + + return Response.status(Response.Status.OK).entity(createdSnapshot).build(); + } + + @GET + @Path("{flowId}/versions") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get bucket flow versions", + notes = "Gets summary information for all versions of a flow. Versions are ordered newest->oldest.", + response = VersionedFlowSnapshotMetadata.class, + responseContainer = "List", + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getFlowVersions( + @PathParam("bucketId") + @ApiParam("The bucket identifier") + final String bucketId, + @PathParam("flowId") + @ApiParam("The flow identifier") + final String flowId) { + + final SortedSet snapshots = serviceFacade.getFlowSnapshots(bucketId, flowId); + return Response.status(Response.Status.OK).entity(snapshots).build(); + } + + @GET + @Path("{flowId}/versions/latest") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get latest bucket flow version content", + notes = "Gets the latest version of a flow, including the metadata and content of the flow.", + response = VersionedFlowSnapshot.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getLatestFlowVersion( + @PathParam("bucketId") + @ApiParam("The bucket identifier") + final String bucketId, + @PathParam("flowId") + @ApiParam("The flow identifier") + final String flowId) { + + final VersionedFlowSnapshot lastSnapshot = serviceFacade.getLatestFlowSnapshot(bucketId, flowId); + return Response.status(Response.Status.OK).entity(lastSnapshot).build(); + } + + @GET + @Path("{flowId}/versions/latest/metadata") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get latest bucket flow version metadata", + notes = "Gets the metadata for the latest version of a flow.", + response = VersionedFlowSnapshotMetadata.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getLatestFlowVersionMetadata( + @PathParam("bucketId") + @ApiParam("The bucket identifier") + final String bucketId, + @PathParam("flowId") + @ApiParam("The flow identifier") + final String flowId) { + + final VersionedFlowSnapshotMetadata latest = serviceFacade.getLatestFlowSnapshotMetadata(bucketId, flowId); + return Response.status(Response.Status.OK).entity(latest).build(); + } + + @GET + @Path("{flowId}/versions/{versionNumber: \\d+}") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get bucket flow version", + notes = "Gets the given version of a flow, including the metadata and content for the version.", + response = VersionedFlowSnapshot.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getFlowVersion( + @PathParam("bucketId") + @ApiParam("The bucket identifier") + final String bucketId, + @PathParam("flowId") + @ApiParam("The flow identifier") + final String flowId, + @PathParam("versionNumber") + @ApiParam("The version number") + final Integer versionNumber) { + + final VersionedFlowSnapshot snapshot = serviceFacade.getFlowSnapshot(bucketId, flowId, versionNumber); + return Response.status(Response.Status.OK).entity(snapshot).build(); + } + + @GET + @Path("{flowId}/diff/{versionA: \\d+}/{versionB: \\d+}") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get bucket flow diff", + notes = "Computes the differences between two given versions of a flow.", + response = VersionedFlowDifference.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409)}) + public Response getFlowDiff( + @PathParam("bucketId") + @ApiParam("The bucket identifier") + final String bucketId, + @PathParam("flowId") + @ApiParam("The flow identifier") + final String flowId, + @PathParam("versionA") + @ApiParam("The first version number") + final Integer versionNumberA, + @PathParam("versionB") + @ApiParam("The second version number") + final Integer versionNumberB) { + + final VersionedFlowDifference result = serviceFacade.getFlowDiff(bucketId, flowId, versionNumberA, versionNumberB); + return Response.status(Response.Status.OK).entity(result).build(); + } + + private static void verifyPathParamsMatchBody(String bucketIdParam, BucketItem bodyBucketItem) throws BadRequestException { + if (StringUtils.isBlank(bucketIdParam)) { + throw new BadRequestException("Bucket id path parameter cannot be blank"); + } + + if (bodyBucketItem == null) { + throw new BadRequestException("Object in body cannot be null"); + } + + if (bodyBucketItem.getBucketIdentifier() != null && !bucketIdParam.equals(bodyBucketItem.getBucketIdentifier())) { + throw new BadRequestException("Bucket id in path param must match bucket id in body"); + } + } + + private static void verifyPathParamsMatchBody(String bucketIdParam, String flowIdParam, BucketItem bodyBucketItem) throws BadRequestException { + verifyPathParamsMatchBody(bucketIdParam, bodyBucketItem); + + if (StringUtils.isBlank(flowIdParam)) { + throw new BadRequestException("Flow id path parameter cannot be blank"); + } + + if (bodyBucketItem.getIdentifier() != null && !flowIdParam.equals(bodyBucketItem.getIdentifier())) { + throw new BadRequestException("Item id in path param must match item id in body"); + } + } + + private static void verifyPathParamsMatchBody(String bucketIdParam, String flowIdParam, VersionedFlowSnapshot flowSnapshot) throws BadRequestException { + if (StringUtils.isBlank(bucketIdParam)) { + throw new BadRequestException("Bucket id path parameter cannot be blank"); + } + + if (StringUtils.isBlank(flowIdParam)) { + throw new BadRequestException("Flow id path parameter cannot be blank"); + } + + if (flowSnapshot == null) { + throw new BadRequestException("VersionedFlowSnapshot cannot be null in body"); + } + + final VersionedFlowSnapshotMetadata metadata = flowSnapshot.getSnapshotMetadata(); + if (metadata != null && metadata.getBucketIdentifier() != null && !bucketIdParam.equals(metadata.getBucketIdentifier())) { + throw new BadRequestException("Bucket id in path param must match bucket id in body"); + } + if (metadata != null && metadata.getFlowIdentifier() != null && !flowIdParam.equals(metadata.getFlowIdentifier())) { + throw new BadRequestException("Flow id in path param must match flow id in body"); + } + } + + private static void setBucketItemMetadataIfMissing( + @NotNull String bucketIdParam, + @NotNull String bucketItemIdParam, + @NotNull BucketItem bucketItem) { + if (bucketItem.getBucketIdentifier() == null) { + bucketItem.setBucketIdentifier(bucketIdParam); + } + + if (bucketItem.getIdentifier() == null) { + bucketItem.setIdentifier(bucketItemIdParam); + } + } + + private static void setSnaphotMetadataIfMissing( + @NotNull String bucketIdParam, + @NotNull String flowIdParam, + @NotNull VersionedFlowSnapshot flowSnapshot) { + + VersionedFlowSnapshotMetadata metadata = flowSnapshot.getSnapshotMetadata(); + if (metadata == null) { + metadata = new VersionedFlowSnapshotMetadata(); + } + + if (metadata.getBucketIdentifier() == null) { + metadata.setBucketIdentifier(bucketIdParam); + } + + if (metadata.getFlowIdentifier() == null) { + metadata.setFlowIdentifier(flowIdParam); + } + + flowSnapshot.setSnapshotMetadata(metadata); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketResource.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketResource.java new file mode 100644 index 0000000000..1692a2945e --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketResource.java @@ -0,0 +1,249 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.api; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import io.swagger.annotations.Authorization; +import io.swagger.annotations.Extension; +import io.swagger.annotations.ExtensionProperty; +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.event.EventFactory; +import org.apache.nifi.registry.event.EventService; +import org.apache.nifi.registry.field.Fields; +import org.apache.nifi.registry.revision.entity.RevisionInfo; +import org.apache.nifi.registry.revision.web.ClientIdParameter; +import org.apache.nifi.registry.revision.web.LongParameter; +import org.apache.nifi.registry.web.service.ServiceFacade; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.ws.rs.BadRequestException; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +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; +import java.util.Set; + +@Component +@Path("/buckets") +@Api( + value = "buckets", + description = "Create named buckets in the registry to store NiFi objects such flows and extensions. " + + "Search for and retrieve existing buckets.", + authorizations = { @Authorization("Authorization") } +) +public class BucketResource extends ApplicationResource { + + @Autowired + public BucketResource(final ServiceFacade serviceFacade, final EventService eventService) { + super(serviceFacade, eventService); + } + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Create bucket", + response = Bucket.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "write"), + @ExtensionProperty(name = "resource", value = "/buckets") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403) }) + public Response createBucket( + @ApiParam(value = "The bucket to create", required = true) + final Bucket bucket) { + + final Bucket createdBucket = serviceFacade.createBucket(bucket); + publish(EventFactory.bucketCreated(createdBucket)); + return Response.status(Response.Status.OK).entity(createdBucket).build(); + } + + @GET + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get all buckets", + notes = "The returned list will include only buckets for which the user is authorized." + + "If the user is not authorized for any buckets, this returns an empty list.", + response = Bucket.class, + responseContainer = "List" + ) + @ApiResponses({ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401) }) + public Response getBuckets() { + // ServiceFacade will determine which buckets the user is authorized for + // Note: We don't explicitly check for access to (READ, /buckets) because + // a user might have access to individual buckets without top-level access. + // For example, a user that has (READ, /buckets/bucket-id-1) but not access + // to /buckets should not get a 403 error returned from this endpoint. + // This has the side effect that a user with no access to any buckets + // gets an empty array returned from this endpoint instead of 403 as one + // might expect. + final List buckets = serviceFacade.getBuckets(); + return Response.status(Response.Status.OK).entity(buckets).build(); + } + + @GET + @Path("{bucketId}") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get bucket", + notes = "Gets the bucket with the given id.", + response = Bucket.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404) }) + public Response getBucket( + @PathParam("bucketId") + @ApiParam("The bucket identifier") + final String bucketId) { + + final Bucket bucket = serviceFacade.getBucket(bucketId); + return Response.status(Response.Status.OK).entity(bucket).build(); + } + + @PUT + @Path("{bucketId}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Update bucket", + notes = "Updates the bucket with the given id.", + response = Bucket.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "write"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response updateBucket( + @PathParam("bucketId") + @ApiParam("The bucket identifier") + final String bucketId, + @ApiParam(value = "The updated bucket", required = true) + final Bucket bucket) { + + if (StringUtils.isBlank(bucketId)) { + throw new BadRequestException("Bucket id cannot be blank"); + } + + if (bucket == null) { + throw new BadRequestException("Bucket cannot be null"); + } + + if (bucket.getIdentifier() != null && !bucketId.equals(bucket.getIdentifier())) { + throw new BadRequestException("Bucket id in path param must match bucket id in body"); + } else { + bucket.setIdentifier(bucketId); + } + + final Bucket updatedBucket = serviceFacade.updateBucket(bucket); + publish(EventFactory.bucketUpdated(updatedBucket)); + return Response.status(Response.Status.OK).entity(updatedBucket).build(); + } + + @DELETE + @Path("{bucketId}") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Delete bucket", + notes = "Deletes the bucket with the given id, along with all objects stored in the bucket", + response = Bucket.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "delete"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404) }) + public Response deleteBucket( + @ApiParam(value = "The version is used to verify the client is working with the latest version of the entity.", required = true) + @QueryParam(VERSION) + final LongParameter version, + @ApiParam(value = "If the client id is not specified, new one will be generated. This value (whether specified or generated) is included in the response.") + @QueryParam(CLIENT_ID) + @DefaultValue(StringUtils.EMPTY) + final ClientIdParameter clientId, + @PathParam("bucketId") + @ApiParam("The bucket identifier") + final String bucketId) { + + if (StringUtils.isBlank(bucketId)) { + throw new BadRequestException("Bucket id cannot be blank"); + } + + final RevisionInfo revisionInfo = getRevisionInfo(version, clientId); + final Bucket deletedBucket = serviceFacade.deleteBucket(bucketId, revisionInfo); + publish(EventFactory.bucketDeleted(deletedBucket)); + + return Response.status(Response.Status.OK).entity(deletedBucket).build(); + } + + @GET + @Path("fields") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get bucket fields", + notes = "Retrieves bucket field names for searching or sorting on buckets.", + response = Fields.class + ) + public Response getAvailableBucketFields() { + final Set bucketFields = serviceFacade.getBucketFields(); + final Fields fields = new Fields(bucketFields); + return Response.status(Response.Status.OK).entity(fields).build(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BundleResource.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BundleResource.java new file mode 100644 index 0000000000..1edc0a70fd --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BundleResource.java @@ -0,0 +1,479 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.api; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import io.swagger.annotations.Authorization; +import io.swagger.annotations.Extension; +import io.swagger.annotations.ExtensionProperty; +import org.apache.nifi.registry.event.EventFactory; +import org.apache.nifi.registry.event.EventService; +import org.apache.nifi.registry.extension.bundle.Bundle; +import org.apache.nifi.registry.extension.bundle.BundleFilterParams; +import org.apache.nifi.registry.extension.bundle.BundleVersion; +import org.apache.nifi.registry.extension.bundle.BundleVersionFilterParams; +import org.apache.nifi.registry.extension.bundle.BundleVersionMetadata; +import org.apache.nifi.registry.extension.component.ExtensionMetadata; +import org.apache.nifi.registry.web.service.ServiceFacade; +import org.apache.nifi.registry.web.service.StreamingContent; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.StreamingOutput; +import java.util.List; +import java.util.SortedSet; + +@Component +@Path("/bundles") +@Api( + value = "bundles", + description = "Gets metadata about extension bundles and their versions. ", + authorizations = { @Authorization("Authorization") } +) +public class BundleResource extends ApplicationResource { + + public static final String CONTENT_DISPOSITION_HEADER = "content-disposition"; + + @Autowired + public BundleResource(final ServiceFacade serviceFacade, final EventService eventService) { + super(serviceFacade, eventService); + } + + // ---------- Extension Bundles ---------- + + @GET + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get all bundles", + notes = "Gets the metadata for all bundles across all authorized buckets with optional filters applied. " + + "The returned results will include only items from buckets for which the user is authorized. " + + "If the user is not authorized to any buckets, an empty list will be returned. " + NON_GUARANTEED_ENDPOINT, + response = Bundle.class, + responseContainer = "List" + ) + @ApiResponses({ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401) }) + public Response getBundles( + @QueryParam("bucketName") + @ApiParam("Optional bucket name to filter results. The value may be an exact match, or a wildcard, " + + "such as 'My Bucket%' to select all bundles where the bucket name starts with 'My Bucket'.") + final String bucketName, + @QueryParam("groupId") + @ApiParam("Optional groupId to filter results. The value may be an exact match, or a wildcard, " + + "such as 'com.%' to select all bundles where the groupId starts with 'com.'.") + final String groupId, + @QueryParam("artifactId") + @ApiParam("Optional artifactId to filter results. The value may be an exact match, or a wildcard, " + + "such as 'nifi-%' to select all bundles where the artifactId starts with 'nifi-'.") + final String artifactId) { + + final BundleFilterParams filterParams = BundleFilterParams.of(bucketName, groupId, artifactId); + + // Service facade will return only bundles from authorized buckets + final List bundles = serviceFacade.getBundles(filterParams); + return Response.status(Response.Status.OK).entity(bundles).build(); + } + + @GET + @Path("{bundleId}") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get bundle", + notes = "Gets the metadata about an extension bundle. " + NON_GUARANTEED_ENDPOINT, + nickname = "globalGetExtensionBundle", + response = Bundle.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getBundle( + @PathParam("bundleId") + @ApiParam("The extension bundle identifier") + final String bundleId) { + + final Bundle bundle = serviceFacade.getBundle(bundleId); + return Response.status(Response.Status.OK).entity(bundle).build(); + } + + @DELETE + @Path("{bundleId}") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Delete bundle", + notes = "Deletes the given extension bundle and all of it's versions. " + NON_GUARANTEED_ENDPOINT, + nickname = "globalDeleteExtensionBundle", + response = Bundle.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "write"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response deleteBundle( + @PathParam("bundleId") + @ApiParam("The extension bundle identifier") + final String bundleId) { + + final Bundle deletedBundle = serviceFacade.deleteBundle(bundleId); + publish(EventFactory.extensionBundleDeleted(deletedBundle)); + return Response.status(Response.Status.OK).entity(deletedBundle).build(); + } + + // ---------- Extension Bundle Versions ---------- + + @GET + @Path("versions") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get all bundle versions", + notes = "Gets the metadata about extension bundle versions across all authorized buckets with optional filters applied. " + + "If the user is not authorized to any buckets, an empty list will be returned. " + NON_GUARANTEED_ENDPOINT, + response = BundleVersionMetadata.class, + responseContainer = "List" + ) + @ApiResponses({ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401) }) + public Response getBundleVersions( + @QueryParam("groupId") + @ApiParam("Optional groupId to filter results. The value may be an exact match, or a wildcard, " + + "such as 'com.%' to select all bundle versions where the groupId starts with 'com.'.") + final String groupId, + @QueryParam("artifactId") + @ApiParam("Optional artifactId to filter results. The value may be an exact match, or a wildcard, " + + "such as 'nifi-%' to select all bundle versions where the artifactId starts with 'nifi-'.") + final String artifactId, + @QueryParam("version") + @ApiParam("Optional version to filter results. The value maye be an exact match, or a wildcard, " + + "such as '1.0.%' to select all bundle versions where the version starts with '1.0.'.") + final String version + ) { + + final BundleVersionFilterParams filterParams = BundleVersionFilterParams.of(groupId, artifactId, version); + final SortedSet bundleVersions = serviceFacade.getBundleVersions(filterParams); + return Response.status(Response.Status.OK).entity(bundleVersions).build(); + } + + @GET + @Path("{bundleId}/versions") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get bundle versions", + notes = "Gets the metadata for the versions of the given extension bundle. " + NON_GUARANTEED_ENDPOINT, + nickname = "globalGetBundleVersions", + response = BundleVersionMetadata.class, + responseContainer = "List", + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getBundleVersions( + @PathParam("bundleId") + @ApiParam("The extension bundle identifier") + final String bundleId) { + + final SortedSet bundleVersions = serviceFacade.getBundleVersions(bundleId); + return Response.status(Response.Status.OK).entity(bundleVersions).build(); + } + + @GET + @Path("{bundleId}/versions/{version}") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get bundle version", + notes = "Gets the descriptor for the given version of the given extension bundle. " + NON_GUARANTEED_ENDPOINT, + nickname = "globalGetBundleVersion", + response = BundleVersion.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getBundleVersion( + @PathParam("bundleId") + @ApiParam("The extension bundle identifier") + final String bundleId, + @PathParam("version") + @ApiParam("The version of the bundle") + final String version) { + + final BundleVersion bundleVersion = serviceFacade.getBundleVersion(bundleId, version); + return Response.ok(bundleVersion).build(); + } + + @GET + @Path("{bundleId}/versions/{version}/content") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_OCTET_STREAM) + @ApiOperation( + value = "Get bundle version content", + notes = "Gets the binary content for the given version of the given extension bundle. " + NON_GUARANTEED_ENDPOINT, + nickname = "globalGetBundleVersionContent", + response = byte[].class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getBundleVersionContent( + @PathParam("bundleId") + @ApiParam("The extension bundle identifier") + final String bundleId, + @PathParam("version") + @ApiParam("The version of the bundle") + final String version) { + + final StreamingContent streamingContent = serviceFacade.getBundleVersionContent(bundleId, version); + + final String filename = streamingContent.getFilename(); + final StreamingOutput output = streamingContent.getOutput(); + + return Response.ok(output) + .header(CONTENT_DISPOSITION_HEADER,"attachment; filename = " + filename) + .build(); + } + + @DELETE + @Path("{bundleId}/versions/{version}") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Delete bundle version", + notes = "Deletes the given extension bundle version and it's associated binary content. " + NON_GUARANTEED_ENDPOINT, + nickname = "globalDeleteBundleVersion", + response = BundleVersion.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "write"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response deleteBundleVersion( + @PathParam("bundleId") + @ApiParam("The extension bundle identifier") + final String bundleId, + @PathParam("version") + @ApiParam("The version of the bundle") + final String version) { + + final BundleVersion deletedBundleVersion = serviceFacade.deleteBundleVersion(bundleId, version); + publish(EventFactory.extensionBundleVersionDeleted(deletedBundleVersion)); + return Response.status(Response.Status.OK).entity(deletedBundleVersion).build(); + } + + @GET + @Path("{bundleId}/versions/{version}/extensions") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get bundle version extensions", + notes = "Gets the metadata about the extensions in the given extension bundle version. " + NON_GUARANTEED_ENDPOINT, + nickname = "globalGetBundleVersionExtensions", + response = ExtensionMetadata.class, + responseContainer = "List", + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getBundleVersionExtensions( + @PathParam("bundleId") + @ApiParam("The extension bundle identifier") + final String bundleId, + @PathParam("version") + @ApiParam("The version of the bundle") + final String version) { + + final SortedSet extensions = serviceFacade.getExtensionMetadata(bundleId, version); + return Response.ok(extensions).build(); + } + + @GET + @Path("{bundleId}/versions/{version}/extensions/{name}") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get bundle version extension", + notes = "Gets the metadata about the extension with the given name in the given extension bundle version. " + NON_GUARANTEED_ENDPOINT, + nickname = "globalGetBundleVersionExtension", + response = org.apache.nifi.registry.extension.component.manifest.Extension.class, + responseContainer = "List", + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getBundleVersionExtension( + @PathParam("bundleId") + @ApiParam("The extension bundle identifier") + final String bundleId, + @PathParam("version") + @ApiParam("The version of the bundle") + final String version, + @PathParam("name") + @ApiParam("The fully qualified name of the extension") + final String name + ) { + + final org.apache.nifi.registry.extension.component.manifest.Extension extension = + serviceFacade.getExtension(bundleId, version, name); + return Response.ok(extension).build(); + } + + @GET + @Path("{bundleId}/versions/{version}/extensions/{name}/docs") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.TEXT_HTML) + @ApiOperation( + value = "Get bundle version extension docs", + notes = "Gets the documentation for the given extension in the given extension bundle version. " + NON_GUARANTEED_ENDPOINT, + response = String.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getBundleVersionExtensionDocs( + @PathParam("bundleId") + @ApiParam("The extension bundle identifier") + final String bundleId, + @PathParam("version") + @ApiParam("The version of the bundle") + final String version, + @PathParam("name") + @ApiParam("The fully qualified name of the extension") + final String name + ) { + final StreamingOutput streamingOutput = serviceFacade.getExtensionDocs(bundleId, version, name); + return Response.ok(streamingOutput).build(); + } + + @GET + @Path("{bundleId}/versions/{version}/extensions/{name}/docs/additional-details") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.TEXT_HTML) + @ApiOperation( + value = "Get bundle version extension docs details", + notes = "Gets the additional details documentation for the given extension in the given extension bundle version. " + NON_GUARANTEED_ENDPOINT, + response = String.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getBundleVersionExtensionAdditionalDetailsDocs( + @PathParam("bundleId") + @ApiParam("The extension bundle identifier") + final String bundleId, + @PathParam("version") + @ApiParam("The version of the bundle") + final String version, + @PathParam("name") + @ApiParam("The fully qualified name of the extension") + final String name + ) { + final StreamingOutput streamingOutput = serviceFacade.getAdditionalDetailsDocs(bundleId, version, name); + return Response.ok(streamingOutput).build(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ConfigResource.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ConfigResource.java new file mode 100644 index 0000000000..37482c621c --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ConfigResource.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.api; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import io.swagger.annotations.Authorization; +import io.swagger.annotations.Extension; +import io.swagger.annotations.ExtensionProperty; +import org.apache.nifi.registry.RegistryConfiguration; +import org.apache.nifi.registry.event.EventService; +import org.apache.nifi.registry.web.service.ServiceFacade; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +@Component +@Path("/config") +@Api( + value = "config", + description = "Retrieves the configuration for this NiFi Registry.", + authorizations = { @Authorization("Authorization") } +) +public class ConfigResource extends ApplicationResource { + + @Autowired + public ConfigResource( + final ServiceFacade serviceFacade, + final EventService eventService) { + super(serviceFacade, eventService); + } + + @GET + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get configration", + notes = "Gets the NiFi Registry configurations.", + response = RegistryConfiguration.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/policies,/tenants") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401) }) + public Response getConfiguration() { + final RegistryConfiguration config = serviceFacade.getRegistryConfiguration(); + return Response.status(Response.Status.OK).entity(config).build(); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ExtensionRepoResource.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ExtensionRepoResource.java new file mode 100644 index 0000000000..140794be12 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ExtensionRepoResource.java @@ -0,0 +1,545 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.api; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import io.swagger.annotations.Authorization; +import io.swagger.annotations.Extension; +import io.swagger.annotations.ExtensionProperty; +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.event.EventService; +import org.apache.nifi.registry.exception.ResourceNotFoundException; +import org.apache.nifi.registry.extension.bundle.BundleVersionFilterParams; +import org.apache.nifi.registry.extension.bundle.BundleVersionMetadata; +import org.apache.nifi.registry.extension.component.ExtensionMetadata; +import org.apache.nifi.registry.extension.repo.ExtensionRepoArtifact; +import org.apache.nifi.registry.extension.repo.ExtensionRepoBucket; +import org.apache.nifi.registry.extension.repo.ExtensionRepoExtensionMetadata; +import org.apache.nifi.registry.extension.repo.ExtensionRepoGroup; +import org.apache.nifi.registry.extension.repo.ExtensionRepoVersion; +import org.apache.nifi.registry.extension.repo.ExtensionRepoVersionSummary; +import org.apache.nifi.registry.web.service.ServiceFacade; +import org.apache.nifi.registry.web.service.StreamingContent; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.StreamingOutput; +import java.util.List; +import java.util.SortedSet; + +@Component +@Path("/extension-repository") +@Api( + value = "extension repository", + description = "Interact with extension bundles via the hierarchy of bucket/group/artifact/version. ", + authorizations = { @Authorization("Authorization") } +) +public class ExtensionRepoResource extends ApplicationResource { + + public static final String CONTENT_DISPOSITION_HEADER = "content-disposition"; + + @Autowired + public ExtensionRepoResource(final ServiceFacade serviceFacade, final EventService eventService) { + super(serviceFacade, eventService); + } + + @GET + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get extension repo buckets", + notes = "Gets the names of the buckets the current user is authorized for in order to browse the repo by bucket. " + NON_GUARANTEED_ENDPOINT, + response = ExtensionRepoBucket.class, + responseContainer = "List" + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getExtensionRepoBuckets() { + final SortedSet repoBuckets = serviceFacade.getExtensionRepoBuckets(getBaseUri()); + return Response.status(Response.Status.OK).entity(repoBuckets).build(); + } + + @GET + @Path("{bucketName}") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get extension repo groups", + notes = "Gets the groups in the extension repository in the given bucket. " + NON_GUARANTEED_ENDPOINT, + response = ExtensionRepoGroup.class, + responseContainer = "List", + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getExtensionRepoGroups( + @PathParam("bucketName") + @ApiParam("The bucket name") + final String bucketName + ) { + final SortedSet repoGroups = serviceFacade.getExtensionRepoGroups(getBaseUri(), bucketName); + return Response.status(Response.Status.OK).entity(repoGroups).build(); + } + + @GET + @Path("{bucketName}/{groupId}") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get extension repo artifacts", + notes = "Gets the artifacts in the extension repository in the given bucket and group. " + NON_GUARANTEED_ENDPOINT, + response = ExtensionRepoArtifact.class, + responseContainer = "List", + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getExtensionRepoArtifacts( + @PathParam("bucketName") + @ApiParam("The bucket name") + final String bucketName, + @PathParam("groupId") + @ApiParam("The group id") + final String groupId + ) { + final SortedSet repoArtifacts = serviceFacade.getExtensionRepoArtifacts(getBaseUri(), bucketName, groupId); + return Response.status(Response.Status.OK).entity(repoArtifacts).build(); + } + + @GET + @Path("{bucketName}/{groupId}/{artifactId}") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get extension repo versions", + notes = "Gets the versions in the extension repository for the given bucket, group, and artifact. " + NON_GUARANTEED_ENDPOINT, + response = ExtensionRepoVersionSummary.class, + responseContainer = "List", + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getExtensionRepoVersions( + @PathParam("bucketName") + @ApiParam("The bucket name") + final String bucketName, + @PathParam("groupId") + @ApiParam("The group identifier") + final String groupId, + @PathParam("artifactId") + @ApiParam("The artifact identifier") + final String artifactId + ) { + final SortedSet repoVersions = serviceFacade.getExtensionRepoVersions( + getBaseUri(), bucketName, groupId, artifactId); + return Response.status(Response.Status.OK).entity(repoVersions).build(); + } + + @GET + @Path("{bucketName}/{groupId}/{artifactId}/{version}") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get extension repo version", + notes = "Gets information about the version in the given bucket, group, and artifact. " + NON_GUARANTEED_ENDPOINT, + response = ExtensionRepoVersion.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getExtensionRepoVersion( + @PathParam("bucketName") + @ApiParam("The bucket name") + final String bucketName, + @PathParam("groupId") + @ApiParam("The group identifier") + final String groupId, + @PathParam("artifactId") + @ApiParam("The artifact identifier") + final String artifactId, + @PathParam("version") + @ApiParam("The version") + final String version + ) { + final ExtensionRepoVersion repoVersion = serviceFacade.getExtensionRepoVersion( + getBaseUri(), bucketName, groupId, artifactId, version); + return Response.ok(repoVersion).build(); + } + + @GET + @Path("{bucketName}/{groupId}/{artifactId}/{version}/extensions") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get extension repo extensions", + notes = "Gets information about the extensions in the given bucket, group, artifact, and version. " + NON_GUARANTEED_ENDPOINT, + response = ExtensionMetadata.class, + responseContainer = "List", + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getExtensionRepoVersionExtensions( + @PathParam("bucketName") + @ApiParam("The bucket name") + final String bucketName, + @PathParam("groupId") + @ApiParam("The group identifier") + final String groupId, + @PathParam("artifactId") + @ApiParam("The artifact identifier") + final String artifactId, + @PathParam("version") + @ApiParam("The version") + final String version + ) { + + final List extensionRepoExtensions = + serviceFacade.getExtensionRepoExtensions( + getBaseUri(), bucketName, groupId, artifactId, version); + + return Response.ok(extensionRepoExtensions).build(); + } + + @GET + @Path("{bucketName}/{groupId}/{artifactId}/{version}/extensions/{name}") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get extension repo extension", + notes = "Gets information about the extension with the given name in " + + "the given bucket, group, artifact, and version. " + NON_GUARANTEED_ENDPOINT, + response = org.apache.nifi.registry.extension.component.manifest.Extension.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getExtensionRepoVersionExtension( + @PathParam("bucketName") + @ApiParam("The bucket name") + final String bucketName, + @PathParam("groupId") + @ApiParam("The group identifier") + final String groupId, + @PathParam("artifactId") + @ApiParam("The artifact identifier") + final String artifactId, + @PathParam("version") + @ApiParam("The version") + final String version, + @PathParam("name") + @ApiParam("The fully qualified name of the extension") + final String name + ) { + final org.apache.nifi.registry.extension.component.manifest.Extension extension = + serviceFacade.getExtensionRepoExtension( + getBaseUri(), bucketName, groupId, artifactId, version, name); + return Response.ok(extension).build(); + } + + @GET + @Path("{bucketName}/{groupId}/{artifactId}/{version}/extensions/{name}/docs") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.TEXT_HTML) + @ApiOperation( + value = "Get extension repo extension docs", + notes = "Gets the documentation for the extension with the given name in " + + "the given bucket, group, artifact, and version. " + NON_GUARANTEED_ENDPOINT, + response = String.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getExtensionRepoVersionExtensionDocs( + @PathParam("bucketName") + @ApiParam("The bucket name") + final String bucketName, + @PathParam("groupId") + @ApiParam("The group identifier") + final String groupId, + @PathParam("artifactId") + @ApiParam("The artifact identifier") + final String artifactId, + @PathParam("version") + @ApiParam("The version") + final String version, + @PathParam("name") + @ApiParam("The fully qualified name of the extension") + final String name + ) { + final StreamingOutput streamingOutput = serviceFacade.getExtensionRepoExtensionDocs( + getBaseUri(), bucketName, groupId, artifactId, version, name); + return Response.ok(streamingOutput).build(); + } + + @GET + @Path("{bucketName}/{groupId}/{artifactId}/{version}/extensions/{name}/docs/additional-details") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.TEXT_HTML) + @ApiOperation( + value = "Get extension repo extension details", + notes = "Gets the additional details documentation for the extension with the given name in " + + "the given bucket, group, artifact, and version. " + NON_GUARANTEED_ENDPOINT, + response = String.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getExtensionRepoVersionExtensionAdditionalDetailsDocs( + @PathParam("bucketName") + @ApiParam("The bucket name") + final String bucketName, + @PathParam("groupId") + @ApiParam("The group identifier") + final String groupId, + @PathParam("artifactId") + @ApiParam("The artifact identifier") + final String artifactId, + @PathParam("version") + @ApiParam("The version") + final String version, + @PathParam("name") + @ApiParam("The fully qualified name of the extension") + final String name + ) { + final StreamingOutput streamingOutput = serviceFacade.getExtensionRepoExtensionAdditionalDocs( + getBaseUri(), bucketName, groupId, artifactId, version, name); + return Response.ok(streamingOutput).build(); + } + + @GET + @Path("{bucketName}/{groupId}/{artifactId}/{version}/content") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_OCTET_STREAM) + @ApiOperation( + value = "Get extension repo version content", + notes = "Gets the binary content of the bundle with the given bucket, group, artifact, and version. " + NON_GUARANTEED_ENDPOINT, + response = byte[].class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getExtensionRepoVersionContent( + @PathParam("bucketName") + @ApiParam("The bucket name") + final String bucketName, + @PathParam("groupId") + @ApiParam("The group identifier") + final String groupId, + @PathParam("artifactId") + @ApiParam("The artifact identifier") + final String artifactId, + @PathParam("version") + @ApiParam("The version") + final String version + ) { + final StreamingContent streamingContent = serviceFacade.getExtensionRepoVersionContent( + bucketName, groupId, artifactId, version); + + final String filename = streamingContent.getFilename(); + final StreamingOutput streamingOutput = streamingContent.getOutput(); + + return Response.ok(streamingOutput) + .header(CONTENT_DISPOSITION_HEADER,"attachment; filename = " + filename) + .build(); + } + + @GET + @Path("{bucketName}/{groupId}/{artifactId}/{version}/sha256") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.TEXT_PLAIN) + @ApiOperation( + value = "Get extension repo version checksum", + notes = "Gets the hex representation of the SHA-256 digest for the binary content of the bundle " + + "with the given bucket, group, artifact, and version." + NON_GUARANTEED_ENDPOINT, + response = String.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getExtensionRepoVersionSha256( + @PathParam("bucketName") + @ApiParam("The bucket name") + final String bucketName, + @PathParam("groupId") + @ApiParam("The group identifier") + final String groupId, + @PathParam("artifactId") + @ApiParam("The artifact identifier") + final String artifactId, + @PathParam("version") + @ApiParam("The version") + final String version + ) { + final String sha256Hex = serviceFacade.getExtensionRepoVersionSha256(bucketName, groupId, artifactId, version); + return Response.ok(sha256Hex, MediaType.TEXT_PLAIN).build(); + } + + @GET + @Path("{groupId}/{artifactId}/{version}/sha256") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.TEXT_PLAIN) + @ApiOperation( + value = "Get global extension repo version checksum", + notes = "Gets the hex representation of the SHA-256 digest for the binary content with the given bucket, group, artifact, and version. " + + "Since the same group-artifact-version can exist in multiple buckets, this will return the checksum of the first one returned. " + + "This will be consistent since the checksum must be the same when existing in multiple buckets. " + NON_GUARANTEED_ENDPOINT, + response = String.class + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getGlobalExtensionRepoVersionSha256( + @PathParam("groupId") + @ApiParam("The group identifier") + final String groupId, + @PathParam("artifactId") + @ApiParam("The artifact identifier") + final String artifactId, + @PathParam("version") + @ApiParam("The version") + final String version + ) { + // Since we are using the filter params which are optional in the service layer, we need to validate these path params here + + if (StringUtils.isBlank(groupId)) { + throw new IllegalArgumentException("Group id cannot be null or blank"); + } + + if (StringUtils.isBlank(artifactId)) { + throw new IllegalArgumentException("Artifact id cannot be null or blank"); + } + + if (StringUtils.isBlank(version)) { + throw new IllegalArgumentException("Version cannot be null or blank"); + } + + final BundleVersionFilterParams filterParams = BundleVersionFilterParams.of(groupId, artifactId, version); + + final SortedSet bundleVersions = serviceFacade.getBundleVersions(filterParams); + if (bundleVersions.isEmpty()) { + throw new ResourceNotFoundException("An extension bundle version does not exist with the specific group, artifact, and version"); + } else { + BundleVersionMetadata latestVersionMetadata = null; + for (BundleVersionMetadata versionMetadata : bundleVersions) { + if (latestVersionMetadata == null || versionMetadata.getTimestamp() > latestVersionMetadata.getTimestamp()) { + latestVersionMetadata = versionMetadata; + } + } + return Response.ok(latestVersionMetadata.getSha256(), MediaType.TEXT_PLAIN).build(); + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ExtensionResource.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ExtensionResource.java new file mode 100644 index 0000000000..52dbe7197e --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ExtensionResource.java @@ -0,0 +1,173 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.api; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import io.swagger.annotations.Authorization; +import org.apache.nifi.registry.event.EventService; +import org.apache.nifi.registry.extension.bundle.BundleType; +import org.apache.nifi.registry.extension.bundle.BundleTypeValues; +import org.apache.nifi.registry.extension.component.ExtensionFilterParams; +import org.apache.nifi.registry.extension.component.ExtensionMetadata; +import org.apache.nifi.registry.extension.component.ExtensionMetadataContainer; +import org.apache.nifi.registry.extension.component.TagCount; +import org.apache.nifi.registry.extension.component.manifest.ExtensionType; +import org.apache.nifi.registry.extension.component.manifest.ProvidedServiceAPI; +import org.apache.nifi.registry.web.service.ServiceFacade; +import org.springframework.stereotype.Component; + +import javax.ws.rs.Consumes; +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.Collections; +import java.util.Set; +import java.util.SortedSet; + +@Component +@Path("/extensions") +@Api( + value = "extensions", + description = "Find and retrieve extensions. ", + authorizations = { @Authorization("Authorization") } +) +public class ExtensionResource extends ApplicationResource { + + public ExtensionResource(final ServiceFacade serviceFacade, final EventService eventService) { + super(serviceFacade, eventService); + } + + @GET + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get all extensions", + notes = "Gets the metadata for all extensions that match the filter params and are part of bundles located in buckets the " + + "current user is authorized for. If the user is not authorized to any buckets, an empty result set will be returned." + + NON_GUARANTEED_ENDPOINT, + response = ExtensionMetadataContainer.class + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getExtensions( + @QueryParam("bundleType") + @ApiParam(value = "The type of bundles to return", allowableValues = BundleTypeValues.ALL_VALUES) + final BundleType bundleType, + @QueryParam("extensionType") + @ApiParam(value = "The type of extensions to return") + final ExtensionType extensionType, + @QueryParam("tag") + @ApiParam(value = "The tags to filter on, will be used in an OR statement") + final Set tags + ) { + + final ExtensionFilterParams filterParams = new ExtensionFilterParams.Builder() + .bundleType(bundleType) + .extensionType(extensionType) + .addTags(tags == null ? Collections.emptyList() : tags) + .build(); + + final SortedSet extensionMetadata = serviceFacade.getExtensionMetadata(filterParams); + + final ExtensionMetadataContainer container = new ExtensionMetadataContainer(); + container.setExtensions(extensionMetadata); + container.setNumResults(extensionMetadata.size()); + container.setFilterParams(filterParams); + + return Response.status(Response.Status.OK).entity(container).build(); + } + + @GET + @Path("provided-service-api") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get extensions providing service API", + notes = "Gets the metadata for extensions that provide the specified API and are part of bundles located in buckets the " + + "current user is authorized for. If the user is not authorized to any buckets, an empty result set will be returned." + + NON_GUARANTEED_ENDPOINT, + response = ExtensionMetadataContainer.class + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getExtensionsProvidingServiceAPI( + @QueryParam("className") + @ApiParam(value = "The name of the service API class", required = true) + final String className, + @QueryParam("groupId") + @ApiParam(value = "The groupId of the bundle containing the service API class", required = true) + final String groupId, + @QueryParam("artifactId") + @ApiParam(value = "The artifactId of the bundle containing the service API class", required = true) + final String artifactId, + @QueryParam("version") + @ApiParam(value = "The version of the bundle containing the service API class", required = true) + final String version + ) { + final ProvidedServiceAPI serviceAPI = new ProvidedServiceAPI(); + serviceAPI.setClassName(className); + serviceAPI.setGroupId(groupId); + serviceAPI.setArtifactId(artifactId); + serviceAPI.setVersion(version); + + final SortedSet extensionMetadata = serviceFacade.getExtensionMetadata(serviceAPI); + + final ExtensionMetadataContainer container = new ExtensionMetadataContainer(); + container.setExtensions(extensionMetadata); + container.setNumResults(extensionMetadata.size()); + + return Response.status(Response.Status.OK).entity(container).build(); + } + + @GET + @Path("/tags") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get extension tags", + notes = "Gets all the extension tags known to this NiFi Registry instance, along with the " + + "number of extensions that have the given tag." + NON_GUARANTEED_ENDPOINT, + response = TagCount.class, + responseContainer = "List" + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getTags() { + final SortedSet tags = serviceFacade.getExtensionTags(); + return Response.status(Response.Status.OK).entity(tags).build(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/FlowResource.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/FlowResource.java new file mode 100644 index 0000000000..c805988d6f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/FlowResource.java @@ -0,0 +1,226 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.api; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import io.swagger.annotations.Authorization; +import io.swagger.annotations.Extension; +import io.swagger.annotations.ExtensionProperty; +import org.apache.nifi.registry.event.EventService; +import org.apache.nifi.registry.field.Fields; +import org.apache.nifi.registry.flow.VersionedFlow; +import org.apache.nifi.registry.flow.VersionedFlowSnapshot; +import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata; +import org.apache.nifi.registry.web.service.ServiceFacade; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.Set; +import java.util.SortedSet; + +@Component +@Path("/flows") +@Api( + value = "flows", + description = "Gets metadata about flows.", + authorizations = { @Authorization("Authorization") } +) +public class FlowResource extends ApplicationResource { + + @Autowired + public FlowResource(final ServiceFacade serviceFacade, final EventService eventService) { + super(serviceFacade, eventService); + } + + @GET + @Path("fields") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get flow fields", + notes = "Retrieves the flow field names that can be used for searching or sorting on flows.", + response = Fields.class + ) + public Response getAvailableFlowFields() { + final Set flowFields = serviceFacade.getFlowFields(); + final Fields fields = new Fields(flowFields); + return Response.status(Response.Status.OK).entity(fields).build(); + } + + @GET + @Path("{flowId}") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get flow", + notes = "Gets a flow by id.", + nickname = "globalGetFlow", + response = VersionedFlow.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getFlow( + @PathParam("flowId") + @ApiParam("The flow identifier") + final String flowId) { + + final VersionedFlow flow = serviceFacade.getFlow(flowId); + return Response.status(Response.Status.OK).entity(flow).build(); + } + + @GET + @Path("{flowId}/versions") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get flow versions", + notes = "Gets summary information for all versions of a given flow. Versions are ordered newest->oldest.", + nickname = "globalGetFlowVersions", + response = VersionedFlowSnapshotMetadata.class, + responseContainer = "List", + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getFlowVersions( + @PathParam("flowId") + @ApiParam("The flow identifier") + final String flowId) { + + final SortedSet snapshots = serviceFacade.getFlowSnapshots(flowId); + return Response.status(Response.Status.OK).entity(snapshots).build(); + } + + @GET + @Path("{flowId}/versions/{versionNumber: \\d+}") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get flow version", + notes = "Gets the given version of a flow, including metadata and flow content.", + nickname = "globalGetFlowVersion", + response = VersionedFlowSnapshot.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getFlowVersion( + @PathParam("flowId") + @ApiParam("The flow identifier") + final String flowId, + @PathParam("versionNumber") + @ApiParam("The version number") + final Integer versionNumber) { + + final VersionedFlowSnapshot snapshot = serviceFacade.getFlowSnapshot(flowId, versionNumber); + return Response.status(Response.Status.OK).entity(snapshot).build(); + } + + @GET + @Path("{flowId}/versions/latest") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get latest flow version", + notes = "Gets the latest version of a flow, including metadata and flow content.", + nickname = "globalGetLatestFlowVersion", + response = VersionedFlowSnapshot.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getLatestFlowVersion( + @PathParam("flowId") + @ApiParam("The flow identifier") + final String flowId) { + + final VersionedFlowSnapshot lastSnapshot = serviceFacade.getLatestFlowSnapshot(flowId); + return Response.status(Response.Status.OK).entity(lastSnapshot).build(); + } + + @GET + @Path("{flowId}/versions/latest/metadata") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get latest flow version metadata", + notes = "Gets the metadata for the latest version of a flow.", + nickname = "globalGetLatestFlowVersionMetadata", + response = VersionedFlowSnapshotMetadata.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getLatestFlowVersionMetadata( + @PathParam("flowId") + @ApiParam("The flow identifier") + final String flowId) { + + final VersionedFlowSnapshotMetadata latestMetadata = serviceFacade.getLatestFlowSnapshotMetadata(flowId); + return Response.status(Response.Status.OK).entity(latestMetadata).build(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/HttpStatusMessages.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/HttpStatusMessages.java new file mode 100644 index 0000000000..a3ba939d29 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/HttpStatusMessages.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.api; + +class HttpStatusMessages { + + /* 4xx messages */ + static final String MESSAGE_400 = "NiFi Registry was unable to complete the request because it was invalid. The request should not be retried without modification."; + static final String MESSAGE_401 = "Client could not be authenticated."; + static final String MESSAGE_403 = "Client is not authorized to make this request."; + static final String MESSAGE_404 = "The specified resource could not be found."; + static final String MESSAGE_409 = "NiFi Registry was unable to complete the request because it assumes a server state that is not valid."; + + /* 5xx messages */ + static final String MESSAGE_500 = "NiFi Registry was unable to complete the request because an unexpected error occurred."; +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ItemResource.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ItemResource.java new file mode 100644 index 0000000000..9548074d16 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ItemResource.java @@ -0,0 +1,133 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.api; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import io.swagger.annotations.Authorization; +import io.swagger.annotations.Extension; +import io.swagger.annotations.ExtensionProperty; +import org.apache.nifi.registry.bucket.BucketItem; +import org.apache.nifi.registry.event.EventService; +import org.apache.nifi.registry.field.Fields; +import org.apache.nifi.registry.web.service.ServiceFacade; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import java.util.List; +import java.util.Set; + +@Component +@Path("/items") +@Api( + value = "items", + description = "Retrieve items across all buckets for which the user is authorized.", + authorizations = { @Authorization("Authorization") } +) +public class ItemResource extends ApplicationResource { + + @Context + UriInfo uriInfo; + + @Autowired + public ItemResource(final ServiceFacade serviceFacade, final EventService eventService) { + super(serviceFacade, eventService); + } + + + @GET + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get all items", + notes = "Get items across all buckets. The returned items will include only items from buckets for which the user is authorized. " + + "If the user is not authorized to any buckets, an empty list will be returned.", + response = BucketItem.class, + responseContainer = "List" + ) + @ApiResponses({ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401) }) + public Response getItems() { + // Service facade with return only items from authorized buckets + // Note: We don't explicitly check for access to (READ, /buckets) or + // (READ, /items ) because a user might have access to individual buckets + // without top-level access. For example, a user that has + // (READ, /buckets/bucket-id-1) but not access to /buckets should not + // get a 403 error returned from this endpoint. This has the side effect + // that a user with no access to any buckets gets an empty array returned + // from this endpoint instead of 403 as one might expect. + final List items = serviceFacade.getBucketItems(); + return Response.status(Response.Status.OK).entity(items).build(); + } + + @GET + @Path("{bucketId}") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get bucket items", + notes = "Gets the items located in the given bucket.", + response = BucketItem.class, + responseContainer = "List", + nickname = "getItemsInBucket", + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404) }) + public Response getItems( + @PathParam("bucketId") + @ApiParam("The bucket identifier") + final String bucketId) { + + final List items = serviceFacade.getBucketItems(bucketId); + return Response.status(Response.Status.OK).entity(items).build(); + } + + @GET + @Path("fields") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get item fields", + notes = "Retrieves the item field names for searching or sorting on bucket items.", + response = Fields.class + ) + public Response getAvailableBucketItemFields() { + final Set bucketFields = serviceFacade.getBucketItemFields(); + final Fields fields = new Fields(bucketFields); + return Response.status(Response.Status.OK).entity(fields).build(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/TenantResource.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/TenantResource.java new file mode 100644 index 0000000000..5d7052bffe --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/TenantResource.java @@ -0,0 +1,492 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.api; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import io.swagger.annotations.Authorization; +import io.swagger.annotations.Extension; +import io.swagger.annotations.ExtensionProperty; +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.authorization.User; +import org.apache.nifi.registry.authorization.UserGroup; +import org.apache.nifi.registry.event.EventFactory; +import org.apache.nifi.registry.event.EventService; +import org.apache.nifi.registry.revision.entity.RevisionInfo; +import org.apache.nifi.registry.revision.web.ClientIdParameter; +import org.apache.nifi.registry.revision.web.LongParameter; +import org.apache.nifi.registry.web.service.ServiceFacade; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.net.URI; +import java.util.List; + +/** + * RESTful endpoints for managing tenants, ie, users and user groups. + */ +@Component +@Path("tenants") +@Api( + value = "tenants", + description = "Endpoint for managing users and user groups.", + authorizations = { @Authorization("Authorization") } +) +public class TenantResource extends ApplicationResource { + + @Autowired + public TenantResource(final ServiceFacade serviceFacade, + final EventService eventService) { + super(serviceFacade, eventService); + } + + + // ---------- User endpoints -------------------------------------------------------------------------------------- + + /** + * Creates a new user. + * + * @param httpServletRequest request + * @param requestUser the user to create + * @return the user that was created + */ + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Path("users") + @ApiOperation( + value = "Create user", + notes = NON_GUARANTEED_ENDPOINT, + response = User.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "write"), + @ExtensionProperty(name = "resource", value = "/tenants") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response createUser( + @Context + final HttpServletRequest httpServletRequest, + @ApiParam(value = "The user configuration details.", required = true) + final User requestUser) { + + final User createdUser = serviceFacade.createUser(requestUser); + publish(EventFactory.userCreated(createdUser)); + + String locationUri = generateUserUri(createdUser); + return generateCreatedResponse(URI.create(locationUri), createdUser).build(); + } + + /** + * Retrieves all the of users in this NiFi. + * + * @return a list of users + */ + @GET + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @Path("users") + @ApiOperation( + value = "Get all users", + notes = NON_GUARANTEED_ENDPOINT, + response = User.class, + responseContainer = "List", + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/tenants") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getUsers() { + // get all the users + final List users = serviceFacade.getUsers(); + + // generate the response + return generateOkResponse(users).build(); + } + + /** + * Retrieves the specified user. + * + * @param identifier The id of the user to retrieve + * @return An userEntity. + */ + @GET + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @Path("users/{id}") + @ApiOperation( + value = "Get user", + notes = NON_GUARANTEED_ENDPOINT, + response = User.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/tenants") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getUser( + @ApiParam(value = "The user id.", required = true) + @PathParam("id") final String identifier) { + final User user = serviceFacade.getUser(identifier); + return generateOkResponse(user).build(); + } + + /** + * Updates a user. + * + * @param httpServletRequest request + * @param identifier The id of the user to update + * @param requestUser The user with updated fields. + * @return The updated user + */ + @PUT + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Path("users/{id}") + @ApiOperation( + value = "Update user", + notes = NON_GUARANTEED_ENDPOINT, + response = User.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "write"), + @ExtensionProperty(name = "resource", value = "/tenants") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response updateUser( + @Context + final HttpServletRequest httpServletRequest, + @ApiParam(value = "The user id.", required = true) + @PathParam("id") + final String identifier, + @ApiParam(value = "The user configuration details.", required = true) + final User requestUser) { + + if (requestUser == null) { + throw new IllegalArgumentException("User details must be specified when updating a user."); + } + if (!identifier.equals(requestUser.getIdentifier())) { + throw new IllegalArgumentException(String.format("The user id in the request body (%s) does not equal the " + + "user id of the requested resource (%s).", requestUser.getIdentifier(), identifier)); + } + + final User updatedUser = serviceFacade.updateUser(requestUser); + publish(EventFactory.userUpdated(updatedUser)); + return generateOkResponse(updatedUser).build(); + } + + /** + * Removes the specified user. + * + * @param httpServletRequest request + * @param identifier The id of the user to remove. + * @return A entity containing the client id and an updated revision. + */ + @DELETE + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @Path("users/{id}") + @ApiOperation( + value = "Delete user", + notes = NON_GUARANTEED_ENDPOINT, + response = User.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "delete"), + @ExtensionProperty(name = "resource", value = "/tenants") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response removeUser( + @Context + final HttpServletRequest httpServletRequest, + @ApiParam(value = "The version is used to verify the client is working with the latest version of the entity.", required = true) + @QueryParam(VERSION) + final LongParameter version, + @ApiParam(value = "If the client id is not specified, new one will be generated. This value (whether specified or generated) is included in the response.") + @QueryParam(CLIENT_ID) + @DefaultValue(StringUtils.EMPTY) + final ClientIdParameter clientId, + @ApiParam(value = "The user id.", required = true) + @PathParam("id") + final String identifier) { + + final RevisionInfo revisionInfo = getRevisionInfo(version, clientId); + final User user = serviceFacade.deleteUser(identifier, revisionInfo); + publish(EventFactory.userDeleted(user)); + return generateOkResponse(user).build(); + } + + + // ---------- User Group endpoints -------------------------------------------------------------------------------- + + /** + * Creates a new user group. + * + * @param httpServletRequest request + * @param requestUserGroup the user group to create + * @return the created user group + */ + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Path("user-groups") + @ApiOperation( + value = "Create user group", + notes = NON_GUARANTEED_ENDPOINT, + response = UserGroup.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "write"), + @ExtensionProperty(name = "resource", value = "/tenants") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response createUserGroup( + @Context + final HttpServletRequest httpServletRequest, + @ApiParam(value = "The user group configuration details.", required = true) + final UserGroup requestUserGroup) { + + final UserGroup createdGroup = serviceFacade.createUserGroup(requestUserGroup); + publish(EventFactory.userGroupCreated(createdGroup)); + + final String locationUri = generateUserGroupUri(createdGroup); + return generateCreatedResponse(URI.create(locationUri), createdGroup).build(); + } + + /** + * Retrieves all the of user groups in this NiFi. + * + * @return a list of all user groups in this NiFi. + */ + @GET + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @Path("user-groups") + @ApiOperation( + value = "Get user groups", + notes = NON_GUARANTEED_ENDPOINT, + response = UserGroup.class, + responseContainer = "List", + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/tenants") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getUserGroups() { + final List userGroups = serviceFacade.getUserGroups(); + return generateOkResponse(userGroups).build(); + } + + /** + * Retrieves the specified user group. + * + * @param identifier The id of the user group to retrieve + * @return An userGroupEntity. + */ + @GET + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @Path("user-groups/{id}") + @ApiOperation( + value = "Get user group", + notes = NON_GUARANTEED_ENDPOINT, + response = UserGroup.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "read"), + @ExtensionProperty(name = "resource", value = "/tenants") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getUserGroup( + @ApiParam(value = "The user group id.", required = true) + @PathParam("id") final String identifier) { + final UserGroup userGroup = serviceFacade.getUserGroup(identifier); + return generateOkResponse(userGroup).build(); + } + + /** + * Updates a user group. + * + * @param httpServletRequest request + * @param identifier The id of the user group to update. + * @param requestUserGroup The user group with updated fields. + * @return The resulting, updated user group. + */ + @PUT + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Path("user-groups/{id}") + @ApiOperation( + value = "Update user group", + notes = NON_GUARANTEED_ENDPOINT, + response = UserGroup.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "write"), + @ExtensionProperty(name = "resource", value = "/tenants") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response updateUserGroup( + @Context + final HttpServletRequest httpServletRequest, + @ApiParam(value = "The user group id.", required = true) + @PathParam("id") + final String identifier, + @ApiParam(value = "The user group configuration details.", required = true) + final UserGroup requestUserGroup) { + + if (requestUserGroup == null) { + throw new IllegalArgumentException("User group details must be specified to update a user group."); + } + if (!identifier.equals(requestUserGroup.getIdentifier())) { + throw new IllegalArgumentException(String.format("The user group id in the request body (%s) does not equal the " + + "user group id of the requested resource (%s).", requestUserGroup.getIdentifier(), identifier)); + } + + final UserGroup updatedUserGroup = serviceFacade.updateUserGroup(requestUserGroup); + publish(EventFactory.userGroupUpdated(updatedUserGroup)); + return generateOkResponse(updatedUserGroup).build(); + } + + /** + * Removes the specified user group. + * + * @param httpServletRequest request + * @param identifier The id of the user group to remove. + * @return The deleted user group. + */ + @DELETE + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @Path("user-groups/{id}") + @ApiOperation( + value = "Delete user group", + notes = NON_GUARANTEED_ENDPOINT, + response = UserGroup.class, + extensions = { + @Extension(name = "access-policy", properties = { + @ExtensionProperty(name = "action", value = "delete"), + @ExtensionProperty(name = "resource", value = "/tenants") }) + } + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response removeUserGroup( + @Context + final HttpServletRequest httpServletRequest, + @ApiParam(value = "The version is used to verify the client is working with the latest version of the entity.", required = true) + @QueryParam(VERSION) + final LongParameter version, + @ApiParam(value = "If the client id is not specified, new one will be generated. This value (whether specified or generated) is included in the response.") + @QueryParam(CLIENT_ID) + @DefaultValue(StringUtils.EMPTY) + final ClientIdParameter clientId, + @ApiParam(value = "The user group id.", required = true) + @PathParam("id") + final String identifier) { + + final RevisionInfo revisionInfo = getRevisionInfo(version, clientId); + final UserGroup userGroup = serviceFacade.deleteUserGroup(identifier, revisionInfo); + publish(EventFactory.userGroupDeleted(userGroup)); + return generateOkResponse(userGroup).build(); + } + + private String generateUserUri(final User user) { + return generateResourceUri("tenants", "users", user.getIdentifier()); + } + + private String generateUserGroupUri(final UserGroup userGroup) { + return generateResourceUri("tenants", "user-groups", userGroup.getIdentifier()); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/exception/UnauthorizedException.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/exception/UnauthorizedException.java new file mode 100644 index 0000000000..46e6fc9745 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/exception/UnauthorizedException.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.exception; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.security.authentication.IdentityProviderUsage; + +import java.util.List; + +/** + * An exception for a convenient way to create a 401 Unauthorized response + * using an exception mapper + */ +public class UnauthorizedException extends RuntimeException { + + private String[] wwwAuthenticateChallenge; + + public UnauthorizedException() { + } + + public UnauthorizedException(String message) { + super(message); + } + + public UnauthorizedException(String message, Throwable cause) { + super(message, cause); + } + + public UnauthorizedException(Throwable cause) { + super(cause); + } + + public UnauthorizedException withAuthenticateChallenge(IdentityProviderUsage.AuthType authType) { + wwwAuthenticateChallenge = new String[] { authType.getHttpAuthScheme() }; + return this; + } + + public UnauthorizedException withAuthenticateChallenge(List authTypes) { + wwwAuthenticateChallenge = new String[authTypes.size()]; + for (int i = 0; i < authTypes.size(); i++) { + wwwAuthenticateChallenge[i] = authTypes.get(i).getHttpAuthScheme(); + } + return this; + } + + public UnauthorizedException withAuthenticateChallenge(String authType) { + wwwAuthenticateChallenge = new String[] { authType }; + return this; + } + + public UnauthorizedException withAuthenticateChallenge(String[] authTypes) { + wwwAuthenticateChallenge = authTypes; + return this; + } + + public String getWwwAuthenticateChallenge() { + return StringUtils.join(wwwAuthenticateChallenge, ","); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/LinkBuilder.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/LinkBuilder.java new file mode 100644 index 0000000000..1e2bc2964a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/LinkBuilder.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.link; + +import javax.ws.rs.core.Link; + +/** + * Creates a Link for a given type. + * + * @param the type to create a link for + */ +public interface LinkBuilder { + + Link createLink(T t); + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/LinkService.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/LinkService.java new file mode 100644 index 0000000000..e79964e210 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/LinkService.java @@ -0,0 +1,385 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.link; + +import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.extension.bundle.Bundle; +import org.apache.nifi.registry.extension.bundle.BundleInfo; +import org.apache.nifi.registry.extension.bundle.BundleVersion; +import org.apache.nifi.registry.extension.bundle.BundleVersionMetadata; +import org.apache.nifi.registry.extension.component.ExtensionMetadata; +import org.apache.nifi.registry.extension.repo.ExtensionRepoArtifact; +import org.apache.nifi.registry.extension.repo.ExtensionRepoBucket; +import org.apache.nifi.registry.extension.repo.ExtensionRepoExtensionMetadata; +import org.apache.nifi.registry.extension.repo.ExtensionRepoGroup; +import org.apache.nifi.registry.extension.repo.ExtensionRepoVersionSummary; +import org.apache.nifi.registry.flow.VersionedFlow; +import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata; +import org.apache.nifi.registry.link.LinkableDocs; +import org.apache.nifi.registry.link.LinkableEntity; +import org.springframework.stereotype.Service; + +import javax.ws.rs.core.Link; +import javax.ws.rs.core.UriBuilder; +import java.net.URI; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@Service +public class LinkService { + + private static final String BUCKET_PATH = "buckets/{id}"; + + private static final String FLOW_PATH = "buckets/{bucketId}/flows/{flowId}"; + private static final String FLOW_SNAPSHOT_PATH = "buckets/{bucketId}/flows/{flowId}/versions/{versionNumber}"; + + private static final String EXTENSION_BUNDLE_PATH = "bundles/{bundleId}"; + private static final String EXTENSION_BUNDLE_VERSION_PATH = "bundles/{bundleId}/versions/{version}"; + private static final String EXTENSION_BUNDLE_VERSION_CONTENT_PATH = "bundles/{bundleId}/versions/{version}/content"; + private static final String EXTENSION_BUNDLE_VERSION_EXTENSION_PATH = "bundles/{bundleId}/versions/{version}/extensions/{name}"; + private static final String EXTENSION_BUNDLE_VERSION_EXTENSION_DOCS_PATH = "bundles/{bundleId}/versions/{version}/extensions/{name}/docs"; + + private static final String EXTENSION_REPO_BUCKET_PATH = "extension-repository/{bucketName}"; + private static final String EXTENSION_REPO_GROUP_PATH = "extension-repository/{bucketName}/{groupId}"; + private static final String EXTENSION_REPO_ARTIFACT_PATH = "extension-repository/{bucketName}/{groupId}/{artifactId}"; + private static final String EXTENSION_REPO_VERSION_PATH = "extension-repository/{bucketName}/{groupId}/{artifactId}/{version}"; + private static final String EXTENSION_REPO_EXTENSION_PATH = "extension-repository/{bucketName}/{groupId}/{artifactId}/{version}/extensions/{name}"; + private static final String EXTENSION_REPO_EXTENSION_DOCS_PATH = "extension-repository/{bucketName}/{groupId}/{artifactId}/{version}/extensions/{name}/docs"; + + + private static final LinkBuilder BUCKET_LINK_BUILDER = (bucket) -> { + if (bucket == null) { + return null; + } + + final URI uri = UriBuilder.fromPath(BUCKET_PATH) + .resolveTemplate("id", bucket.getIdentifier()) + .build(); + + return Link.fromUri(uri).rel("self").build(); + }; + + // -- Flow LinkBuilders + + private static final LinkBuilder FLOW_LINK_BUILDER = (versionedFlow -> { + if (versionedFlow == null) { + return null; + } + + final URI uri = UriBuilder.fromPath(FLOW_PATH) + .resolveTemplate("bucketId", versionedFlow.getBucketIdentifier()) + .resolveTemplate("flowId", versionedFlow.getIdentifier()) + .build(); + + return Link.fromUri(uri).rel("self").build(); + }); + + private static final LinkBuilder FLOW_SNAPSHOT_LINK_BUILDER = (snapshotMetadata) -> { + if (snapshotMetadata == null) { + return null; + } + + final URI uri = UriBuilder.fromPath(FLOW_SNAPSHOT_PATH) + .resolveTemplate("bucketId", snapshotMetadata.getBucketIdentifier()) + .resolveTemplate("flowId", snapshotMetadata.getFlowIdentifier()) + .resolveTemplate("versionNumber", snapshotMetadata.getVersion()) + .build(); + + return Link.fromUri(uri).rel("content").build(); + }; + + // -- Bundles & Extension LinkBuilders + + private static final LinkBuilder EXTENSION_BUNDLE_LINK_BUILDER = (extensionBundle -> { + if (extensionBundle == null) { + return null; + } + + final URI uri = UriBuilder.fromPath(EXTENSION_BUNDLE_PATH) + .resolveTemplate("bundleId", extensionBundle.getIdentifier()) + .build(); + + return Link.fromUri(uri).rel("self").build(); + }); + + private static final LinkBuilder EXTENSION_BUNDLE_VERSION_LINK_BUILDER = (bundleVersion -> { + if (bundleVersion == null) { + return null; + } + + final URI uri = UriBuilder.fromPath(EXTENSION_BUNDLE_VERSION_PATH) + .resolveTemplate("bundleId", bundleVersion.getBundleId()) + .resolveTemplate("version", bundleVersion.getVersion()) + .build(); + + return Link.fromUri(uri).rel("self").build(); + }); + + private static final LinkBuilder EXTENSION_BUNDLE_VERSION_CONTENT_LINK_BUILDER = (bundleVersion -> { + if (bundleVersion == null) { + return null; + } + + final URI uri = UriBuilder.fromPath(EXTENSION_BUNDLE_VERSION_CONTENT_PATH) + .resolveTemplate("bundleId", bundleVersion.getBundle().getIdentifier()) + .resolveTemplate("version", bundleVersion.getVersionMetadata().getVersion()) + .build(); + + return Link.fromUri(uri).rel("self").build(); + }); + + private static final LinkBuilder EXTENSION_METADATA_LINK_BUILDER = (extensionMetadata -> { + if (extensionMetadata == null) { + return null; + } + + final URI uri = UriBuilder.fromPath(EXTENSION_BUNDLE_VERSION_EXTENSION_PATH) + .resolveTemplate("bundleId", extensionMetadata.getBundleInfo().getBundleId()) + .resolveTemplate("version", extensionMetadata.getBundleInfo().getVersion()) + .resolveTemplate("name", extensionMetadata.getName()) + .build(); + + return Link.fromUri(uri).rel("self").build(); + }); + + private static final LinkBuilder EXTENSION_METADATA_DOCS_LINK_BUILDER = (extensionMetadata -> { + if (extensionMetadata == null) { + return null; + } + + final URI uri = UriBuilder.fromPath(EXTENSION_BUNDLE_VERSION_EXTENSION_DOCS_PATH) + .resolveTemplate("bundleId", extensionMetadata.getBundleInfo().getBundleId()) + .resolveTemplate("version", extensionMetadata.getBundleInfo().getVersion()) + .resolveTemplate("name", extensionMetadata.getName()) + .build(); + + return Link.fromUri(uri).rel("docs").build(); + }); + + // -- Extension Repo LinkBuilders + + private static final LinkBuilder EXTENSION_REPO_BUCKET_LINK_BUILDER = (extensionRepoBucket -> { + if (extensionRepoBucket == null) { + return null; + } + + final URI uri = UriBuilder.fromPath(EXTENSION_REPO_BUCKET_PATH) + .resolveTemplate("bucketName", extensionRepoBucket.getBucketName()) + .build(); + + return Link.fromUri(uri).rel("self").build(); + }); + + private static final LinkBuilder EXTENSION_REPO_GROUP_LINK_BUILDER = (extensionRepoGroup -> { + if (extensionRepoGroup == null) { + return null; + } + + final URI uri = UriBuilder.fromPath(EXTENSION_REPO_GROUP_PATH) + .resolveTemplate("bucketName", extensionRepoGroup.getBucketName()) + .resolveTemplate("groupId", extensionRepoGroup.getGroupId()) + .build(); + + return Link.fromUri(uri).rel("self").build(); + }); + + private static final LinkBuilder EXTENSION_REPO_ARTIFACT_LINK_BUILDER = (extensionRepoArtifact -> { + if (extensionRepoArtifact == null) { + return null; + } + + final URI uri = UriBuilder.fromPath(EXTENSION_REPO_ARTIFACT_PATH) + .resolveTemplate("bucketName", extensionRepoArtifact.getBucketName()) + .resolveTemplate("groupId", extensionRepoArtifact.getGroupId()) + .resolveTemplate("artifactId", extensionRepoArtifact.getArtifactId()) + .build(); + + return Link.fromUri(uri).rel("self").build(); + }); + + private static final LinkBuilder EXTENSION_REPO_VERSION_LINK_BUILDER = (extensionRepoVersion -> { + if (extensionRepoVersion == null) { + return null; + } + + final URI uri = UriBuilder.fromPath(EXTENSION_REPO_VERSION_PATH) + .resolveTemplate("bucketName", extensionRepoVersion.getBucketName()) + .resolveTemplate("groupId", extensionRepoVersion.getGroupId()) + .resolveTemplate("artifactId", extensionRepoVersion.getArtifactId()) + .resolveTemplate("version", extensionRepoVersion.getVersion()) + .build(); + + return Link.fromUri(uri).rel("self").build(); + }); + + private static final LinkBuilder EXTENSION_REPO_EXTENSION_METADATA_LINK_BUILDER = (extensionMetadata -> { + if (extensionMetadata == null + || extensionMetadata.getExtensionMetadata() == null + || extensionMetadata.getExtensionMetadata().getBundleInfo() == null) { + return null; + } + + final ExtensionMetadata metadata = extensionMetadata.getExtensionMetadata(); + final BundleInfo bundleInfo = metadata.getBundleInfo(); + + final URI uri = UriBuilder.fromPath(EXTENSION_REPO_EXTENSION_PATH) + .resolveTemplate("bucketName", bundleInfo.getBucketName()) + .resolveTemplate("groupId", bundleInfo.getGroupId()) + .resolveTemplate("artifactId", bundleInfo.getArtifactId()) + .resolveTemplate("version", bundleInfo.getVersion()) + .resolveTemplate("name", metadata.getName()) + .build(); + + return Link.fromUri(uri).rel("self").build(); + }); + + private static final LinkBuilder EXTENSION_REPO_EXTENSION_METADATA_DOCS_LINK_BUILDER = (extensionMetadata -> { + if (extensionMetadata == null + || extensionMetadata.getExtensionMetadata() == null + || extensionMetadata.getExtensionMetadata().getBundleInfo() == null) { + return null; + } + + final ExtensionMetadata metadata = extensionMetadata.getExtensionMetadata(); + final BundleInfo bundleInfo = metadata.getBundleInfo(); + + final URI uri = UriBuilder.fromPath(EXTENSION_REPO_EXTENSION_DOCS_PATH) + .resolveTemplate("bucketName", bundleInfo.getBucketName()) + .resolveTemplate("groupId", bundleInfo.getGroupId()) + .resolveTemplate("artifactId", bundleInfo.getArtifactId()) + .resolveTemplate("version", bundleInfo.getVersion()) + .resolveTemplate("name", metadata.getName()) + .build(); + + return Link.fromUri(uri).rel("docs").build(); + }); + + + private static final Map LINK_BUILDERS; + static { + final Map builderMap = new HashMap<>(); + // -- buckets + builderMap.put(Bucket.class, BUCKET_LINK_BUILDER); + + // -- flows + builderMap.put(VersionedFlow.class, FLOW_LINK_BUILDER); + builderMap.put(VersionedFlowSnapshotMetadata.class, FLOW_SNAPSHOT_LINK_BUILDER); + + // -- bundles & extensions + builderMap.put(Bundle.class, EXTENSION_BUNDLE_LINK_BUILDER); + builderMap.put(BundleVersionMetadata.class, EXTENSION_BUNDLE_VERSION_LINK_BUILDER); + builderMap.put(BundleVersion.class, EXTENSION_BUNDLE_VERSION_CONTENT_LINK_BUILDER); + builderMap.put(ExtensionMetadata.class, EXTENSION_METADATA_LINK_BUILDER); + + // -- extension repo + builderMap.put(ExtensionRepoBucket.class, EXTENSION_REPO_BUCKET_LINK_BUILDER); + builderMap.put(ExtensionRepoGroup.class, EXTENSION_REPO_GROUP_LINK_BUILDER); + builderMap.put(ExtensionRepoArtifact.class, EXTENSION_REPO_ARTIFACT_LINK_BUILDER); + builderMap.put(ExtensionRepoVersionSummary.class, EXTENSION_REPO_VERSION_LINK_BUILDER); + builderMap.put(ExtensionRepoExtensionMetadata.class, EXTENSION_REPO_EXTENSION_METADATA_LINK_BUILDER); + + LINK_BUILDERS = Collections.unmodifiableMap(builderMap); + } + + private static final Map DOCS_LINK_BUILDERS; + static { + final Map builderMap = new HashMap<>(); + builderMap.put(ExtensionMetadata.class, EXTENSION_METADATA_DOCS_LINK_BUILDER); + builderMap.put(ExtensionRepoExtensionMetadata.class, EXTENSION_REPO_EXTENSION_METADATA_DOCS_LINK_BUILDER); + DOCS_LINK_BUILDERS = Collections.unmodifiableMap(builderMap); + } + + + public void populateLinks(final E entity) { + final LinkBuilder linkBuilder = LINK_BUILDERS.get(entity.getClass()); + if (linkBuilder == null) { + throw new IllegalArgumentException("No LinkBuilder found for " + entity.getClass().getCanonicalName()); + } + + final Link link = linkBuilder.createLink(entity); + entity.setLink(link); + + if (entity instanceof LinkableDocs) { + final LinkBuilder docsLinkBuilder = DOCS_LINK_BUILDERS.get(entity.getClass()); + if (docsLinkBuilder == null) { + throw new IllegalArgumentException("No documentation LinkBuilder found for " + entity.getClass().getCanonicalName()); + } + + final Link docsLink = docsLinkBuilder.createLink(entity); + final LinkableDocs docsEntity = (LinkableDocs) entity; + docsEntity.setLinkDocs(docsLink); + } + } + + public void populateLinks(final Iterable entities) { + if (entities == null) { + return; + } + + entities.forEach(e -> populateLinks(e)); + } + + public void populateFullLinks(final E entity, final URI baseUri) { + final LinkBuilder linkBuilder = LINK_BUILDERS.get(entity.getClass()); + if (linkBuilder == null) { + throw new IllegalArgumentException("No LinkBuilder found for " + entity.getClass().getCanonicalName()); + } + + if (baseUri == null) { + throw new IllegalArgumentException("Base URI cannot be null"); + } + + final Link relativeLink = linkBuilder.createLink(entity); + final Link fullLink = getFullLink(baseUri, relativeLink); + entity.setLink(fullLink); + + if (entity instanceof LinkableDocs) { + final LinkBuilder docsLinkBuilder = DOCS_LINK_BUILDERS.get(entity.getClass()); + if (docsLinkBuilder == null) { + throw new IllegalArgumentException("No documentation LinkBuilder found for " + entity.getClass().getCanonicalName()); + } + + final Link relativeDocsLink = docsLinkBuilder.createLink(entity); + final Link fullDocsLink = getFullLink(baseUri, relativeDocsLink); + + final LinkableDocs docsEntity = (LinkableDocs) entity; + docsEntity.setLinkDocs(fullDocsLink); + } + } + + public void populateFullLinks(final Iterable entities, final URI baseUri) { + if (entities == null) { + return; + } + + entities.forEach(e -> populateFullLinks(e, baseUri)); + } + + private Link getFullLink(final URI baseUri, final Link relativeLink) { + final URI relativeUri = relativeLink.getUri(); + + final URI fullUri = UriBuilder.fromUri(baseUri) + .path(relativeUri.getPath()) + .build(); + + return Link.fromUri(fullUri) + .rel(relativeLink.getRel()) + .build(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AccessDeniedExceptionMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AccessDeniedExceptionMapper.java new file mode 100644 index 0000000000..5b9e3eecaa --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AccessDeniedExceptionMapper.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.mapper; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException; +import org.apache.nifi.registry.security.authorization.user.NiFiUser; +import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +/** + * Maps access denied exceptions into a client response. + */ +@Component +@Provider +public class AccessDeniedExceptionMapper implements ExceptionMapper { + + private static final Logger logger = LoggerFactory.getLogger(AccessDeniedExceptionMapper.class); + + @Override + public Response toResponse(AccessDeniedException exception) { + // get the current user + NiFiUser user = NiFiUserUtils.getNiFiUser(); + + // if the user was authenticated - forbidden, otherwise unauthorized... the user may be null if the + // AccessDeniedException was thrown from a /access endpoint that isn't subject to the security + // filter chain. for instance, one that performs kerberos negotiation + final Status status; + if (user == null || user.isAnonymous()) { + status = Status.UNAUTHORIZED; + } else { + status = Status.FORBIDDEN; + } + + final String identity; + if (user == null) { + identity = ""; + } else { + identity = user.toString(); + } + + logger.info(String.format("%s does not have permission to access the requested resource. %s Returning %s response.", identity, exception.getMessage(), status)); + + if (logger.isDebugEnabled()) { + logger.debug(StringUtils.EMPTY, exception); + } + + return Response.status(status) + .entity(String.format("%s Contact the system administrator.", exception.getMessage())) + .type("text/plain") + .build(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AdministrationExceptionMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AdministrationExceptionMapper.java new file mode 100644 index 0000000000..b97222c4ae --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AdministrationExceptionMapper.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.mapper; + +import org.apache.nifi.registry.exception.AdministrationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +/** + * Maps administration exceptions into client responses. + */ +@Component +@Provider +public class AdministrationExceptionMapper implements ExceptionMapper { + + private static final Logger logger = LoggerFactory.getLogger(AdministrationExceptionMapper.class); + + @Override + public Response toResponse(AdministrationException exception) { + // log the error + logger.error(String.format("%s. Returning %s response.", exception, Response.Status.INTERNAL_SERVER_ERROR), exception); + + // generate the response + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(exception.getMessage()).type("text/plain").build(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AuthenticationCredentialsNotFoundExceptionMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AuthenticationCredentialsNotFoundExceptionMapper.java new file mode 100644 index 0000000000..ee7fb74f29 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AuthenticationCredentialsNotFoundExceptionMapper.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.mapper; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.stereotype.Component; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +/** + * Maps exceptions that occur because no valid credentials were found into the corresponding response. + */ +@Component +@Provider +public class AuthenticationCredentialsNotFoundExceptionMapper implements ExceptionMapper { + + private static final Logger logger = LoggerFactory.getLogger(AuthenticationCredentialsNotFoundExceptionMapper.class); + + @Override + public Response toResponse(AuthenticationCredentialsNotFoundException exception) { + // log the error + logger.info(String.format("No valid credentials were found in the request: %s. Returning %s response.", exception, Response.Status.FORBIDDEN)); + + if (logger.isDebugEnabled()) { + logger.debug(StringUtils.EMPTY, exception); + } + + return Response.status(Response.Status.FORBIDDEN).entity("Access is denied.").type("text/plain").build(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AuthorizationAccessExceptionMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AuthorizationAccessExceptionMapper.java new file mode 100644 index 0000000000..ff4b7ec245 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AuthorizationAccessExceptionMapper.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.mapper; + +import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +/** + * Maps authorization access exceptions into client responses. + */ +@Component +@Provider +public class AuthorizationAccessExceptionMapper implements ExceptionMapper { + + private static final Logger logger = LoggerFactory.getLogger(AuthorizationAccessExceptionMapper.class); + + @Override + public Response toResponse(AuthorizationAccessException e) { + // log the error + logger.error(String.format("%s. Returning %s response.", e, Response.Status.INTERNAL_SERVER_ERROR), e); + + // generate the response + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).type("text/plain").build(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/BadRequestExceptionMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/BadRequestExceptionMapper.java new file mode 100644 index 0000000000..2577fa0edd --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/BadRequestExceptionMapper.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.mapper; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.ws.rs.BadRequestException; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +/** + * Maps exceptions into client responses. + */ +@Component +@Provider +public class BadRequestExceptionMapper implements ExceptionMapper { + + private static final Logger logger = LoggerFactory.getLogger(BadRequestExceptionMapper.class); + + @Override + public Response toResponse(BadRequestException exception) { + logger.info(String.format("%s. Returning %s response.", exception, Response.Status.BAD_REQUEST)); + + if (logger.isDebugEnabled()) { + logger.debug(StringUtils.EMPTY, exception); + } + + return Response.status(Response.Status.BAD_REQUEST).entity(exception.getMessage()).type("text/plain").build(); + } + +} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/ConstraintViolationExceptionMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/ConstraintViolationExceptionMapper.java new file mode 100644 index 0000000000..dd855b70b5 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/ConstraintViolationExceptionMapper.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.mapper; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.Path; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +@Component +@Provider +public class ConstraintViolationExceptionMapper implements ExceptionMapper { + + private static final Logger logger = LoggerFactory.getLogger(ConstraintViolationExceptionMapper.class); + + @Override + public Response toResponse(ConstraintViolationException exception) { + // start with the overall message which will be something like "Cannot create xyz" + final StringBuilder errorMessage = new StringBuilder(exception.getMessage()).append(" - "); + + boolean first = true; + for (final ConstraintViolation violation : exception.getConstraintViolations()) { + if (!first) { + errorMessage.append(", "); + } + first = false; + + // lastNode should end up as the field that failed validation + Path.Node lastNode = null; + for (final Path.Node node : violation.getPropertyPath()) { + lastNode = node; + } + + // append something like "xyz must not be..." + errorMessage.append(lastNode.getName()).append(" ").append(violation.getMessage()); + } + + logger.info(String.format("%s. Returning %s response.", errorMessage, Response.Status.BAD_REQUEST)); + if (logger.isDebugEnabled()) { + logger.debug(StringUtils.EMPTY, exception); + } + + return Response.status(Response.Status.BAD_REQUEST).entity(errorMessage.toString()).type("text/plain").build(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/IllegalArgumentExceptionMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/IllegalArgumentExceptionMapper.java new file mode 100644 index 0000000000..7186c0f2da --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/IllegalArgumentExceptionMapper.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.mapper; + +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +/** + * Maps exceptions into client responses. + */ +@Component +@Provider +public class IllegalArgumentExceptionMapper implements ExceptionMapper { + + private static final Logger logger = LoggerFactory.getLogger(IllegalArgumentExceptionMapper.class); + + @Override + public Response toResponse(IllegalArgumentException exception) { + logger.info(String.format("%s. Returning %s response.", exception, Response.Status.BAD_REQUEST)); + + if (logger.isDebugEnabled()) { + logger.debug(StringUtils.EMPTY, exception); + } + + return Response.status(Status.BAD_REQUEST).entity(exception.getMessage()).type("text/plain").build(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/IllegalStateExceptionMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/IllegalStateExceptionMapper.java new file mode 100644 index 0000000000..11fc09dedc --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/IllegalStateExceptionMapper.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.mapper; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +@Component +@Provider +public class IllegalStateExceptionMapper implements ExceptionMapper { + + private static final Logger logger = LoggerFactory.getLogger(IllegalStateExceptionMapper.class); + + @Override + public Response toResponse(IllegalStateException exception) { + // log the error + logger.info(String.format("%s. Returning %s response.", exception, Response.Status.CONFLICT)); + + if (logger.isDebugEnabled()) { + logger.debug(StringUtils.EMPTY, exception); + } + + return Response.status(Response.Status.CONFLICT).entity(exception.getMessage()).type("text/plain").build(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/InvalidAuthenticationExceptionMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/InvalidAuthenticationExceptionMapper.java new file mode 100644 index 0000000000..15bb2272fd --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/InvalidAuthenticationExceptionMapper.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.mapper; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.web.security.authentication.exception.InvalidAuthenticationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +/** + * Maps access denied exceptions into a client response. + */ +@Component +@Provider +public class InvalidAuthenticationExceptionMapper implements ExceptionMapper { + + private static final Logger logger = LoggerFactory.getLogger(InvalidAuthenticationExceptionMapper.class); + + @Override + public Response toResponse(InvalidAuthenticationException exception) { + if (logger.isDebugEnabled()) { + logger.debug(StringUtils.EMPTY, exception); + } + + return Response.status(Response.Status.UNAUTHORIZED).entity(exception.getMessage()).type("text/plain").build(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/InvalidRevisionExceptionMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/InvalidRevisionExceptionMapper.java new file mode 100644 index 0000000000..e9fd628c66 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/InvalidRevisionExceptionMapper.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.mapper; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.revision.api.InvalidRevisionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +/** + * Maps invalid revision exceptions into client responses. + */ +@Provider +public class InvalidRevisionExceptionMapper implements ExceptionMapper { + + private static final Logger logger = LoggerFactory.getLogger(InvalidRevisionExceptionMapper.class); + + @Override + public Response toResponse(final InvalidRevisionException exception) { + // log the error + logger.info(String.format("%s. Returning %s response.", exception, Response.Status.BAD_REQUEST)); + + if (logger.isDebugEnabled()) { + logger.debug(StringUtils.EMPTY, exception); + } + + return Response.status(Response.Status.BAD_REQUEST).entity(exception.getMessage()).type("text/plain").build(); + } + +} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/NiFiRegistryJsonProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/NiFiRegistryJsonProvider.java new file mode 100644 index 0000000000..10a044aca3 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/NiFiRegistryJsonProvider.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.mapper; + +import org.apache.nifi.registry.serialization.jackson.ObjectMapperProvider; +import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJaxbJsonProvider; +import org.springframework.stereotype.Component; + +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.ext.Provider; + +@Component +@Provider +@Produces(MediaType.APPLICATION_JSON) +public class NiFiRegistryJsonProvider extends JacksonJaxbJsonProvider { + + public NiFiRegistryJsonProvider() { + super(); + setMapper(ObjectMapperProvider.getMapper()); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/NotAllowedExceptionMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/NotAllowedExceptionMapper.java new file mode 100644 index 0000000000..923773576a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/NotAllowedExceptionMapper.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.mapper; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.ws.rs.NotAllowedException; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +/** + * Maps exceptions into client responses. + */ +@Component +@Provider +public class NotAllowedExceptionMapper implements ExceptionMapper { + + private static final Logger logger = LoggerFactory.getLogger(NotAllowedExceptionMapper.class); + + @Override + public Response toResponse(NotAllowedException exception) { + logger.info(String.format("%s. Returning %s response.", exception, Status.METHOD_NOT_ALLOWED)); + logger.debug(StringUtils.EMPTY, exception); + return Response.status(Status.METHOD_NOT_ALLOWED).entity(exception.getMessage()).type("text/plain").build(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/NotFoundExceptionMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/NotFoundExceptionMapper.java new file mode 100644 index 0000000000..8abd4c07f7 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/NotFoundExceptionMapper.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.mapper; + +import javax.ws.rs.NotFoundException; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +/** + * Maps not found exceptions into client responses. + */ +@Component +@Provider +public class NotFoundExceptionMapper implements ExceptionMapper { + + private static final Logger logger = LoggerFactory.getLogger(NotFoundExceptionMapper.class); + + @Override + public Response toResponse(NotFoundException exception) { + // log the error + logger.info(String.format("%s. Returning %s response.", exception, Response.Status.NOT_FOUND)); + + if (logger.isDebugEnabled()) { + logger.debug(StringUtils.EMPTY, exception); + } + + return Response.status(Response.Status.NOT_FOUND).entity(exception.getMessage()).type("text/plain").build(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/QueryParamExceptionMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/QueryParamExceptionMapper.java new file mode 100644 index 0000000000..30dbe1c6a9 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/QueryParamExceptionMapper.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.mapper; + +import org.apache.commons.lang3.StringUtils; +import org.glassfish.jersey.server.ParamException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +@Component +@Provider +public class QueryParamExceptionMapper implements ExceptionMapper { + + private static final Logger logger = LoggerFactory.getLogger(QueryParamExceptionMapper.class); + + @Override + public Response toResponse(ParamException.QueryParamException exception) { + logger.info(String.format("%s. Returning %s response.", exception, Response.Status.BAD_REQUEST)); + + if (logger.isDebugEnabled()) { + logger.debug(StringUtils.EMPTY, exception); + } + + final String message = "Invalid value for " + exception.getParameterName(); + return Response.status(Response.Status.BAD_REQUEST).entity(message).type("text/plain").build(); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/ResourceNotFoundExceptionMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/ResourceNotFoundExceptionMapper.java new file mode 100644 index 0000000000..a71452d063 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/ResourceNotFoundExceptionMapper.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.mapper; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.exception.ResourceNotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +@Component +@Provider +public class ResourceNotFoundExceptionMapper implements ExceptionMapper { + + private static final Logger logger = LoggerFactory.getLogger(ResourceNotFoundExceptionMapper.class); + + @Override + public Response toResponse(ResourceNotFoundException exception) { + // log the error + logger.info(String.format("%s. Returning %s response.", exception, Response.Status.NOT_FOUND)); + + if (logger.isDebugEnabled()) { + logger.debug(StringUtils.EMPTY, exception); + } + + return Response.status(Response.Status.NOT_FOUND).entity(exception.getMessage()).type("text/plain").build(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/SerializationExceptionMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/SerializationExceptionMapper.java new file mode 100644 index 0000000000..8f53141e52 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/SerializationExceptionMapper.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.mapper; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.serialization.SerializationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +@Component +@Provider +public class SerializationExceptionMapper implements ExceptionMapper { + + private static final Logger logger = LoggerFactory.getLogger(SerializationExceptionMapper.class); + + @Override + public Response toResponse(SerializationException exception) { + // log the error + logger.info(String.format("%s. Returning %s response.", exception, Response.Status.INTERNAL_SERVER_ERROR)); + + if (logger.isDebugEnabled()) { + logger.debug(StringUtils.EMPTY, exception); + } + + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(exception.getMessage()).type("text/plain").build(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/ThrowableMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/ThrowableMapper.java new file mode 100644 index 0000000000..4b434ed355 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/ThrowableMapper.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.mapper; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +/** + * Maps unknown node exceptions into client responses. + */ +@Component +@Provider +public class ThrowableMapper implements ExceptionMapper { + + private static final Logger logger = LoggerFactory.getLogger(ThrowableMapper.class); + + @Override + public Response toResponse(Throwable exception) { + // log the error + logger.error(String.format("An unexpected error has occurred: %s. Returning %s response.", exception, Response.Status.INTERNAL_SERVER_ERROR), exception); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("An unexpected error has occurred. Please check the logs for additional details.").type("text/plain").build(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/UnauthorizedExceptionMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/UnauthorizedExceptionMapper.java new file mode 100644 index 0000000000..1c67e94a70 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/UnauthorizedExceptionMapper.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.mapper; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.web.exception.UnauthorizedException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +/** + * Maps Unauthorized exceptions into client responses that set the WWW-Authenticate header + * with a list of challenges (i.e., acceptable auth scheme types). + */ +@Component +@Provider +public class UnauthorizedExceptionMapper implements ExceptionMapper { + + private static final Logger logger = LoggerFactory.getLogger(UnauthorizedExceptionMapper.class); + + private static final String AUTHENTICATION_CHALLENGE_HEADER_NAME = "WWW-Authenticate"; + + @Override + public Response toResponse(UnauthorizedException exception) { + + logger.info("{}. Returning {} response.", exception, Response.Status.UNAUTHORIZED); + logger.debug(StringUtils.EMPTY, exception); + + final Response.ResponseBuilder response = Response.status(Response.Status.UNAUTHORIZED); + if (exception.getWwwAuthenticateChallenge() != null) { + response.header(AUTHENTICATION_CHALLENGE_HEADER_NAME, exception.getWwwAuthenticateChallenge()); + } + response.entity(exception.getMessage()).type("text/plain"); + return response.build(); + + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/UntrustedProxyExceptionMapper.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/UntrustedProxyExceptionMapper.java new file mode 100644 index 0000000000..453dbd5026 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/UntrustedProxyExceptionMapper.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.mapper; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.security.authorization.UntrustedProxyException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +/** + * Maps an UntrustedProxyException to a FORBIDDEN response. + */ +@Component +@Provider +public class UntrustedProxyExceptionMapper implements ExceptionMapper { + + private static Logger LOGGER = LoggerFactory.getLogger(UntrustedProxyException.class); + + @Override + public Response toResponse(final UntrustedProxyException exception) { + LOGGER.info("{}. Returning {} response.", exception, Response.Status.FORBIDDEN); + LOGGER.debug(StringUtils.EMPTY, exception); + + return Response.status(Response.Status.FORBIDDEN) + .entity(exception.getMessage()) + .type("text/plain") + .build(); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistryMasterKeyProviderFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistryMasterKeyProviderFactory.java new file mode 100644 index 0000000000..8026ccabbd --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistryMasterKeyProviderFactory.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.security; + +import org.apache.nifi.registry.NiFiRegistryApiApplication; +import org.apache.nifi.registry.security.crypto.CryptoKeyProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.context.ServletContextAware; + +import javax.servlet.ServletContext; + +@Configuration +public class NiFiRegistryMasterKeyProviderFactory implements ServletContextAware { + + private static final Logger logger = LoggerFactory.getLogger(NiFiRegistryMasterKeyProviderFactory.class); + + private CryptoKeyProvider masterKeyProvider = null; + + @Bean + public CryptoKeyProvider getNiFiRegistryMasterKeyProvider() { + return masterKeyProvider; + } + + @Override + public void setServletContext(ServletContext servletContext) { + Object rawKeyProviderObject = servletContext.getAttribute(NiFiRegistryApiApplication.NIFI_REGISTRY_MASTER_KEY_ATTRIBUTE); + + if (rawKeyProviderObject == null) { + logger.warn("Value of {} was null. " + + "{} bean will not be available in Application Context, so any attempt to load protected property values may fail.", + NiFiRegistryApiApplication.NIFI_REGISTRY_MASTER_KEY_ATTRIBUTE, + CryptoKeyProvider.class.getSimpleName()); + return; + } + + if (!(rawKeyProviderObject instanceof CryptoKeyProvider)) { + logger.warn("Expected value of {} to be of type {}, but instead got {}. " + + "{} bean will NOT be available in Application Context, so any attempt to load protected property values may fail.", + NiFiRegistryApiApplication.NIFI_REGISTRY_MASTER_KEY_ATTRIBUTE, + CryptoKeyProvider.class.getName(), + rawKeyProviderObject.getClass().getName(), + CryptoKeyProvider.class.getSimpleName()); + return; + } + + logger.info("Updating Application Context with {} bean for obtaining NiFi Registry master key.", CryptoKeyProvider.class.getSimpleName()); + masterKeyProvider = (CryptoKeyProvider) rawKeyProviderObject; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java new file mode 100644 index 0000000000..d1b6012a2d --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java @@ -0,0 +1,204 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.security; + +import org.apache.nifi.registry.security.authorization.Authorizer; +import org.apache.nifi.registry.security.authorization.resource.ResourceType; +import org.apache.nifi.registry.security.identity.IdentityMapper; +import org.apache.nifi.registry.service.AuthorizationService; +import org.apache.nifi.registry.web.security.authentication.AnonymousIdentityFilter; +import org.apache.nifi.registry.web.security.authentication.IdentityAuthenticationProvider; +import org.apache.nifi.registry.web.security.authentication.IdentityFilter; +import org.apache.nifi.registry.web.security.authentication.jwt.JwtIdentityProvider; +import org.apache.nifi.registry.web.security.authentication.x509.X509IdentityAuthenticationProvider; +import org.apache.nifi.registry.web.security.authentication.x509.X509IdentityProvider; +import org.apache.nifi.registry.web.security.authorization.ResourceAuthorizationFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; +import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * NiFi Registry Web Api Spring security + */ +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class NiFiRegistrySecurityConfig extends WebSecurityConfigurerAdapter { + + private static final Logger logger = LoggerFactory.getLogger(NiFiRegistrySecurityConfig.class); + + @Autowired + private IdentityMapper identityMapper; + + @Autowired + private AuthorizationService authorizationService; + + @Autowired + private Authorizer authorizer; + + private AnonymousIdentityFilter anonymousAuthenticationFilter = new AnonymousIdentityFilter(); + + @Autowired + private X509IdentityProvider x509IdentityProvider; + private IdentityFilter x509AuthenticationFilter; + private IdentityAuthenticationProvider x509AuthenticationProvider; + + @Autowired + private JwtIdentityProvider jwtIdentityProvider; + private IdentityFilter jwtAuthenticationFilter; + private IdentityAuthenticationProvider jwtAuthenticationProvider; + + private ResourceAuthorizationFilter resourceAuthorizationFilter; + + public NiFiRegistrySecurityConfig() { + super(true); // disable defaults + } + + @Override + public void configure(WebSecurity webSecurity) throws Exception { + // allow any client to access the endpoint for logging in to generate an access token + webSecurity.ignoring().antMatchers( "/access/token", "/access/token/kerberos", + "/access/oidc/exchange", "/access/oidc/callback", "/access/oidc/request", "/access/token/identity-provider" ); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .rememberMe().disable() + .authorizeRequests() + .anyRequest().fullyAuthenticated() + .and() + .exceptionHandling() + .authenticationEntryPoint(http401AuthenticationEntryPoint()) + .and() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS); + + // Apply security headers for registry API. Security headers for docs and UI are applied with Jetty filters in registry-core. + http.headers().xssProtection(); + http.headers().contentSecurityPolicy("frame-ancestors 'self'"); + http.headers().httpStrictTransportSecurity().maxAgeInSeconds(31540000); + http.headers().frameOptions().sameOrigin(); + + // x509 + http.addFilterBefore(x509AuthenticationFilter(), AnonymousAuthenticationFilter.class); + + // jwt + http.addFilterBefore(jwtAuthenticationFilter(), AnonymousAuthenticationFilter.class); + + // otp + // todo, if needed one-time password auth filter goes here + + // add an anonymous authentication filter that will populate the authenticated, + // anonymous user if no other user identity is detected earlier in the Spring filter chain + http.anonymous().authenticationFilter(anonymousAuthenticationFilter); + + // After Spring Security filter chain is complete (so authentication is done), + // but before the Jersey application endpoints get the request, + // insert the ResourceAuthorizationFilter to do its authorization checks + http.addFilterAfter(resourceAuthorizationFilter(), FilterSecurityInterceptor.class); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth + .authenticationProvider(x509AuthenticationProvider()) + .authenticationProvider(jwtAuthenticationProvider()); + } + + private IdentityFilter x509AuthenticationFilter() throws Exception { + if (x509AuthenticationFilter == null) { + x509AuthenticationFilter = new IdentityFilter(x509IdentityProvider); + } + return x509AuthenticationFilter; + } + + private IdentityAuthenticationProvider x509AuthenticationProvider() { + if (x509AuthenticationProvider == null) { + x509AuthenticationProvider = new X509IdentityAuthenticationProvider(authorizer, x509IdentityProvider, identityMapper); + } + return x509AuthenticationProvider; + } + + private IdentityFilter jwtAuthenticationFilter() throws Exception { + if (jwtAuthenticationFilter == null) { + jwtAuthenticationFilter = new IdentityFilter(jwtIdentityProvider); + } + return jwtAuthenticationFilter; + } + + private IdentityAuthenticationProvider jwtAuthenticationProvider() { + if (jwtAuthenticationProvider == null) { + jwtAuthenticationProvider = new IdentityAuthenticationProvider(authorizer, jwtIdentityProvider, identityMapper); + } + return jwtAuthenticationProvider; + } + + private ResourceAuthorizationFilter resourceAuthorizationFilter() { + if (resourceAuthorizationFilter == null) { + resourceAuthorizationFilter = ResourceAuthorizationFilter.builder() + .setAuthorizationService(authorizationService) + .addResourceType(ResourceType.Actuator) + .addResourceType(ResourceType.Swagger) + .build(); + } + return resourceAuthorizationFilter; + } + + private AuthenticationEntryPoint http401AuthenticationEntryPoint() { + // This gets used for both secured and unsecured configurations. It will be called by Spring Security if a request makes it through the filter chain without being authenticated. + // For unsecured, this should never be reached because the custom AnonymousAuthenticationFilter should always populate a fully-authenticated anonymous user + // For secured, this will cause attempt to access any API endpoint (except those explicitly ignored) without providing credentials to return a 401 Unauthorized challenge + return new AuthenticationEntryPoint() { + @Override + public void commence(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authenticationException) + throws IOException, ServletException { + + // return a 401 response + final int status = HttpServletResponse.SC_UNAUTHORIZED; + logger.info("Client could not be authenticated due to: {} Returning 401 response.", authenticationException.toString()); + logger.debug("", authenticationException); + + if (!response.isCommitted()) { + response.setStatus(status); + response.setContentType("text/plain"); + response.getWriter().println(String.format("%s Contact the system administrator.", authenticationException.getLocalizedMessage())); + } + } + }; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/PermissionsService.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/PermissionsService.java new file mode 100644 index 0000000000..a15fea3bdc --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/PermissionsService.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.security; + +import org.apache.nifi.registry.authorization.Permissions; +import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.bucket.BucketItem; +import org.apache.nifi.registry.security.authorization.AuthorizableLookup; +import org.apache.nifi.registry.security.authorization.resource.Authorizable; +import org.apache.nifi.registry.service.AuthorizationService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * This is a class that Resource classes can utilized to populate fields + * on model objects returned by the {@link org.apache.nifi.registry.service.RegistryService} + * before returning them to a client. + * + * The fields cannot be populated by the RegistryService because they require + * the {@link AuthorizationService}, which RegistryService does not depend on. + */ +@Service +public class PermissionsService { + + private AuthorizationService authorizationService; + private AuthorizableLookup authorizableLookup; + + @Autowired + public PermissionsService(AuthorizationService authorizationService, AuthorizableLookup authorizableLookup) { + this.authorizationService = authorizationService; + this.authorizableLookup = authorizableLookup; + } + + public void populateBucketPermissions(final Iterable buckets) { + Permissions topLevelBucketPermissions = authorizationService.getPermissionsForResource(authorizableLookup.getBucketsAuthorizable()); + buckets.forEach(b -> populateBucketPermissions(b, topLevelBucketPermissions)); + } + + public void populateBucketPermissions(final Bucket bucket) { + populateBucketPermissions(bucket, null); + } + + public void populateItemPermissions(final Iterable bucketItems) { + Permissions topLevelBucketPermissions = authorizationService.getPermissionsForResource(authorizableLookup.getBucketsAuthorizable()); + bucketItems.forEach(i -> populateItemPermissions(i, topLevelBucketPermissions)); + } + + public void populateItemPermissions(final BucketItem bucketItem) { + populateItemPermissions(bucketItem, null); + } + + private void populateBucketPermissions(final Bucket bucket, final Permissions knownPermissions) { + if (bucket == null) { + return; + } + + Permissions bucketPermissions = createPermissionsForBucketId(bucket.getIdentifier(), knownPermissions); + bucket.setPermissions(bucketPermissions); + } + + private void populateItemPermissions(final BucketItem bucketItem, final Permissions knownPermissions) { + if (bucketItem == null) { + return; + } + + Permissions bucketItemPermissions = createPermissionsForBucketId(bucketItem.getBucketIdentifier(), knownPermissions); + bucketItem.setPermissions(bucketItemPermissions); + } + + private Permissions createPermissionsForBucketId(String bucketId, final Permissions knownPermissions) { + Authorizable bucketResource = authorizableLookup.getBucketAuthorizable(bucketId); + + Permissions permissions = knownPermissions == null + ? authorizationService.getPermissionsForResource(bucketResource) + : authorizationService.getPermissionsForResource(bucketResource, knownPermissions); + + return permissions; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/AnonymousIdentityFilter.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/AnonymousIdentityFilter.java new file mode 100644 index 0000000000..f879f0dee8 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/AnonymousIdentityFilter.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.security.authentication; + +import org.apache.nifi.registry.security.authorization.user.NiFiUserDetails; +import org.apache.nifi.registry.security.authorization.user.StandardNiFiUser; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; + +import javax.servlet.http.HttpServletRequest; + +public class AnonymousIdentityFilter extends AnonymousAuthenticationFilter { + + private static final String ANONYMOUS_KEY = "anonymousNifiKey"; + + public AnonymousIdentityFilter() { + super(ANONYMOUS_KEY); + } + + @Override + protected Authentication createAuthentication(HttpServletRequest request) { + return new AuthenticationSuccessToken(new NiFiUserDetails(StandardNiFiUser.ANONYMOUS)); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/AuthenticationRequestToken.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/AuthenticationRequestToken.java new file mode 100644 index 0000000000..a5a5ec3046 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/AuthenticationRequestToken.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.security.authentication; + +import org.apache.nifi.registry.security.authentication.AuthenticationRequest; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; + +import java.security.Principal; +import java.util.Collection; + +/** + * Wraps an AuthenticationRequest in a Token that implements the Spring Security Authentication interface. + */ +public class AuthenticationRequestToken implements Authentication { + + private final AuthenticationRequest authenticationRequest; + private final Class authenticationRequestOrigin; + private final String clientAddress; + + public AuthenticationRequestToken(AuthenticationRequest authenticationRequest, Class authenticationRequestOrigin, String clientAddress) { + this.authenticationRequest = authenticationRequest; + this.authenticationRequestOrigin = authenticationRequestOrigin; + this.clientAddress = clientAddress; + } + + @Override + public Collection getAuthorities() { + return null; + } + + @Override + public Object getCredentials() { + return authenticationRequest.getCredentials(); + } + + @Override + public Object getDetails() { + return authenticationRequest.getDetails(); + } + + @Override + public Object getPrincipal() { + return new Principal() { + @Override + public String getName() { + return authenticationRequest.getUsername(); + } + }; + } + + @Override + public boolean isAuthenticated() { + return false; + } + + @Override + public void setAuthenticated(boolean b) throws IllegalArgumentException { + throw new IllegalArgumentException("AuthenticationRequestWrapper cannot be trusted. It is only to be used for storing an identity claim."); + } + + @Override + public String getName() { + return authenticationRequest.getUsername(); + } + + @Override + public int hashCode() { + return authenticationRequest.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return authenticationRequest.equals(obj); + } + + @Override + public String toString() { + return authenticationRequest.toString(); + } + + public AuthenticationRequest getAuthenticationRequest() { + return authenticationRequest; + } + + public Class getAuthenticationRequestOrigin() { + return authenticationRequestOrigin; + } + + public String getClientAddress() { + return clientAddress; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/AuthenticationSuccessToken.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/AuthenticationSuccessToken.java new file mode 100644 index 0000000000..ea6f1e9825 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/AuthenticationSuccessToken.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.security.authentication; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.userdetails.UserDetails; + +/** + * An authentication token that represents an Authenticated and Authorized user of the NiFi Apis. The authorities are based off the specified UserDetails. + */ +public class AuthenticationSuccessToken extends AbstractAuthenticationToken { + + private final UserDetails nifiUserDetails; + + public AuthenticationSuccessToken(final UserDetails nifiUserDetails) { + super(nifiUserDetails.getAuthorities()); + super.setAuthenticated(true); + setDetails(nifiUserDetails); + this.nifiUserDetails = nifiUserDetails; + } + + @Override + public Object getCredentials() { + return nifiUserDetails.getPassword(); + } + + @Override + public Object getPrincipal() { + return nifiUserDetails; + } + + @Override + public final void setAuthenticated(boolean authenticated) { + throw new IllegalArgumentException("Cannot change the authenticated state."); + } + + @Override + public String toString() { + return nifiUserDetails.getUsername(); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityAuthenticationProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityAuthenticationProvider.java new file mode 100644 index 0000000000..b552a1a874 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityAuthenticationProvider.java @@ -0,0 +1,135 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.security.authentication; + +import org.apache.nifi.registry.security.authentication.AuthenticationRequest; +import org.apache.nifi.registry.security.authentication.AuthenticationResponse; +import org.apache.nifi.registry.security.authentication.IdentityProvider; +import org.apache.nifi.registry.security.authentication.exception.InvalidCredentialsException; +import org.apache.nifi.registry.security.authorization.Authorizer; +import org.apache.nifi.registry.security.authorization.Group; +import org.apache.nifi.registry.security.authorization.ManagedAuthorizer; +import org.apache.nifi.registry.security.authorization.UserAndGroups; +import org.apache.nifi.registry.security.authorization.UserGroupProvider; +import org.apache.nifi.registry.security.authorization.user.NiFiUserDetails; +import org.apache.nifi.registry.security.authorization.user.StandardNiFiUser; +import org.apache.nifi.registry.security.identity.IdentityMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; + +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; + +public class IdentityAuthenticationProvider implements AuthenticationProvider { + + private static final Logger LOGGER = LoggerFactory.getLogger(IdentityAuthenticationProvider.class); + + protected Authorizer authorizer; + protected final IdentityProvider identityProvider; + protected final IdentityMapper identityMapper; + + public IdentityAuthenticationProvider( + Authorizer authorizer, + IdentityProvider identityProvider, + IdentityMapper identityMapper) { + this.authorizer = authorizer; + this.identityProvider = identityProvider; + this.identityMapper = identityMapper; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + + // Determine if this AuthenticationProvider's identityProvider should be able to support this AuthenticationRequest + boolean tokenOriginatedFromThisIdentityProvider = checkTokenOriginatedFromThisIdentityProvider(authentication); + + if (!tokenOriginatedFromThisIdentityProvider) { + // Returning null indicates to The Spring Security AuthenticationManager that this AuthenticationProvider + // cannot authenticate this token and another provider should be tried. + return null; + } + + AuthenticationRequestToken authenticationRequestToken = ((AuthenticationRequestToken)authentication); + AuthenticationRequest authenticationRequest = authenticationRequestToken.getAuthenticationRequest(); + + try { + AuthenticationResponse authenticationResponse = identityProvider.authenticate(authenticationRequest); + if (authenticationResponse == null) { + return null; + } + return buildAuthenticatedToken(authenticationRequestToken, authenticationResponse); + } catch (InvalidCredentialsException e) { + throw new BadCredentialsException("Identity Provider authentication failed.", e); + } + + } + + @Override + public boolean supports(Class authenticationClazz) { + // is authenticationClazz a subclass of AuthenticationRequestWrapper? + return AuthenticationRequestToken.class.isAssignableFrom(authenticationClazz); + } + + protected AuthenticationSuccessToken buildAuthenticatedToken( + AuthenticationRequestToken requestToken, + AuthenticationResponse response) { + + final String mappedIdentity = mapIdentity(response.getIdentity()); + + return new AuthenticationSuccessToken(new NiFiUserDetails( + new StandardNiFiUser.Builder() + .identity(mappedIdentity) + .groups(getUserGroups(mappedIdentity)) + .clientAddress(requestToken.getClientAddress()) + .build())); + } + + protected boolean checkTokenOriginatedFromThisIdentityProvider(Authentication authentication) { + return (authentication instanceof AuthenticationRequestToken + && identityProvider.getClass().equals(((AuthenticationRequestToken) authentication).getAuthenticationRequestOrigin())); + } + + protected String mapIdentity(final String identity) { + return identityMapper.mapUser(identity); + } + + protected Set getUserGroups(final String identity) { + return getUserGroups(authorizer, identity); + } + + private static Set getUserGroups(final Authorizer authorizer, final String userIdentity) { + if (authorizer instanceof ManagedAuthorizer) { + final ManagedAuthorizer managedAuthorizer = (ManagedAuthorizer) authorizer; + final UserGroupProvider userGroupProvider = managedAuthorizer.getAccessPolicyProvider().getUserGroupProvider(); + final UserAndGroups userAndGroups = userGroupProvider.getUserAndGroups(userIdentity); + final Set userGroups = userAndGroups.getGroups(); + + if (userGroups == null || userGroups.isEmpty()) { + return Collections.emptySet(); + } else { + return userAndGroups.getGroups().stream().map(Group::getName).collect(Collectors.toSet()); + } + } else { + return null; + } + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityFilter.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityFilter.java new file mode 100644 index 0000000000..cd5e2bf1f4 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityFilter.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.security.authentication; + +import org.apache.nifi.registry.security.authentication.AuthenticationRequest; +import org.apache.nifi.registry.security.authentication.IdentityProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.GenericFilterBean; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +/** + * A class that will extract an identity / credentials claim from an HttpServlet Request using an injected IdentityProvider. + * + * This class is designed to be used in collaboration with an {@link IdentityAuthenticationProvider}. The identity/credentials will be + * extracted by this filter and later validated by the {@link IdentityAuthenticationProvider} in the default SecurityInterceptorFilter. + */ +public class IdentityFilter extends GenericFilterBean { + + private static final Logger logger = LoggerFactory.getLogger(IdentityFilter.class); + + private final IdentityProvider identityProvider; + + public IdentityFilter(IdentityProvider identityProvider) { + this.identityProvider = identityProvider; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + + // Only require authentication from an identity provider if the NiFi registry is running securely. + if (!servletRequest.isSecure()) { + // Otherwise, requests will be "authenticated" by the AnonymousIdentityFilter + filterChain.doFilter(servletRequest, servletResponse); + return; + } + + if (identityProvider == null) { + logger.warn("Identity Filter configured with NULL identity provider. Credentials will not be extracted."); + filterChain.doFilter(servletRequest, servletResponse); + return; + } + + if (credentialsAlreadyPresent()) { + logger.debug("Credentials already extracted for [{}], skipping credentials extraction filter using {}", + SecurityContextHolder.getContext().getAuthentication().getPrincipal().toString(), + identityProvider.getClass().getSimpleName()); + filterChain.doFilter(servletRequest, servletResponse); + return; + } + + logger.debug("Attempting to extract user credentials using {}", identityProvider.getClass().getSimpleName()); + + try { + AuthenticationRequest authenticationRequest = identityProvider.extractCredentials((HttpServletRequest)servletRequest); + if (authenticationRequest != null) { + Authentication authentication = new AuthenticationRequestToken(authenticationRequest, identityProvider.getClass(), servletRequest.getRemoteAddr()); + logger.debug("Adding credentials claim to SecurityContext to be authenticated. Credentials extracted by {}: {}", + identityProvider.getClass().getSimpleName(), + authenticationRequest); + SecurityContextHolder.getContext().setAuthentication(authentication); + // This filter's job, which is merely to search for and extract an identity claim, is done. + // The actual authentication of the identity claim will be handled by a corresponding IdentityAuthenticationProvider + } + } catch (Exception e) { + logger.debug("Exception occurred while extracting credentials:", e); + } + + filterChain.doFilter(servletRequest, servletResponse); + } + + private boolean credentialsAlreadyPresent() { + return SecurityContextHolder.getContext().getAuthentication() != null; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/exception/InvalidAuthenticationException.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/exception/InvalidAuthenticationException.java new file mode 100644 index 0000000000..016e9cb19c --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/exception/InvalidAuthenticationException.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.security.authentication.exception; + +import org.springframework.security.core.AuthenticationException; + +/** + * Thrown if the authentication of a given request is invalid. For instance, + * an expired certificate or token. + */ +public class InvalidAuthenticationException extends AuthenticationException { + + public InvalidAuthenticationException(String msg) { + super(msg); + } + + public InvalidAuthenticationException(String msg, Throwable t) { + super(msg, t); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtIdentityProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtIdentityProvider.java new file mode 100644 index 0000000000..d3f12c9114 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtIdentityProvider.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.security.authentication.jwt; + +import io.jsonwebtoken.JwtException; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.apache.nifi.registry.security.authentication.AuthenticationRequest; +import org.apache.nifi.registry.security.authentication.AuthenticationResponse; +import org.apache.nifi.registry.security.authentication.BearerAuthIdentityProvider; +import org.apache.nifi.registry.security.authentication.IdentityProvider; +import org.apache.nifi.registry.security.authentication.IdentityProviderConfigurationContext; +import org.apache.nifi.registry.security.authentication.exception.IdentityAccessException; +import org.apache.nifi.registry.security.authentication.exception.InvalidCredentialsException; +import org.apache.nifi.registry.security.authorization.Authorizer; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException; +import org.apache.nifi.registry.web.security.authentication.exception.InvalidAuthenticationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Component +public class JwtIdentityProvider extends BearerAuthIdentityProvider implements IdentityProvider { + + private static final Logger logger = LoggerFactory.getLogger(JwtIdentityProvider.class); + + private static final String issuer = JwtIdentityProvider.class.getSimpleName(); + + private static final long expiration = TimeUnit.MILLISECONDS.convert(12, TimeUnit.HOURS); + + private final JwtService jwtService; + + @Autowired + public JwtIdentityProvider(JwtService jwtService, NiFiRegistryProperties nifiProperties, Authorizer authorizer) { + this.jwtService = jwtService; + } + + @Override + public AuthenticationResponse authenticate(AuthenticationRequest authenticationRequest) throws InvalidCredentialsException, IdentityAccessException { + + if (authenticationRequest == null) { + logger.info("Cannot authenticate null authenticationRequest, returning null."); + return null; + } + + final Object credentials = authenticationRequest.getCredentials(); + String jwtAuthToken = credentials != null && credentials instanceof String ? (String) credentials : null; + + if (credentials == null) { + logger.info("JWT not found in authenticationRequest credentials, returning null."); + return null; + } + + try { + final String jwtPrincipal = jwtService.getAuthenticationFromToken(jwtAuthToken); + return new AuthenticationResponse(jwtPrincipal, jwtPrincipal, expiration, issuer); + } catch (JwtException e) { + throw new InvalidAuthenticationException(e.getMessage(), e); + } + } + + @Override + public void onConfigured(IdentityProviderConfigurationContext configurationContext) throws SecurityProviderCreationException {} + + @Override + public void preDestruction() throws SecurityProviderDestructionException {} + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtService.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtService.java new file mode 100644 index 0000000000..a0e2d252c7 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtService.java @@ -0,0 +1,245 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.security.authentication.jwt; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.SignatureException; +import io.jsonwebtoken.SigningKeyResolverAdapter; +import io.jsonwebtoken.UnsupportedJwtException; +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.security.authentication.AuthenticationResponse; +import org.apache.nifi.registry.security.key.Key; +import org.apache.nifi.registry.security.key.KeyService; +import org.apache.nifi.registry.web.security.authentication.exception.InvalidAuthenticationException; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +// TODO, look into replacing this JwtService service with Apache Licensed JJWT library +@Service +public class JwtService { + + private static final org.slf4j.Logger logger = LoggerFactory.getLogger(JwtService.class); + + private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256; + private static final String KEY_ID_CLAIM = "kid"; + private static final String USERNAME_CLAIM = "preferred_username"; + private static final Pattern tokenPattern = Pattern.compile("^Bearer (\\S*\\.\\S*\\.\\S*)$"); + public static final String AUTHORIZATION = "Authorization"; + + private final KeyService keyService; + + @Autowired + public JwtService(final KeyService keyService) { + this.keyService = keyService; + } + + public String getAuthenticationFromToken(final String base64EncodedToken) throws JwtException { + // The library representations of the JWT should be kept internal to this service. + try { + final Jws jws = parseTokenFromBase64EncodedString(base64EncodedToken); + + if (jws == null) { + throw new JwtException("Unable to parse token"); + } + + // Additional validation that subject is present + if (StringUtils.isEmpty(jws.getBody().getSubject())) { + throw new JwtException("No subject available in token"); + } + + // TODO: Validate issuer against active IdentityProvider? + if (StringUtils.isEmpty(jws.getBody().getIssuer())) { + throw new JwtException("No issuer available in token"); + } + return jws.getBody().getSubject(); + } catch (JwtException e) { + logger.debug("The Base64 encoded JWT: " + base64EncodedToken); + final String errorMessage = "There was an error validating the JWT"; + logger.error(errorMessage, e); + throw e; + } + } + + private Jws parseTokenFromBase64EncodedString(final String base64EncodedToken) throws JwtException { + try { + return Jwts.parser().setSigningKeyResolver(new SigningKeyResolverAdapter() { + @Override + public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { + final String identity = claims.getSubject(); + + // Get the key based on the key id in the claims + final String keyId = claims.get(KEY_ID_CLAIM, String.class); + final Key key = keyService.getKey(keyId); + + // Ensure we were able to find a key that was previously issued by this key service for this user + if (key == null || key.getKey() == null) { + throw new UnsupportedJwtException("Unable to determine signing key for " + identity + " [kid: " + keyId + "]"); + } + + return key.getKey().getBytes(StandardCharsets.UTF_8); + } + }).parseClaimsJws(base64EncodedToken); + } catch (final MalformedJwtException | UnsupportedJwtException | SignatureException | ExpiredJwtException | IllegalArgumentException e) { + // TODO: Exercise all exceptions to ensure none leak key material to logs + final String errorMessage = "Unable to validate the access token."; + throw new JwtException(errorMessage, e); + } + } + + /** + * Generates a signed JWT token from the provided IdentityProvider AuthenticationResponse + * + * @param authenticationResponse an instance issued by an IdentityProvider after identity claim has been verified as authentic + * @return a signed JWT containing the user identity and the identity provider, Base64-encoded + * @throws JwtException if there is a problem generating the signed token + */ + public String generateSignedToken(final AuthenticationResponse authenticationResponse) throws JwtException { + if (authenticationResponse == null) { + throw new IllegalArgumentException("Cannot generate a JWT for a null authenticationResponse"); + } + + return generateSignedToken( + authenticationResponse.getIdentity(), + authenticationResponse.getUsername(), + authenticationResponse.getIssuer(), + authenticationResponse.getIssuer(), + authenticationResponse.getExpiration()); + } + + public String generateSignedToken(String identity, String preferredUsername, String issuer, String audience, long expirationMillis) throws JwtException { + + if (identity == null || StringUtils.isEmpty(identity)) { + String errorMessage = "Cannot generate a JWT for a token with an empty identity"; + errorMessage = issuer != null ? errorMessage + " issued by " + issuer + "." : "."; + logger.error(errorMessage); + throw new IllegalArgumentException(errorMessage); + } + + // Compute expiration + final Calendar now = Calendar.getInstance(); + long expirationMillisRelativeToNow = validateTokenExpiration(expirationMillis, identity); + long expirationMillisSinceEpoch = now.getTimeInMillis() + expirationMillisRelativeToNow; + final Calendar expiration = new Calendar.Builder().setInstant(expirationMillisSinceEpoch).build(); + + try { + // Get/create the key for this user + final Key key = keyService.getOrCreateKey(identity); + final byte[] keyBytes = key.getKey().getBytes(StandardCharsets.UTF_8); + + //logger.trace("Generating JWT for " + describe(authenticationResponse)); + + // TODO: Implement "jti" claim with nonce to prevent replay attacks and allow blacklisting of revoked tokens + // Build the token + return Jwts.builder().setSubject(identity) + .setIssuer(issuer) + .setAudience(audience) + .claim(USERNAME_CLAIM, preferredUsername) + .claim(KEY_ID_CLAIM, key.getId()) + .setIssuedAt(now.getTime()) + .setExpiration(expiration.getTime()) + .signWith(SIGNATURE_ALGORITHM, keyBytes).compact(); + } catch (NullPointerException e) { + final String errorMessage = "Could not retrieve the signing key for JWT for " + identity; + logger.error(errorMessage, e); + throw new JwtException(errorMessage, e); + } + + } + + public void logOut(String userIdentity) { + if (userIdentity == null || userIdentity.isEmpty()) { + throw new JwtException("Log out failed: The user identity was not present in the request token to log out user."); + } + + try { + keyService.deleteKey(userIdentity); + logger.info("Deleted token from database."); + } catch (Exception e) { + logger.error("Unable to log out user: " + userIdentity + ". Failed to remove their token from database."); + throw e; + } + } + + private static long validateTokenExpiration(long proposedTokenExpiration, String identity) { + final long maxExpiration = TimeUnit.MILLISECONDS.convert(12, TimeUnit.HOURS); + final long minExpiration = TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES); + + if (proposedTokenExpiration > maxExpiration) { + logger.warn(String.format("Max token expiration exceeded. Setting expiration to %s from %s for %s", maxExpiration, + proposedTokenExpiration, identity)); + proposedTokenExpiration = maxExpiration; + } else if (proposedTokenExpiration < minExpiration) { + logger.warn(String.format("Min token expiration not met. Setting expiration to %s from %s for %s", minExpiration, + proposedTokenExpiration, identity)); + proposedTokenExpiration = minExpiration; + } + + return proposedTokenExpiration; + } + + private static String describe(AuthenticationResponse authenticationResponse) { + Calendar expirationTime = Calendar.getInstance(); + expirationTime.setTimeInMillis(authenticationResponse.getExpiration()); + long remainingTime = expirationTime.getTimeInMillis() - Calendar.getInstance().getTimeInMillis(); + + SimpleDateFormat dateFormat = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss.SSS"); + dateFormat.setTimeZone(expirationTime.getTimeZone()); + String expirationTimeString = dateFormat.format(expirationTime.getTime()); + + return new StringBuilder("LoginAuthenticationToken for ") + .append(authenticationResponse.getUsername()) + .append(" issued by ") + .append(authenticationResponse.getIssuer()) + .append(" expiring at ") + .append(expirationTimeString) + .append(" [") + .append(authenticationResponse.getExpiration()) + .append(" ms, ") + .append(remainingTime) + .append(" ms remaining]") + .toString(); + } + + public void logOutUsingAuthHeader(String authorizationHeader) { + String base64EncodedToken = getTokenFromHeader(authorizationHeader); + logOut(getAuthenticationFromToken(base64EncodedToken)); + } + + public static String getTokenFromHeader(String authenticationHeader) { + Matcher matcher = tokenPattern.matcher(authenticationHeader); + if(matcher.matches()) { + return matcher.group(1); + } else { + throw new InvalidAuthenticationException("JWT did not match expected pattern."); + } + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosIdentityProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosIdentityProvider.java new file mode 100644 index 0000000000..ee55c02b17 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosIdentityProvider.java @@ -0,0 +1,141 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.security.authentication.kerberos; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.security.authentication.AuthenticationRequest; +import org.apache.nifi.registry.security.authentication.AuthenticationResponse; +import org.apache.nifi.registry.security.authentication.BasicAuthIdentityProvider; +import org.apache.nifi.registry.security.authentication.IdentityProviderConfigurationContext; +import org.apache.nifi.registry.security.authentication.exception.IdentityAccessException; +import org.apache.nifi.registry.security.authentication.exception.InvalidCredentialsException; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException; +import org.apache.nifi.registry.security.util.kerberos.KerberosPrincipalParser; +import org.apache.nifi.registry.util.FormatUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.kerberos.authentication.KerberosAuthenticationProvider; +import org.springframework.security.kerberos.authentication.sun.SunJaasKerberosClient; + +import java.util.concurrent.TimeUnit; + +public class KerberosIdentityProvider extends BasicAuthIdentityProvider { + + private static final Logger logger = LoggerFactory.getLogger(KerberosIdentityProvider.class); + private static final String issuer = KerberosIdentityProvider.class.getSimpleName(); + private static final String default_expiration = "12 hours"; + + private KerberosAuthenticationProvider provider; + + private String defaultRealm; + private long expiration; + + @Override + public void onConfigured(IdentityProviderConfigurationContext configurationContext) throws SecurityProviderCreationException { + + String rawDebug = configurationContext.getProperty("Enable Debug"); + boolean enableDebug = (rawDebug != null && rawDebug.equalsIgnoreCase("true")); + + String rawExpiration = configurationContext.getProperty("Authentication Expiration"); + if (StringUtils.isBlank(rawExpiration)) { + rawExpiration = default_expiration; + logger.info("No Authentication Expiration specified, defaulting to " + default_expiration); + } + + try { + expiration = FormatUtils.getTimeDuration(rawExpiration, TimeUnit.MILLISECONDS); + } catch (final IllegalArgumentException iae) { + throw new SecurityProviderCreationException( + String.format("The Expiration Duration '%s' is not a valid time duration", rawExpiration)); + } + + defaultRealm = configurationContext.getProperty("Default Realm"); + if (StringUtils.isNotBlank(defaultRealm) && defaultRealm.contains("@")) { + throw new SecurityProviderCreationException(String.format("The Default Realm '%s' must not contain \"@\"", defaultRealm)); + } + + provider = new KerberosAuthenticationProvider(); + SunJaasKerberosClient client = new SunJaasKerberosClient(); + client.setDebug(enableDebug); + provider.setKerberosClient(client); + provider.setUserDetailsService(new KerberosUserDetailsService()); + + } + + @Override + public AuthenticationResponse authenticate(AuthenticationRequest authenticationRequest) throws InvalidCredentialsException, IdentityAccessException { + + if (provider == null) { + throw new IdentityAccessException("The Kerberos authentication provider is not initialized."); + } + + + try { + final String rawPrincipal = authenticationRequest.getUsername(); + final Object credentials = authenticationRequest.getCredentials(); + final String parsedRealm = KerberosPrincipalParser.getRealm(rawPrincipal); + + // Apply default realm from KerberosIdentityProvider's configuration specified in identity-providers.xml if a principal without a realm was given + // Otherwise, the default realm configured from the krb5 configuration specified in the nifi.registry.kerberos.krb5.file property will end up being used + boolean realmInRawPrincipal = StringUtils.isNotBlank(parsedRealm); + final String identity; + if (realmInRawPrincipal) { + // there's a realm already in the given principal, use it + identity = rawPrincipal; + logger.debug("Realm was specified in principal {}, default realm was not added to the identity being authenticated", rawPrincipal); + } else if (StringUtils.isNotBlank(defaultRealm)) { + // the value for the default realm is not blank, append the realm to the given principal + identity = StringUtils.joinWith("@", rawPrincipal, defaultRealm); + logger.debug("Realm was not specified in principal {}, default realm {} was added to the identity being authenticated", rawPrincipal, defaultRealm); + } else { + // otherwise, use the given principal, which will use the default realm as specified in the krb5 configuration + identity = rawPrincipal; + logger.debug("Realm was not specified in principal {}, default realm is blank and was not added to the identity being authenticated", rawPrincipal); + } + + // perform the authentication + final UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(identity, credentials); + + if (logger.isDebugEnabled()) { + logger.debug("Created authentication token " + token.toString()); + } + + final Authentication authentication = provider.authenticate(token); + if (logger.isDebugEnabled()) { + logger.debug("Ran provider.authenticate(token) and returned authentication for " + + "principal={} with name={} and isAuthenticated={}", + authentication.getPrincipal(), + authentication.getName(), + authentication.isAuthenticated()); + } + + return new AuthenticationResponse(authentication.getName(), identity, expiration, issuer); + } catch (final AuthenticationException e) { + throw new InvalidCredentialsException(e.getMessage(), e); + } + + } + + @Override + public void preDestruction() throws SecurityProviderDestructionException { + + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosSpnegoFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosSpnegoFactory.java new file mode 100644 index 0000000000..16211ed27d --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosSpnegoFactory.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.security.authentication.kerberos; + +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.kerberos.authentication.KerberosServiceAuthenticationProvider; +import org.springframework.security.kerberos.authentication.KerberosTicketValidator; + +@Configuration +public class KerberosSpnegoFactory { + + @Autowired + private NiFiRegistryProperties properties; + + @Autowired(required = false) + private KerberosTicketValidator kerberosTicketValidator; + + private KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider; + private KerberosSpnegoIdentityProvider kerberosSpnegoIdentityProvider; + + @Bean + public KerberosSpnegoIdentityProvider kerberosSpnegoIdentityProvider() throws Exception { + + if (kerberosSpnegoIdentityProvider == null && properties.isKerberosSpnegoSupportEnabled()) { + kerberosSpnegoIdentityProvider = new KerberosSpnegoIdentityProvider( + kerberosServiceAuthenticationProvider(), + properties); + } + + return kerberosSpnegoIdentityProvider; + } + + + private KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider() throws Exception { + + if (kerberosServiceAuthenticationProvider == null && properties.isKerberosSpnegoSupportEnabled()) { + + KerberosServiceAuthenticationProvider ksap = new KerberosServiceAuthenticationProvider(); + ksap.setTicketValidator(kerberosTicketValidator); + ksap.setUserDetailsService(new KerberosUserDetailsService()); + ksap.afterPropertiesSet(); + + kerberosServiceAuthenticationProvider = ksap; + + } + + return kerberosServiceAuthenticationProvider; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosSpnegoIdentityProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosSpnegoIdentityProvider.java new file mode 100644 index 0000000000..f44d766fa3 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosSpnegoIdentityProvider.java @@ -0,0 +1,182 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.security.authentication.kerberos; + +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.apache.nifi.registry.security.authentication.AuthenticationRequest; +import org.apache.nifi.registry.security.authentication.AuthenticationResponse; +import org.apache.nifi.registry.security.authentication.IdentityProvider; +import org.apache.nifi.registry.security.authentication.IdentityProviderConfigurationContext; +import org.apache.nifi.registry.security.authentication.IdentityProviderUsage; +import org.apache.nifi.registry.security.authentication.exception.IdentityAccessException; +import org.apache.nifi.registry.security.authentication.exception.InvalidCredentialsException; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException; +import org.apache.nifi.registry.security.util.CryptoUtils; +import org.apache.nifi.registry.util.FormatUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.Nullable; +import org.springframework.security.authentication.AuthenticationDetailsSource; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.crypto.codec.Base64; +import org.springframework.security.kerberos.authentication.KerberosServiceAuthenticationProvider; +import org.springframework.security.kerberos.authentication.KerberosServiceRequestToken; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; + +import javax.servlet.http.HttpServletRequest; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; + +public class KerberosSpnegoIdentityProvider implements IdentityProvider { + + private static final Logger logger = LoggerFactory.getLogger(KerberosSpnegoIdentityProvider.class); + + private static final String issuer = KerberosSpnegoIdentityProvider.class.getSimpleName(); + + private static final IdentityProviderUsage usage = new IdentityProviderUsage() { + @Override + public String getText() { + return "The Kerberos user credentials must be passed in the HTTP Authorization header as specified by SPNEGO-based Kerberos. " + + "That is: 'Authorization: Negotiate ', " + + "where is a value that will be validated by this identity provider against a Kerberos cluster."; + } + + @Override + public AuthType getAuthType() { + return AuthType.NEGOTIATE; + } + }; + + private static final String AUTHORIZATION = "Authorization"; + private static final String AUTHORIZATION_NEGOTIATE = "Negotiate"; + + private long expiration = TimeUnit.MILLISECONDS.convert(12, TimeUnit.HOURS);; + private KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider; + private AuthenticationDetailsSource authenticationDetailsSource; + + @Autowired + public KerberosSpnegoIdentityProvider( + @Nullable KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider, + NiFiRegistryProperties properties) { + this.kerberosServiceAuthenticationProvider = kerberosServiceAuthenticationProvider; + authenticationDetailsSource = new WebAuthenticationDetailsSource(); + + final String expirationFromProperties = properties.getKerberosSpnegoAuthenticationExpiration(); + if (expirationFromProperties != null) { + long expiration = FormatUtils.getTimeDuration(expirationFromProperties, TimeUnit.MILLISECONDS); + } + } + + @Override + public IdentityProviderUsage getUsageInstructions() { + return usage; + } + + @Override + public AuthenticationRequest extractCredentials(HttpServletRequest request) { + + // Only support Kerberos authentication when running securely + if (!request.isSecure()) { + return null; + } + + String headerValue = request.getHeader(AUTHORIZATION); + + if (!isValidKerberosHeader(headerValue)) { + return null; + } + + logger.debug("Detected 'Authorization: Negotiate header in request {}", request.getRequestURL()); + byte[] base64Token = headerValue.substring(headerValue.indexOf(" ") + 1).getBytes(StandardCharsets.UTF_8); + byte[] kerberosTicket = Base64.decode(base64Token); + if (kerberosTicket != null) { + logger.debug("Successfully decoded SPNEGO/Kerberos ticket passed in Authorization: Negotiate header.", request.getRequestURL()); + } + + return new AuthenticationRequest(null, kerberosTicket, authenticationDetailsSource.buildDetails(request)); + + } + + @Override + public AuthenticationResponse authenticate(AuthenticationRequest authenticationRequest) throws InvalidCredentialsException, IdentityAccessException { + + if (authenticationRequest == null) { + logger.info("Cannot authenticate null authenticationRequest, returning null."); + return null; + } + + final Object credentials = authenticationRequest.getCredentials(); + byte[] kerberosTicket = credentials != null && credentials instanceof byte[] ? (byte[]) authenticationRequest.getCredentials() : null; + + if (credentials == null) { + logger.info("Kerberos Ticket not found in authenticationRequest credentials, returning null."); + return null; + } + + if (kerberosServiceAuthenticationProvider == null) { + throw new IdentityAccessException("The Kerberos authentication provider is not initialized."); + } + + try { + KerberosServiceRequestToken kerberosServiceRequestToken = new KerberosServiceRequestToken(kerberosTicket); + kerberosServiceRequestToken.setDetails(authenticationRequest.getDetails()); + Authentication authentication = kerberosServiceAuthenticationProvider.authenticate(kerberosServiceRequestToken); + if (authentication == null) { + throw new InvalidCredentialsException("Kerberos credentials could not be authenticated."); + } + + final String kerberosPrincipal = authentication.getName(); + + return new AuthenticationResponse(kerberosPrincipal, kerberosPrincipal, expiration, issuer); + + } catch (AuthenticationException e) { + String authFailedMessage = "Kerberos credentials could not be authenticated."; + + /* Kerberos uses encryption with up to AES-256, specifically AES256-CTS-HMAC-SHA1-96. + * That is not available in every JRE, particularly if Unlimited Strength Encryption + * policies are not installed in the Java home lib dir. The Kerberos lib does not + * differentiate between failures due to decryption and those due to bad credentials + * without walking the causes of the exception, so this check puts something + * potentially useful in the logs for those troubleshooting Kerberos authentication. */ + if (!Boolean.FALSE.equals(CryptoUtils.isCryptoRestricted())) { + authFailedMessage += " This Java Runtime does not support unlimited strength encryption. " + + "This could cause Kerberos authentication to fail as it can require AES-256."; + } + + logger.info(authFailedMessage); + throw new InvalidCredentialsException(authFailedMessage, e); + } + + } + + @Override + public void onConfigured(IdentityProviderConfigurationContext configurationContext) throws SecurityProviderCreationException { + throw new SecurityProviderCreationException(KerberosSpnegoIdentityProvider.class.getSimpleName() + + " does not currently support being loaded via IdentityProviderFactory"); + } + + @Override + public void preDestruction() throws SecurityProviderDestructionException { + } + + public boolean isValidKerberosHeader(String headerValue) { + return headerValue != null && (headerValue.startsWith(AUTHORIZATION_NEGOTIATE + " ") || headerValue.startsWith("Kerberos ")); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosTicketValidatorFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosTicketValidatorFactory.java new file mode 100644 index 0000000000..ed3e6ebfb5 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosTicketValidatorFactory.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.security.authentication.kerberos; + +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.FileSystemResource; +import org.springframework.security.kerberos.authentication.KerberosTicketValidator; +import org.springframework.security.kerberos.authentication.sun.GlobalSunJaasKerberosConfig; +import org.springframework.security.kerberos.authentication.sun.SunJaasKerberosTicketValidator; + +import java.io.File; + +@Configuration +public class KerberosTicketValidatorFactory { + + private NiFiRegistryProperties properties; + + private KerberosTicketValidator kerberosTicketValidator; + + @Autowired + public KerberosTicketValidatorFactory(NiFiRegistryProperties properties) { + this.properties = properties; + } + + @Bean + public KerberosTicketValidator kerberosTicketValidator() throws Exception { + + if (kerberosTicketValidator == null && properties.isKerberosSpnegoSupportEnabled()) { + + // Configure SunJaasKerberos (global) + final File krb5ConfigFile = properties.getKerberosConfigurationFile(); + if (krb5ConfigFile != null) { + final GlobalSunJaasKerberosConfig krb5Config = new GlobalSunJaasKerberosConfig(); + krb5Config.setKrbConfLocation(krb5ConfigFile.getAbsolutePath()); + krb5Config.afterPropertiesSet(); + } + + // Create ticket validator to inject into KerberosServiceAuthenticationProvider + SunJaasKerberosTicketValidator ticketValidator = new SunJaasKerberosTicketValidator(); + ticketValidator.setServicePrincipal(properties.getKerberosSpnegoPrincipal()); + ticketValidator.setKeyTabLocation(new FileSystemResource(properties.getKerberosSpnegoKeytabLocation())); + ticketValidator.afterPropertiesSet(); + + kerberosTicketValidator = ticketValidator; + + } + + return kerberosTicketValidator; + + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosUserDetailsService.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosUserDetailsService.java new file mode 100644 index 0000000000..5471906f54 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosUserDetailsService.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.security.authentication.kerberos; + +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +public class KerberosUserDetailsService implements UserDetailsService { + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + return new User( + username, + "notUsed", + true, + true, + true, + true, + AuthorityUtils.createAuthorityList("ROLE_USER")); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/oidc/OidcIdentityProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/oidc/OidcIdentityProvider.java new file mode 100644 index 0000000000..53e3fe22a0 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/oidc/OidcIdentityProvider.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.security.authentication.oidc; + +import com.nimbusds.oauth2.sdk.AuthorizationGrant; +import com.nimbusds.oauth2.sdk.Scope; +import com.nimbusds.oauth2.sdk.id.ClientID; + +import java.io.IOException; +import java.net.URI; + +public interface OidcIdentityProvider { + + String OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED = "OpenId Connect support is not configured"; + + /** + * Initializes the provider. + */ + void initializeProvider(); + + /** + * Returns whether OIDC support is enabled. + * + * @return whether OIDC support is enabled + */ + boolean isOidcEnabled(); + + /** + * Returns the configured client id. + * + * @return the client id + */ + ClientID getClientId(); + + /** + * Returns the URI for the authorization endpoint. + * + * @return uri for the authorization endpoint + */ + URI getAuthorizationEndpoint(); + + /** + * Returns the URI for the end session endpoint. + * + * @return uri for the end session endpoint + */ + URI getEndSessionEndpoint(); + + /** + * Returns the scopes supported by the OIDC provider. + * + * @return support scopes + */ + Scope getScope(); + + /** + * Exchanges the supplied authorization grant for an ID token. Extracts the identity from the ID + * token and converts it into NiFi JWT. + * + * @param authorizationGrant authorization grant for invoking the Token Endpoint + * @return a NiFi JWT + * @throws IOException if there was an exceptional error while communicating with the OIDC provider + */ + String exchangeAuthorizationCode(AuthorizationGrant authorizationGrant) throws IOException; +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/oidc/OidcService.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/oidc/OidcService.java new file mode 100644 index 0000000000..65b160bb17 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/oidc/OidcService.java @@ -0,0 +1,258 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.security.authentication.oidc; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.nimbusds.oauth2.sdk.AuthorizationGrant; +import com.nimbusds.oauth2.sdk.Scope; +import com.nimbusds.oauth2.sdk.id.State; +import org.apache.nifi.registry.web.security.authentication.util.CacheKey; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.math.BigInteger; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +/** + * OidcService is a service for managing the OpenId Connect Authorization flow. + */ +@Service +public class OidcService { + + private OidcIdentityProvider identityProvider; + private Cache stateLookupForPendingRequests; // identifier from cookie -> state value + private Cache jwtLookupForCompletedRequests; // identifier from cookie -> jwt or identity (and generate jwt on retrieval) + + /** + * Creates a new OtpService with an expiration of 1 minute. + * + * @param identityProvider The identity provider + */ + @Autowired + public OidcService(final OidcIdentityProvider identityProvider) { + this(identityProvider, 60, TimeUnit.SECONDS); + } + + /** + * Creates a new OtpService. + * + * @param identityProvider The identity provider + * @param duration The expiration duration + * @param units The expiration units + * @throws NullPointerException If units is null + * @throws IllegalArgumentException If duration is negative + */ + public OidcService(final OidcIdentityProvider identityProvider, final int duration, final TimeUnit units) { + if (identityProvider == null) { + throw new RuntimeException("The OidcIdentityProvider must be specified."); + } + + identityProvider.initializeProvider(); + this.identityProvider = identityProvider; + this.stateLookupForPendingRequests = CacheBuilder.newBuilder().expireAfterWrite(duration, units).build(); + this.jwtLookupForCompletedRequests = CacheBuilder.newBuilder().expireAfterWrite(duration, units).build(); + } + + /** + * Returns whether OpenId Connect is enabled. + * + * @return whether OpenId Connect is enabled + */ + public boolean isOidcEnabled() { + return identityProvider.isOidcEnabled(); + } + + /** + * Returns the OpenId Connect authorization endpoint. + * + * @return the authorization endpoint + */ + public URI getAuthorizationEndpoint() { + return identityProvider.getAuthorizationEndpoint(); + } + + /** + * Returns the OpenId Connect end session endpoint. + * + * @return the end session endpoint + */ + public URI getEndSessionEndpoint() { + return identityProvider.getEndSessionEndpoint(); + } + + /** + * Returns the OpenId Connect scope. + * + * @return scope + */ + public Scope getScope() { + return identityProvider.getScope(); + } + + /** + * Returns the OpenId Connect client id. + * + * @return client id + */ + public String getClientId() { + return identityProvider.getClientId().getValue(); + } + + /** + * Initiates an OpenId Connection authorization code flow using the specified request identifier to maintain state. + * + * @param oidcRequestIdentifier request identifier + * @return state + */ + public State createState(final String oidcRequestIdentifier) { + if (!isOidcEnabled()) { + throw new IllegalStateException(OidcIdentityProvider.OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED); + } + + final CacheKey oidcRequestIdentifierKey = new CacheKey(oidcRequestIdentifier); + final State state = new State(generateStateValue()); + + try { + synchronized (stateLookupForPendingRequests) { + final State cachedState = stateLookupForPendingRequests.get(oidcRequestIdentifierKey, () -> state); + if (!timeConstantEqualityCheck(state.getValue(), cachedState.getValue())) { + throw new IllegalStateException("An existing login request is already in progress."); + } + } + } catch (ExecutionException e) { + throw new IllegalStateException("Unable to store the login request state."); + } + + return state; + } + + /** + * Generates a value to use as State in the OpenId Connect login sequence. 128 bits is considered cryptographically strong + * with current hardware/software, but a Base32 digit needs 5 bits to be fully encoded, so 128 is rounded up to 130. Base32 + * is chosen because it encodes data with a single case and without including confusing or URI-incompatible characters, + * unlike Base64, but is approximately 20% more compact than Base16/hexadecimal + * + * @return the state value + */ + private String generateStateValue() { + return new BigInteger(130, new SecureRandom()).toString(32); + } + + /** + * Validates the proposed state with the given request identifier. Will return false if the + * state does not match or if entry for this request identifier has expired. + * + * @param oidcRequestIdentifier request identifier + * @param proposedState proposed state + * @return whether the state is valid or not + */ + public boolean isStateValid(final String oidcRequestIdentifier, final State proposedState) { + if (!isOidcEnabled()) { + throw new IllegalStateException(OidcIdentityProvider.OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED); + } + + if (proposedState == null) { + throw new IllegalArgumentException("Proposed state must be specified."); + } + + final CacheKey oidcRequestIdentifierKey = new CacheKey(oidcRequestIdentifier); + + synchronized (stateLookupForPendingRequests) { + final State state = stateLookupForPendingRequests.getIfPresent(oidcRequestIdentifierKey); + if (state != null) { + stateLookupForPendingRequests.invalidate(oidcRequestIdentifierKey); + } + + return state != null && timeConstantEqualityCheck(state.getValue(), proposedState.getValue()); + } + } + + /** + * Exchanges the specified authorization grant for an ID token for the given request identifier. + * + * @param oidcRequestIdentifier request identifier + * @param authorizationGrant authorization grant + * @throws IOException exceptional case for communication error with the OpenId Connect provider + */ + public void exchangeAuthorizationCode(final String oidcRequestIdentifier, final AuthorizationGrant authorizationGrant) throws IOException { + if (!isOidcEnabled()) { + throw new IllegalStateException(OidcIdentityProvider.OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED); + } + + final CacheKey oidcRequestIdentifierKey = new CacheKey(oidcRequestIdentifier); + final String nifiJwt = identityProvider.exchangeAuthorizationCode(authorizationGrant); + + try { + // cache the jwt for later retrieval + synchronized (jwtLookupForCompletedRequests) { + final String cachedJwt = jwtLookupForCompletedRequests.get(oidcRequestIdentifierKey, () -> nifiJwt); + if (!timeConstantEqualityCheck(nifiJwt, cachedJwt)) { + throw new IllegalStateException("An existing login request is already in progress."); + } + } + } catch (final ExecutionException e) { + throw new IllegalStateException("Unable to store the login authentication token."); + } + } + + /** + * Returns the resulting JWT for the given request identifier. Will return null if the request + * identifier is not associated with a JWT or if the login sequence was not completed before + * this request identifier expired. + * + * @param oidcRequestIdentifier request identifier + * @return jwt token + */ + public String getJwt(final String oidcRequestIdentifier) { + if (!isOidcEnabled()) { + throw new IllegalStateException(OidcIdentityProvider.OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED); + } + + final CacheKey oidcRequestIdentifierKey = new CacheKey(oidcRequestIdentifier); + + synchronized (jwtLookupForCompletedRequests) { + final String jwt = jwtLookupForCompletedRequests.getIfPresent(oidcRequestIdentifierKey); + if (jwt != null) { + jwtLookupForCompletedRequests.invalidate(oidcRequestIdentifierKey); + } + + return jwt; + } + } + + /** + * Implements a time constant equality check. If either value is null, false is returned. + * + * @param value1 value1 + * @param value2 value2 + * @return if value1 equals value2 + */ + private boolean timeConstantEqualityCheck(final String value1, final String value2) { + if (value1 == null || value2 == null) { + return false; + } + + return MessageDigest.isEqual(value1.getBytes(StandardCharsets.UTF_8), value2.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/oidc/StandardOidcIdentityProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/oidc/StandardOidcIdentityProvider.java new file mode 100644 index 0000000000..f43bef08fc --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/oidc/StandardOidcIdentityProvider.java @@ -0,0 +1,466 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.security.authentication.oidc; + +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.apache.nifi.registry.security.authentication.exception.IdentityAccessException; +import org.apache.nifi.registry.util.FormatUtils; +import org.apache.nifi.registry.web.security.authentication.jwt.JwtService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.proc.BadJOSEException; +import com.nimbusds.jose.util.DefaultResourceRetriever; +import com.nimbusds.jose.util.ResourceRetriever; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.oauth2.sdk.AuthorizationGrant; +import com.nimbusds.oauth2.sdk.ParseException; +import com.nimbusds.oauth2.sdk.Request; +import com.nimbusds.oauth2.sdk.Scope; +import com.nimbusds.oauth2.sdk.TokenErrorResponse; +import com.nimbusds.oauth2.sdk.TokenRequest; +import com.nimbusds.oauth2.sdk.TokenResponse; +import com.nimbusds.oauth2.sdk.auth.ClientAuthentication; +import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod; +import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic; +import com.nimbusds.oauth2.sdk.auth.ClientSecretPost; +import com.nimbusds.oauth2.sdk.auth.Secret; +import com.nimbusds.oauth2.sdk.http.HTTPRequest; +import com.nimbusds.oauth2.sdk.http.HTTPResponse; +import com.nimbusds.oauth2.sdk.id.ClientID; +import com.nimbusds.oauth2.sdk.token.BearerAccessToken; +import com.nimbusds.openid.connect.sdk.OIDCTokenResponse; +import com.nimbusds.openid.connect.sdk.OIDCTokenResponseParser; +import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse; +import com.nimbusds.openid.connect.sdk.UserInfoRequest; +import com.nimbusds.openid.connect.sdk.UserInfoResponse; +import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse; +import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet; +import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata; +import com.nimbusds.openid.connect.sdk.token.OIDCTokens; +import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator; +import net.minidev.json.JSONObject; + +/** + * OidcProvider for managing the OpenId Connect Authorization flow. + */ +@Component +public class StandardOidcIdentityProvider implements OidcIdentityProvider { + + private static final Logger logger = LoggerFactory.getLogger(StandardOidcIdentityProvider.class); + private final String EMAIL_CLAIM = "email"; + + private NiFiRegistryProperties properties; + private JwtService jwtService; + private OIDCProviderMetadata oidcProviderMetadata; + private int oidcConnectTimeout; + private int oidcReadTimeout; + private IDTokenValidator tokenValidator; + private ClientID clientId; + private Secret clientSecret; + + /** + * Creates a new StandardOidcIdentityProvider. + * + * @param jwtService jwt service + * @param properties properties + */ + @Autowired + public StandardOidcIdentityProvider(final JwtService jwtService, final NiFiRegistryProperties properties) { + this.properties = properties; + this.jwtService = jwtService; + } + + /** + * Loads OIDC configuration values from {@link NiFiRegistryProperties}, connects to external OIDC provider, and retrieves + * and validates provider metadata. + */ + @Override + public void initializeProvider() { + // attempt to process the oidc configuration if configured + if (!properties.isOidcEnabled()) { + logger.warn("The OIDC provider is not configured or enabled"); + return; + } + + validateOIDCConfiguration(); + + try { + // retrieve the oidc provider metadata + oidcProviderMetadata = retrieveOidcProviderMetadata(properties.getOidcDiscoveryUrl()); + } catch (IOException | ParseException e) { + throw new RuntimeException("Unable to retrieve OpenId Connect Provider metadata from: " + properties.getOidcDiscoveryUrl(), e); + } + + validateOIDCProviderMetadata(); + } + + /** + * Validates the retrieved OIDC provider metadata. + */ + private void validateOIDCProviderMetadata() { + // ensure the authorization endpoint is present + if (oidcProviderMetadata.getAuthorizationEndpointURI() == null) { + throw new RuntimeException("OpenId Connect Provider metadata does not contain an Authorization Endpoint."); + } + + // ensure the token endpoint is present + if (oidcProviderMetadata.getTokenEndpointURI() == null) { + throw new RuntimeException("OpenId Connect Provider metadata does not contain a Token Endpoint."); + } + + // ensure the oidc provider supports basic or post client auth + List clientAuthenticationMethods = oidcProviderMetadata.getTokenEndpointAuthMethods(); + logger.info("OpenId Connect: Available clientAuthenticationMethods {} ", clientAuthenticationMethods); + if (clientAuthenticationMethods == null || clientAuthenticationMethods.isEmpty()) { + clientAuthenticationMethods = new ArrayList<>(); + clientAuthenticationMethods.add(ClientAuthenticationMethod.CLIENT_SECRET_BASIC); + oidcProviderMetadata.setTokenEndpointAuthMethods(clientAuthenticationMethods); + logger.warn("OpenId Connect: ClientAuthenticationMethods is null, Setting clientAuthenticationMethods as CLIENT_SECRET_BASIC"); + } else if (!clientAuthenticationMethods.contains(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + && !clientAuthenticationMethods.contains(ClientAuthenticationMethod.CLIENT_SECRET_POST)) { + throw new RuntimeException(String.format("OpenId Connect Provider does not support %s or %s", + ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue(), + ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue())); + } + + // extract the supported json web signature algorithms + final List allowedAlgorithms = oidcProviderMetadata.getIDTokenJWSAlgs(); + if (allowedAlgorithms == null || allowedAlgorithms.isEmpty()) { + throw new RuntimeException("The OpenId Connect Provider does not support any JWS algorithms."); + } + + try { + // get the preferred json web signature algorithm + final String rawPreferredJwsAlgorithm = properties.getOidcPreferredJwsAlgorithm(); + + final JWSAlgorithm preferredJwsAlgorithm; + if (StringUtils.isBlank(rawPreferredJwsAlgorithm)) { + preferredJwsAlgorithm = JWSAlgorithm.RS256; + } else { + if ("none".equalsIgnoreCase(rawPreferredJwsAlgorithm)) { + preferredJwsAlgorithm = null; + } else { + preferredJwsAlgorithm = JWSAlgorithm.parse(rawPreferredJwsAlgorithm); + } + } + + if (preferredJwsAlgorithm == null) { + tokenValidator = new IDTokenValidator(oidcProviderMetadata.getIssuer(), clientId); + } else if (JWSAlgorithm.HS256.equals(preferredJwsAlgorithm) || JWSAlgorithm.HS384.equals(preferredJwsAlgorithm) || JWSAlgorithm.HS512.equals(preferredJwsAlgorithm)) { + tokenValidator = new IDTokenValidator(oidcProviderMetadata.getIssuer(), clientId, preferredJwsAlgorithm, clientSecret); + } else { + final ResourceRetriever retriever = new DefaultResourceRetriever(oidcConnectTimeout, oidcReadTimeout); + tokenValidator = new IDTokenValidator(oidcProviderMetadata.getIssuer(), clientId, preferredJwsAlgorithm, oidcProviderMetadata.getJWKSetURI().toURL(), retriever); + } + } catch (final Exception e) { + throw new RuntimeException("Unable to create the ID token validator for the configured OpenId Connect Provider: " + e.getMessage(), e); + } + } + + /** + * Loads the initial configuration values relating to the OIDC provider from the class {@link NiFiRegistryProperties} and populates the individual fields. + */ + private void validateOIDCConfiguration() { + if (properties.isLoginIdentityProviderEnabled()) { + throw new RuntimeException("OpenId Connect support cannot be enabled if the Login Identity Provider or Apache Knox SSO is configured."); + } + + // oidc connect timeout + final String rawConnectTimeout = properties.getOidcConnectTimeout(); + try { + oidcConnectTimeout = (int) FormatUtils.getPreciseTimeDuration(rawConnectTimeout, TimeUnit.MILLISECONDS); + } catch (final Exception e) { + logger.warn("Failed to parse value of property '{}' as a valid time period. Value was '{}'. Ignoring this value and using the default value of '{}'", + NiFiRegistryProperties.SECURITY_USER_OIDC_CONNECT_TIMEOUT, rawConnectTimeout, NiFiRegistryProperties.DEFAULT_SECURITY_USER_OIDC_CONNECT_TIMEOUT); + oidcConnectTimeout = (int) FormatUtils.getPreciseTimeDuration(NiFiRegistryProperties.DEFAULT_SECURITY_USER_OIDC_CONNECT_TIMEOUT, TimeUnit.MILLISECONDS); + } + + // oidc read timeout + final String rawReadTimeout = properties.getOidcReadTimeout(); + try { + oidcReadTimeout = (int) FormatUtils.getPreciseTimeDuration(rawReadTimeout, TimeUnit.MILLISECONDS); + } catch (final Exception e) { + logger.warn("Failed to parse value of property '{}' as a valid time period. Value was '{}'. Ignoring this value and using the default value of '{}'", + NiFiRegistryProperties.SECURITY_USER_OIDC_READ_TIMEOUT, rawReadTimeout, NiFiRegistryProperties.DEFAULT_SECURITY_USER_OIDC_READ_TIMEOUT); + oidcReadTimeout = (int) FormatUtils.getPreciseTimeDuration(NiFiRegistryProperties.DEFAULT_SECURITY_USER_OIDC_READ_TIMEOUT, TimeUnit.MILLISECONDS); + } + + // client id + final String rawClientId = properties.getOidcClientId(); + if (StringUtils.isBlank(rawClientId)) { + throw new RuntimeException("Client ID is required when configuring an OIDC Provider."); + } + clientId = new ClientID(rawClientId); + + // client secret + final String rawClientSecret = properties.getOidcClientSecret(); + if (StringUtils.isBlank(rawClientSecret)) { + throw new RuntimeException("Client secret is required when configuring an OIDC Provider."); + } + clientSecret = new Secret(rawClientSecret); + } + + /** + * Returns the retrieved OIDC provider metadata from the external provider. + * + * @param discoveryUri the remote OIDC provider endpoint for service discovery + * @return the provider metadata + * @throws IOException if there is a problem connecting to the remote endpoint + * @throws ParseException if there is a problem parsing the response + */ + private OIDCProviderMetadata retrieveOidcProviderMetadata(final String discoveryUri) throws IOException, ParseException { + final URL url = new URL(discoveryUri); + final HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.GET, url); + httpRequest.setConnectTimeout(oidcConnectTimeout); + httpRequest.setReadTimeout(oidcReadTimeout); + + final HTTPResponse httpResponse = httpRequest.send(); + + if (httpResponse.getStatusCode() != 200) { + throw new IOException("Unable to download OpenId Connect Provider metadata from " + url + ": Status code " + httpResponse.getStatusCode()); + } + + final JSONObject jsonObject = httpResponse.getContentAsJSONObject(); + return OIDCProviderMetadata.parse(jsonObject); + } + + @Override + public boolean isOidcEnabled() { + return properties.isOidcEnabled(); + } + + @Override + public URI getAuthorizationEndpoint() { + if (!isOidcEnabled()) { + throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED); + } + + return oidcProviderMetadata.getAuthorizationEndpointURI(); + } + + @Override + public URI getEndSessionEndpoint() { + if (!isOidcEnabled()) { + throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED); + } + return oidcProviderMetadata.getEndSessionEndpointURI(); + } + + @Override + public Scope getScope() { + if (!isOidcEnabled()) { + throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED); + } + + Scope scope = new Scope("openid", EMAIL_CLAIM); + + for (String additionalScope : properties.getOidcAdditionalScopes()) { + // Scope automatically prevents duplicated entries + scope.add(additionalScope); + } + + return scope; + } + + @Override + public ClientID getClientId() { + if (!isOidcEnabled()) { + throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED); + } + + return clientId; + } + + @Override + public String exchangeAuthorizationCode(final AuthorizationGrant authorizationGrant) throws IOException { + // Check if OIDC is enabled + if (!isOidcEnabled()) { + throw new IllegalStateException(OPEN_ID_CONNECT_SUPPORT_IS_NOT_CONFIGURED); + } + + // Build ClientAuthentication + final ClientAuthentication clientAuthentication = createClientAuthentication(); + + try { + // Build the token request + final HTTPRequest tokenHttpRequest = createTokenHTTPRequest(authorizationGrant, clientAuthentication); + return authorizeClient(tokenHttpRequest); + + } catch (final ParseException | JOSEException | BadJOSEException | java.text.ParseException e) { + throw new RuntimeException("Unable to parse the response from the Token request: " + e.getMessage()); + } + } + + private String authorizeClient(HTTPRequest tokenHttpRequest) throws ParseException, IOException, BadJOSEException, JOSEException, java.text.ParseException { + // Get the token response + final TokenResponse response = OIDCTokenResponseParser.parse(tokenHttpRequest.send()); + + // Handle success + if (response.indicatesSuccess()) { + return convertOIDCTokenToNiFiToken((OIDCTokenResponse) response); + } else { + // If the response was not successful + final TokenErrorResponse errorResponse = (TokenErrorResponse) response; + throw new RuntimeException("An error occurred while invoking the Token endpoint: " + + errorResponse.getErrorObject().getDescription()); + } + } + + private String convertOIDCTokenToNiFiToken(OIDCTokenResponse response) throws BadJOSEException, JOSEException, java.text.ParseException, IOException { + final OIDCTokenResponse oidcTokenResponse = response; + final OIDCTokens oidcTokens = oidcTokenResponse.getOIDCTokens(); + final JWT oidcJwt = oidcTokens.getIDToken(); + + // validate the token - no nonce required for authorization code flow + final IDTokenClaimsSet claimsSet = tokenValidator.validate(oidcJwt, null); + + // attempt to extract the configured claim to access the user's identity; default is 'email' + String identityClaim = properties.getOidcClaimIdentifyingUser(); + String identity = claimsSet.getStringClaim(identityClaim); + + // If default identity not available, attempt secondary identity extraction + if (StringUtils.isBlank(identity)) { + // Provide clear message to admin that desired claim is missing and present available claims + List availableClaims = getAvailableClaims(oidcJwt.getJWTClaimsSet()); + logger.warn("Failed to obtain the identity of the user with the claim '{}'. The available claims on " + + "the OIDC response are: {}. Will attempt to obtain the identity from secondary sources", + identityClaim, availableClaims); + + // If the desired user claim was not "email" and "email" is present, use that + if (!identityClaim.equalsIgnoreCase(EMAIL_CLAIM) && availableClaims.contains(EMAIL_CLAIM)) { + identity = claimsSet.getStringClaim(EMAIL_CLAIM); + logger.info("The 'email' claim was present. Using that claim to avoid extra remote call"); + } else { + identity = retrieveIdentityFromUserInfoEndpoint(oidcTokens); + logger.info("Retrieved identity from UserInfo endpoint"); + } + } + + // extract expiration details from the claims set + final Calendar now = Calendar.getInstance(); + final Date expiration = claimsSet.getExpirationTime(); + final long expiresIn = expiration.getTime() - now.getTimeInMillis(); + final String issuer = claimsSet.getIssuer().getValue(); + + // convert into a nifi jwt for retrieval later + return jwtService.generateSignedToken(identity, identity, issuer, issuer, expiresIn); + } + + private String retrieveIdentityFromUserInfoEndpoint(OIDCTokens oidcTokens) throws IOException { + // explicitly try to get the identity from the UserInfo endpoint with the configured claim + // extract the bearer access token + final BearerAccessToken bearerAccessToken = oidcTokens.getBearerAccessToken(); + if (bearerAccessToken == null) { + throw new IllegalStateException("No access token found in the ID tokens"); + } + + // invoke the UserInfo endpoint + HTTPRequest userInfoRequest = createUserInfoRequest(bearerAccessToken); + return lookupIdentityInUserInfo(userInfoRequest); + } + + private HTTPRequest createTokenHTTPRequest(AuthorizationGrant authorizationGrant, ClientAuthentication clientAuthentication) { + final TokenRequest request = new TokenRequest(oidcProviderMetadata.getTokenEndpointURI(), clientAuthentication, authorizationGrant); + return formHTTPRequest(request); + } + + private HTTPRequest createUserInfoRequest(BearerAccessToken bearerAccessToken) { + final UserInfoRequest request = new UserInfoRequest(oidcProviderMetadata.getUserInfoEndpointURI(), bearerAccessToken); + return formHTTPRequest(request); + } + + private HTTPRequest formHTTPRequest(Request request) { + final HTTPRequest httpRequest = request.toHTTPRequest(); + httpRequest.setConnectTimeout(oidcConnectTimeout); + httpRequest.setReadTimeout(oidcReadTimeout); + return httpRequest; + } + + private ClientAuthentication createClientAuthentication() { + final ClientAuthentication clientAuthentication; + List authMethods = oidcProviderMetadata.getTokenEndpointAuthMethods(); + if (authMethods != null && authMethods.contains(ClientAuthenticationMethod.CLIENT_SECRET_POST)) { + clientAuthentication = new ClientSecretPost(clientId, clientSecret); + } else { + clientAuthentication = new ClientSecretBasic(clientId, clientSecret); + } + return clientAuthentication; + } + + private static List getAvailableClaims(JWTClaimsSet claimSet) { + // Get the claims available in the ID token response + List presentClaims = claimSet.getClaims().entrySet().stream() + // Check claim values are not empty + .filter(e -> StringUtils.isNotBlank(e.getValue().toString())) + // If not empty, put claim name in a map + .map(Map.Entry::getKey) + .sorted() + .collect(Collectors.toList()); + return presentClaims; + } + + private String lookupIdentityInUserInfo(final HTTPRequest userInfoHttpRequest) throws IOException { + try { + // send the user request + final UserInfoResponse response = UserInfoResponse.parse(userInfoHttpRequest.send()); + + // interpret the details + if (response.indicatesSuccess()) { + final UserInfoSuccessResponse successResponse = (UserInfoSuccessResponse) response; + + final JWTClaimsSet claimsSet; + if (successResponse.getUserInfo() != null) { + claimsSet = successResponse.getUserInfo().toJWTClaimsSet(); + } else { + claimsSet = successResponse.getUserInfoJWT().getJWTClaimsSet(); + } + + final String identity = claimsSet.getStringClaim(properties.getOidcClaimIdentifyingUser()); + + // ensure we were able to get the user's identity + if (StringUtils.isBlank(identity)) { + throw new IllegalStateException("Unable to extract identity from the UserInfo token using the claim '" + + properties.getOidcClaimIdentifyingUser() + "'."); + } else { + return identity; + } + } else { + final UserInfoErrorResponse errorResponse = (UserInfoErrorResponse) response; + throw new IdentityAccessException("An error occurred while invoking the UserInfo endpoint: " + errorResponse.getErrorObject().getDescription()); + } + } catch (final ParseException | java.text.ParseException e) { + throw new IdentityAccessException("Unable to parse the response from the UserInfo token request: " + e.getMessage()); + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/util/CacheKey.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/util/CacheKey.java new file mode 100644 index 0000000000..e8c56e669f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/util/CacheKey.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.security.authentication.util; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; + +/** + * An authentication token that represents an Authenticated and Authorized user of the NiFi Apis. The authorities are based off the specified UserDetails. + */ + +/** + * Key for the cache. Necessary to override the default String.equals() to utilize MessageDigest.isEquals() to prevent timing attacks. + */ +public class CacheKey { + final String key; + + public CacheKey(String key) { + this.key = key; + } + + public String getKey() { + return key; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + final CacheKey otherCacheKey = (CacheKey) o; + return MessageDigest.isEqual(key.getBytes(StandardCharsets.UTF_8), otherCacheKey.key.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public int hashCode() { + return key.hashCode(); + } + + @Override + public String toString() { + return "CacheKey{token ending in '..." + key.substring(key.length() - 6) + "'}"; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/SubjectDnX509PrincipalExtractor.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/SubjectDnX509PrincipalExtractor.java new file mode 100644 index 0000000000..a9deae19e3 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/SubjectDnX509PrincipalExtractor.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.security.authentication.x509; + +import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor; +import org.springframework.stereotype.Component; + +import java.security.cert.X509Certificate; + +/** + * Principal extractor for extracting a DN. + */ +@Component +public class SubjectDnX509PrincipalExtractor implements X509PrincipalExtractor { + + @Override + public Object extractPrincipal(X509Certificate cert) { + return cert.getSubjectDN().getName().trim(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationRequestDetails.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationRequestDetails.java new file mode 100644 index 0000000000..aa24cd6ea5 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationRequestDetails.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.security.authentication.x509; + +import java.util.Objects; + +public class X509AuthenticationRequestDetails { + + private final String proxiedEntitiesChain; + + private final String httpMethod; + + public X509AuthenticationRequestDetails(final String proxiedEntitiesChain, final String httpMethod) { + this.proxiedEntitiesChain = proxiedEntitiesChain; + this.httpMethod = Objects.requireNonNull(httpMethod); + } + + public String getProxiedEntitiesChain() { + return proxiedEntitiesChain; + } + + public String getHttpMethod() { + return httpMethod; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509CertificateExtractor.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509CertificateExtractor.java new file mode 100644 index 0000000000..34ceada294 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509CertificateExtractor.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.security.authentication.x509; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import java.security.cert.X509Certificate; + +/** + * Extracts client certificates from Http requests. + */ +@Component +public class X509CertificateExtractor { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + /** + * Extract the client certificate from the specified HttpServletRequest or + * null if none is specified. + * + * @param request http request + * @return cert + */ + public X509Certificate[] extractClientCertificate(HttpServletRequest request) { + X509Certificate[] certs = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate"); + + if (certs != null && certs.length > 0) { + return certs; + } + + if (logger.isDebugEnabled()) { + logger.debug("No client certificate found in request."); + } + + return null; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityAuthenticationProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityAuthenticationProvider.java new file mode 100644 index 0000000000..04e4673e28 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityAuthenticationProvider.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.security.authentication.x509; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.security.authentication.AuthenticationRequest; +import org.apache.nifi.registry.security.authentication.AuthenticationResponse; +import org.apache.nifi.registry.security.authentication.IdentityProvider; +import org.apache.nifi.registry.security.authorization.Authorizer; +import org.apache.nifi.registry.security.authorization.user.NiFiUser; +import org.apache.nifi.registry.security.authorization.user.NiFiUserDetails; +import org.apache.nifi.registry.security.authorization.user.StandardNiFiUser; +import org.apache.nifi.registry.security.identity.IdentityMapper; +import org.apache.nifi.registry.security.util.ProxiedEntitiesUtils; +import org.apache.nifi.registry.web.security.authentication.AuthenticationRequestToken; +import org.apache.nifi.registry.web.security.authentication.AuthenticationSuccessToken; +import org.apache.nifi.registry.web.security.authentication.IdentityAuthenticationProvider; + +import java.util.List; +import java.util.ListIterator; +import java.util.Set; + +public class X509IdentityAuthenticationProvider extends IdentityAuthenticationProvider { + + public X509IdentityAuthenticationProvider(Authorizer authorizer, IdentityProvider identityProvider, IdentityMapper identityMapper) { + super(authorizer, identityProvider, identityMapper); + } + + @Override + protected AuthenticationSuccessToken buildAuthenticatedToken( + AuthenticationRequestToken requestToken, + AuthenticationResponse response) { + + final AuthenticationRequest authenticationRequest = requestToken.getAuthenticationRequest(); + + final Object requestDetails = authenticationRequest.getDetails(); + if (requestDetails == null || !(requestDetails instanceof X509AuthenticationRequestDetails)) { + throw new IllegalStateException("Invalid request details specified"); + } + + final X509AuthenticationRequestDetails x509RequestDetails = (X509AuthenticationRequestDetails) authenticationRequest.getDetails(); + + final String proxiedEntitiesChain = x509RequestDetails.getProxiedEntitiesChain(); + if (StringUtils.isBlank(proxiedEntitiesChain)) { + return super.buildAuthenticatedToken(requestToken, response); + } + + // build the entire proxy chain if applicable - + final List proxyChain = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(proxiedEntitiesChain); + proxyChain.add(response.getIdentity()); + + // add the chain as appropriate to each proxy + NiFiUser proxy = null; + for (final ListIterator chainIter = proxyChain.listIterator(proxyChain.size()); chainIter.hasPrevious(); ) { + String identity = chainIter.previous(); + + // determine if the user is anonymous + final boolean isAnonymous = StringUtils.isBlank(identity); + if (isAnonymous) { + identity = StandardNiFiUser.ANONYMOUS_IDENTITY; + } else { + identity = mapIdentity(identity); + } + + final Set groups = getUserGroups(identity); + + // Only set the client address for client making the request because we don't know the clientAddress of the proxied entities + String clientAddress = (proxy == null) ? requestToken.getClientAddress() : null; + proxy = createUser(identity, groups, proxy, clientAddress, isAnonymous); + } + + // Defer authorization of proxy until later in FrameworkAuthorizer + + return new AuthenticationSuccessToken(new NiFiUserDetails(proxy)); + } + + /** + * Returns a regular user populated with the provided values, or if the user should be anonymous, a well-formed instance of the anonymous user with the provided values. + * + * @param identity the user's identity + * @param chain the proxied entities + * @param clientAddress the requesting IP address + * @param isAnonymous if true, an anonymous user will be returned (identity will be ignored) + * @return the populated user + */ + private static NiFiUser createUser(String identity, Set groups, NiFiUser chain, String clientAddress, boolean isAnonymous) { + if (isAnonymous) { + return StandardNiFiUser.populateAnonymousUser(chain, clientAddress); + } else { + return new StandardNiFiUser.Builder().identity(identity).groups(groups).chain(chain).clientAddress(clientAddress).build(); + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityProvider.java new file mode 100644 index 0000000000..fc74f666bb --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityProvider.java @@ -0,0 +1,175 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.security.authentication.x509; + +import org.apache.nifi.registry.security.authentication.AuthenticationRequest; +import org.apache.nifi.registry.security.authentication.AuthenticationResponse; +import org.apache.nifi.registry.security.authentication.IdentityProvider; +import org.apache.nifi.registry.security.authentication.IdentityProviderConfigurationContext; +import org.apache.nifi.registry.security.authentication.IdentityProviderUsage; +import org.apache.nifi.registry.security.authentication.exception.InvalidCredentialsException; +import org.apache.nifi.registry.security.exception.SecurityProviderCreationException; +import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException; +import org.apache.nifi.registry.security.util.ProxiedEntitiesUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import java.util.concurrent.TimeUnit; + +/** + * Identity provider for extract the authenticating a ServletRequest with a X509Certificate. + */ +@Component +public class X509IdentityProvider implements IdentityProvider { + + private static final Logger logger = LoggerFactory.getLogger(X509IdentityProvider.class); + + private static final String issuer = X509IdentityProvider.class.getSimpleName(); + + private static final long expiration = TimeUnit.MILLISECONDS.convert(12, TimeUnit.HOURS); + + private static final IdentityProviderUsage usage = new IdentityProviderUsage() { + @Override + public String getText() { + return "The client must connect over HTTPS and must provide a client certificate during the TLS handshake. " + + "Additionally, the client may declare itself a proxy for another user identity by populating the " + + ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN + " HTTP header field with a value of the format " + + "'...'" + + "for all identities in the chain prior to this client. If the " + ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN + + " header is present in the request, this client's identity will be extracted from the client certificate " + + "used for TLS and added to the end of the chain, and then the entire chain will be authorized. Each proxy " + + "will be authorized to have 'write' access to '/proxy', and the originating user identity will be " + + "authorized for access to the resource being accessed in the request."; + } + + @Override + public AuthType getAuthType() { + return AuthType.OTHER.httpAuthScheme("TLS-client-cert"); + } + }; + + private X509PrincipalExtractor principalExtractor; + private X509CertificateExtractor certificateExtractor; + + @Autowired + public X509IdentityProvider(X509PrincipalExtractor principalExtractor, X509CertificateExtractor certificateExtractor) { + this.principalExtractor = principalExtractor; + this.certificateExtractor = certificateExtractor; + } + + @Override + public IdentityProviderUsage getUsageInstructions() { + return usage; + } + + /** + * Extracts certificate-based credentials from an {@link HttpServletRequest}. + * + * The resulting {@link AuthenticationRequest} will be populated as: + * - username: principal DN from first client cert + * - credentials: first client certificate (X509Certificate) + * - details: proxied-entities chain (String) + * + * @param servletRequest the {@link HttpServletRequest} request that may contain credentials understood by this IdentityProvider + * @return a populated AuthenticationRequest or null if the credentials could not be found. + */ + @Override + public AuthenticationRequest extractCredentials(HttpServletRequest servletRequest) { + + // only support x509 login when running securely + if (!servletRequest.isSecure()) { + return null; + } + + // look for a client certificate + final X509Certificate[] certificates = certificateExtractor.extractClientCertificate(servletRequest); + if (certificates == null || certificates.length == 0) { + return null; + } + + // extract the principal + final Object certificatePrincipal = principalExtractor.extractPrincipal(certificates[0]); + final String principal = certificatePrincipal.toString(); + + // extract the proxiedEntitiesChain header value from the servletRequest + final String proxiedEntitiesChainHeader = servletRequest.getHeader(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN); + final X509AuthenticationRequestDetails details = new X509AuthenticationRequestDetails(proxiedEntitiesChainHeader, servletRequest.getMethod()); + + return new AuthenticationRequest(principal, certificates[0], details); + + } + + /** + * For a given {@link AuthenticationRequest}, this validates the client certificate and creates a populated {@link AuthenticationResponse}. + * + * The {@link AuthenticationRequest} authenticationRequest paramenter is expected to be populated as: + * - username: principal DN from first client cert + * - credentials: first client certificate (X509Certificate) + * - details: proxied-entities chain (String) + * + * @param authenticationRequest the request, containing identity claim credentials for the IdentityProvider to authenticate and determine an identity + */ + @Override + public AuthenticationResponse authenticate(AuthenticationRequest authenticationRequest) throws InvalidCredentialsException { + + if (authenticationRequest == null || authenticationRequest.getUsername() == null) { + return null; + } + + String principal = authenticationRequest.getUsername(); + + try { + X509Certificate clientCertificate = (X509Certificate)authenticationRequest.getCredentials(); + validateClientCertificate(clientCertificate); + } catch (CertificateExpiredException cee) { + final String message = String.format("Client certificate for (%s) is expired.", principal); + logger.warn(message, cee); + throw new InvalidCredentialsException(message, cee); + } catch (CertificateNotYetValidException cnyve) { + final String message = String.format("Client certificate for (%s) is not yet valid.", principal); + logger.warn(message, cnyve); + throw new InvalidCredentialsException(message, cnyve); + } catch (final Exception e) { + logger.warn(e.getMessage(), e); + } + + // build the authentication response + return new AuthenticationResponse(principal, principal, expiration, issuer); + } + + @Override + public void onConfigured(IdentityProviderConfigurationContext configurationContext) throws SecurityProviderCreationException { + throw new SecurityProviderCreationException(X509IdentityProvider.class.getSimpleName() + + " does not currently support being loaded via IdentityProviderFactory"); + } + + @Override + public void preDestruction() throws SecurityProviderDestructionException {} + + + private void validateClientCertificate(X509Certificate certificate) throws CertificateExpiredException, CertificateNotYetValidException { + certificate.checkValidity(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authorization/HttpMethodAuthorizationRules.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authorization/HttpMethodAuthorizationRules.java new file mode 100644 index 0000000000..c940359bcb --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authorization/HttpMethodAuthorizationRules.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.security.authorization; + +import org.apache.nifi.registry.security.authorization.RequestAction; +import org.springframework.http.HttpMethod; + +public interface HttpMethodAuthorizationRules { + + default boolean requiresAuthorization(HttpMethod httpMethod) { + return true; + } + + default RequestAction mapHttpMethodToAction(HttpMethod httpMethod) { + + switch (httpMethod) { + case TRACE: + case OPTIONS: + case HEAD: + case GET: + return RequestAction.READ; + case POST: + case PUT: + case PATCH: + return RequestAction.WRITE; + case DELETE: + return RequestAction.DELETE; + default: + throw new IllegalArgumentException("Unknown http method: " + httpMethod); + } + + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authorization/ResourceAuthorizationFilter.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authorization/ResourceAuthorizationFilter.java new file mode 100644 index 0000000000..6e551e1ac5 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authorization/ResourceAuthorizationFilter.java @@ -0,0 +1,218 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.security.authorization; + +import org.apache.nifi.registry.security.authorization.AuthorizableLookup; +import org.apache.nifi.registry.security.authorization.RequestAction; +import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException; +import org.apache.nifi.registry.security.authorization.resource.Authorizable; +import org.apache.nifi.registry.security.authorization.resource.ResourceType; +import org.apache.nifi.registry.security.authorization.user.NiFiUser; +import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils; +import org.apache.nifi.registry.service.AuthorizationService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpMethod; +import org.springframework.web.filter.GenericFilterBean; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * This filter is designed to perform a resource authorization check in the Spring Security filter chain. + * + * It authorizes the current authenticated user for the {@link RequestAction} (based on the HttpMethod) requested + * on the {@link ResourceType} (based on the URI path). + * + * This filter is designed to be place after any authentication and before any application endpoints. + * + * This filter can be used in place of or in addition to authorization checks that occur in the application + * downstream of this filter. + * + * To configure this filter, provide an {@link AuthorizationService} that will be used to perform the authorization + * check, as well as a set of rules that control which resource and HTTP methods are handled by this filter. + * + * Any (ResourceType, HttpMethod) pair that is not configured to require authorization by this filter will be + * allowed to proceed in the filter chain without an authorization check. + * + * Any (ResourceType, HttpMethod) pair that is configured to require authorization by this filter will map + * the HttpMethod to a NiFi Registry RequestAction (configurable when creating this filter), and the + * (Resource Authorizable, RequestAction) pair will be sent to the AuthorizationService, which will use the + * configured Authorizer to authorize the current user for the action on the requested resource. + */ +public class ResourceAuthorizationFilter extends GenericFilterBean { + + private static final Logger logger = LoggerFactory.getLogger(ResourceAuthorizationFilter.class); + + private Map resourceTypeAuthorizationRules; + private AuthorizationService authorizationService; + private AuthorizableLookup authorizableLookup; + + ResourceAuthorizationFilter(Builder builder) { + if (builder.getAuthorizationService() == null || builder.getResourceTypeAuthorizationRules() == null) { + throw new IllegalArgumentException("Builder is missing one or more required fields [authorizationService, resourceTypeAuthorizationRules]."); + } + this.resourceTypeAuthorizationRules = builder.getResourceTypeAuthorizationRules(); + this.authorizationService = builder.getAuthorizationService(); + this.authorizableLookup = this.authorizationService.getAuthorizableLookup(); + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + + HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse; + + boolean authorizationCheckIsRequired = false; + String resourcePath = null; + RequestAction action = null; + + // Only require authorization if the NiFi Registry is running securely. + if (servletRequest.isSecure()) { + + // Only require authorization for resources for which this filter has been configured + resourcePath = httpServletRequest.getServletPath(); + if (resourcePath != null) { + final ResourceType resourceType = ResourceType.mapFullResourcePathToResourceType(resourcePath); + final HttpMethodAuthorizationRules authorizationRules = resourceTypeAuthorizationRules.get(resourceType); + if (authorizationRules != null) { + final String httpMethodStr = httpServletRequest.getMethod().toUpperCase(); + HttpMethod httpMethod = HttpMethod.resolve(httpMethodStr); + + // Only require authorization for HTTP methods included in this resource type's rule set + if (httpMethod != null && authorizationRules.requiresAuthorization(httpMethod)) { + authorizationCheckIsRequired = true; + action = authorizationRules.mapHttpMethodToAction(httpMethod); + } + } + } + } + + if (!authorizationCheckIsRequired) { + forwardRequestWithoutAuthorizationCheck(httpServletRequest, httpServletResponse, filterChain); + return; + } + + // Perform authorization check + try { + authorizeAccess(resourcePath, action); + successfulAuthorization(httpServletRequest, httpServletResponse, filterChain); + } catch (Exception e) { + logger.debug("Exception occurred while performing authorization check.", e); + failedAuthorization(httpServletRequest, httpServletResponse, filterChain, e); + } + } + + private boolean userIsAuthenticated() { + NiFiUser user = NiFiUserUtils.getNiFiUser(); + return (user != null && !user.isAnonymous()); + } + + private void authorizeAccess(String path, RequestAction action) throws AccessDeniedException { + + if (path == null || action == null) { + throw new IllegalArgumentException("Authorization is required, but a required input [resource, action] is absent."); + } + + Authorizable authorizable = authorizableLookup.getAuthorizableByResource(path); + + if (authorizable == null) { + throw new IllegalStateException("Resource Authorization Filter configured for non-authorizable resource: " + path); + } + + // throws AccessDeniedException if current user is not authorized to perform requested action on resource + authorizationService.authorize(authorizable, action); + } + + private void forwardRequestWithoutAuthorizationCheck(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException { + logger.debug("Request filter authorization check is not required for this HTTP Method on this resource. " + + "Allowing request to proceed. An additional authorization check might be performed downstream of this filter."); + chain.doFilter(req, res); + } + + private void successfulAuthorization(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException { + logger.debug("Request filter authorization check passed. Allowing request to proceed."); + chain.doFilter(req, res); + } + + private void failedAuthorization(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Exception failure) throws IOException, ServletException { + logger.debug("Request filter authorization check failed. Blocking access."); + + NiFiUser user = NiFiUserUtils.getNiFiUser(); + final String identity = (user != null) ? user.toString() : ""; + final int status = !userIsAuthenticated() ? HttpServletResponse.SC_UNAUTHORIZED : HttpServletResponse.SC_FORBIDDEN; + + logger.info("{} does not have permission to perform this action on the requested resource. {} Returning {} response.", identity, failure.getMessage(), status); + logger.debug("", failure); + + if (!response.isCommitted()) { + response.setStatus(status); + response.setContentType("text/plain"); + response.getWriter().println(String.format("Access is denied due to: %s Contact the system administrator.", failure.getLocalizedMessage())); + } + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private AuthorizationService authorizationService; + final private Map resourceTypeAuthorizationRules; + + // create via ResourceAuthorizationFilter.builder() + private Builder() { + this.resourceTypeAuthorizationRules = new HashMap<>(); + } + + public AuthorizationService getAuthorizationService() { + return authorizationService; + } + + public Builder setAuthorizationService(AuthorizationService authorizationService) { + this.authorizationService = authorizationService; + return this; + } + + public Map getResourceTypeAuthorizationRules() { + return resourceTypeAuthorizationRules; + } + + public Builder addResourceType(ResourceType resourceType) { + this.resourceTypeAuthorizationRules.put(resourceType, new HttpMethodAuthorizationRules() {}); + return this; + } + + public Builder addResourceType(ResourceType resourceType, HttpMethodAuthorizationRules authorizationRules) { + this.resourceTypeAuthorizationRules.put(resourceType, authorizationRules); + return this; + } + + public ResourceAuthorizationFilter build() { + return new ResourceAuthorizationFilter(this); + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authorization/StandardHttpMethodAuthorizationRules.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authorization/StandardHttpMethodAuthorizationRules.java new file mode 100644 index 0000000000..daa5a37c6f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authorization/StandardHttpMethodAuthorizationRules.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.security.authorization; + +import org.springframework.http.HttpMethod; + +import java.util.EnumSet; +import java.util.Set; + +public class StandardHttpMethodAuthorizationRules implements HttpMethodAuthorizationRules { + + final private Set methodsRequiringAuthorization; + + public StandardHttpMethodAuthorizationRules() { + this(EnumSet.allOf(HttpMethod.class)); + } + + public StandardHttpMethodAuthorizationRules(Set methodsRequiringAuthorization) { + this.methodsRequiringAuthorization = methodsRequiringAuthorization; + } + + @Override + public boolean requiresAuthorization(HttpMethod httpMethod) { + return methodsRequiringAuthorization.contains(httpMethod); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/RevisionConfiguration.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/RevisionConfiguration.java new file mode 100644 index 0000000000..2b270d6e1f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/RevisionConfiguration.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.service; + +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.apache.nifi.registry.revision.api.RevisionManager; +import org.apache.nifi.registry.revision.entity.RevisableEntityService; +import org.apache.nifi.registry.revision.entity.StandardRevisableEntityService; +import org.apache.nifi.registry.revision.jdbc.JdbcRevisionManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; + +/** + * Creates the beans needed for revision management. + */ +@Configuration +public class RevisionConfiguration { + + @Bean + public synchronized RevisionManager getRevisionManager(final JdbcTemplate jdbcTemplate) { + return new JdbcRevisionManager(jdbcTemplate); + } + + @Bean + public synchronized RevisableEntityService getRevisableEntityService(final RevisionManager revisionManager) { + return new StandardRevisableEntityService(revisionManager); + } + + @Bean + public synchronized RevisionFeature getRevisionFeature(final NiFiRegistryProperties properties) { + return () -> { + return properties.areRevisionsEnabled(); + }; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/RevisionFeature.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/RevisionFeature.java new file mode 100644 index 0000000000..c39f6d1b13 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/RevisionFeature.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.service; + +public interface RevisionFeature { + + boolean isEnabled(); + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/ServiceFacade.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/ServiceFacade.java new file mode 100644 index 0000000000..b1faac46c0 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/ServiceFacade.java @@ -0,0 +1,235 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.service; + +import org.apache.nifi.registry.RegistryConfiguration; +import org.apache.nifi.registry.authorization.AccessPolicy; +import org.apache.nifi.registry.authorization.CurrentUser; +import org.apache.nifi.registry.authorization.Resource; +import org.apache.nifi.registry.authorization.User; +import org.apache.nifi.registry.authorization.UserGroup; +import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.bucket.BucketItem; +import org.apache.nifi.registry.diff.VersionedFlowDifference; +import org.apache.nifi.registry.extension.bundle.Bundle; +import org.apache.nifi.registry.extension.bundle.BundleFilterParams; +import org.apache.nifi.registry.extension.bundle.BundleType; +import org.apache.nifi.registry.extension.bundle.BundleVersion; +import org.apache.nifi.registry.extension.bundle.BundleVersionFilterParams; +import org.apache.nifi.registry.extension.bundle.BundleVersionMetadata; +import org.apache.nifi.registry.extension.component.ExtensionFilterParams; +import org.apache.nifi.registry.extension.component.ExtensionMetadata; +import org.apache.nifi.registry.extension.component.TagCount; +import org.apache.nifi.registry.extension.component.manifest.Extension; +import org.apache.nifi.registry.extension.component.manifest.ProvidedServiceAPI; +import org.apache.nifi.registry.extension.repo.ExtensionRepoArtifact; +import org.apache.nifi.registry.extension.repo.ExtensionRepoBucket; +import org.apache.nifi.registry.extension.repo.ExtensionRepoExtensionMetadata; +import org.apache.nifi.registry.extension.repo.ExtensionRepoGroup; +import org.apache.nifi.registry.extension.repo.ExtensionRepoVersion; +import org.apache.nifi.registry.extension.repo.ExtensionRepoVersionSummary; +import org.apache.nifi.registry.flow.VersionedFlow; +import org.apache.nifi.registry.flow.VersionedFlowSnapshot; +import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata; +import org.apache.nifi.registry.revision.entity.RevisionInfo; +import org.apache.nifi.registry.security.authorization.RequestAction; + +import javax.ws.rs.core.StreamingOutput; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.List; +import java.util.Set; +import java.util.SortedSet; + +public interface ServiceFacade { + + // ---------------------- Bucket methods ---------------------------------------------- + + Bucket createBucket(Bucket bucket); + + Bucket getBucket(String bucketIdentifier); + + List getBuckets(); + + Bucket updateBucket(Bucket bucket); + + Bucket deleteBucket(String bucketIdentifier, RevisionInfo revisionInfo); + + // ---------------------- BucketItem methods ---------------------------------------------- + + List getBucketItems(String bucketIdentifier); + + List getBucketItems(); + + // ---------------------- Flow methods ---------------------------------------------- + + VersionedFlow createFlow(String bucketIdentifier, VersionedFlow versionedFlow); + + VersionedFlow getFlow(String bucketIdentifier, String flowIdentifier); + + VersionedFlow getFlow(String flowIdentifier); + + List getFlows(String bucketId); + + VersionedFlow updateFlow(VersionedFlow versionedFlow); + + VersionedFlow deleteFlow(String bucketIdentifier, String flowIdentifier, RevisionInfo revisionInfo); + + // ---------------------- Flow Snapshot methods ---------------------------------------------- + + VersionedFlowSnapshot createFlowSnapshot(VersionedFlowSnapshot flowSnapshot); + + VersionedFlowSnapshot getFlowSnapshot(String bucketIdentifier, String flowIdentifier, Integer version); + + VersionedFlowSnapshot getFlowSnapshot(String flowIdentifier, Integer version); + + VersionedFlowSnapshot getLatestFlowSnapshot(String bucketIdentifier, String flowIdentifier); + + VersionedFlowSnapshot getLatestFlowSnapshot(String flowIdentifier); + + SortedSet getFlowSnapshots(String bucketIdentifier, String flowIdentifier); + + SortedSet getFlowSnapshots(String flowIdentifier); + + VersionedFlowSnapshotMetadata getLatestFlowSnapshotMetadata(String bucketIdentifier, String flowIdentifier); + + VersionedFlowSnapshotMetadata getLatestFlowSnapshotMetadata(String flowIdentifier); + + VersionedFlowDifference getFlowDiff(String bucketIdentifier, String flowIdentifier, Integer versionA, Integer versionB); + + // ---------------------- Bundle methods ---------------------------------------------- + + List getBundles(BundleFilterParams filterParams); + + List getBundlesByBucket(String bucketIdentifier); + + Bundle getBundle(String bundleIdentifier); + + Bundle deleteBundle(String bundleIdentifier); + + // ---------------------- Bundle Version methods ---------------------------------------------- + + BundleVersion createBundleVersion(String bucketIdentifier, BundleType bundleType, InputStream inputStream, String clientSha256) throws IOException; + + SortedSet getBundleVersions(BundleVersionFilterParams filterParams); + + SortedSet getBundleVersions(String bundleIdentifier); + + BundleVersion getBundleVersion(String bundleId, String version); + + StreamingContent getBundleVersionContent(String bundleId, String version); + + BundleVersion deleteBundleVersion(String bundleId, String version); + + // ---------------------- Extension methods ---------------------------------------------- + + SortedSet getExtensionMetadata(ExtensionFilterParams filterParams); + + SortedSet getExtensionMetadata(ProvidedServiceAPI serviceAPI); + + SortedSet getExtensionMetadata(String bundleIdentifier, String version); + + Extension getExtension(String bundleIdentifier, String version, String name); + + StreamingOutput getExtensionDocs(String bundleIdentifier, String version, String name); + + StreamingOutput getAdditionalDetailsDocs(String bundleIdentifier, String version, String name); + + SortedSet getExtensionTags(); + + // ---------------------- Extension Repository methods ---------------------------------------------- + + SortedSet getExtensionRepoBuckets(URI baseUri); + + SortedSet getExtensionRepoGroups(URI baseUri, String bucketName); + + SortedSet getExtensionRepoArtifacts(URI baseUri, String bucketName, String groupId); + + SortedSet getExtensionRepoVersions(URI baseUri, String bucketName, String groupId, String artifactId); + + ExtensionRepoVersion getExtensionRepoVersion(URI baseUri, String bucketName, String groupId, String artifactId, String version); + + StreamingContent getExtensionRepoVersionContent(String bucketName, String groupId, String artifactId, String version); + + String getExtensionRepoVersionSha256(String bucketName, String groupId, String artifactId, String version); + + List getExtensionRepoExtensions(URI baseUri, String bucketName, String groupId, String artifactId, String version); + + Extension getExtensionRepoExtension(URI baseUri, String bucketName, String groupId, String artifactId, String version, String extensionName); + + StreamingOutput getExtensionRepoExtensionDocs(URI baseUri, String bucketName, String groupId, String artifactId, String version, String extensionName); + + StreamingOutput getExtensionRepoExtensionAdditionalDocs(URI baseUri, String bucketName, String groupId, String artifactId, String version, String extensionName); + + // ---------------------- Field methods --------------------------------------------- + + Set getBucketFields(); + + Set getBucketItemFields(); + + Set getFlowFields(); + + // ---------------------- User methods ---------------------------------------------- + + User createUser(User user); + + List getUsers(); + + User getUser(String identifier); + + User updateUser(User user); + + User deleteUser(String identifier, RevisionInfo revisionInfo); + + // ---------------------- User Group methods -------------------------------------- + + UserGroup createUserGroup(UserGroup userGroup); + + List getUserGroups(); + + UserGroup getUserGroup(String identifier); + + UserGroup updateUserGroup(UserGroup userGroup); + + UserGroup deleteUserGroup(String identifier, RevisionInfo revisionInfo); + + // ---------------------- Access Policy methods ---------------------------------------- + + AccessPolicy createAccessPolicy(AccessPolicy accessPolicy); + + AccessPolicy getAccessPolicy(String identifier); + + AccessPolicy getAccessPolicy(String resource, RequestAction action); + + List getAccessPolicies(); + + AccessPolicy updateAccessPolicy(AccessPolicy accessPolicy); + + AccessPolicy deleteAccessPolicy(String identifier, RevisionInfo revisionInfo); + + List getResources(); + + // ---------------------- Permission methods ---------------------------------------- + + CurrentUser getCurrentUser(); + + // ---------------------- Configuration methods ------------------------------------ + + RegistryConfiguration getRegistryConfiguration(); + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StandardServiceFacade.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StandardServiceFacade.java new file mode 100644 index 0000000000..85be421f1f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StandardServiceFacade.java @@ -0,0 +1,1248 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.service; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.RegistryConfiguration; +import org.apache.nifi.registry.authorization.AccessPolicy; +import org.apache.nifi.registry.authorization.CurrentUser; +import org.apache.nifi.registry.authorization.Resource; +import org.apache.nifi.registry.authorization.User; +import org.apache.nifi.registry.authorization.UserGroup; +import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.bucket.BucketItem; +import org.apache.nifi.registry.diff.VersionedFlowDifference; +import org.apache.nifi.registry.extension.bundle.Bundle; +import org.apache.nifi.registry.extension.bundle.BundleFilterParams; +import org.apache.nifi.registry.extension.bundle.BundleType; +import org.apache.nifi.registry.extension.bundle.BundleVersion; +import org.apache.nifi.registry.extension.bundle.BundleVersionFilterParams; +import org.apache.nifi.registry.extension.bundle.BundleVersionMetadata; +import org.apache.nifi.registry.extension.component.ExtensionFilterParams; +import org.apache.nifi.registry.extension.component.ExtensionMetadata; +import org.apache.nifi.registry.extension.component.TagCount; +import org.apache.nifi.registry.extension.component.manifest.Extension; +import org.apache.nifi.registry.extension.component.manifest.ProvidedServiceAPI; +import org.apache.nifi.registry.extension.repo.ExtensionRepoArtifact; +import org.apache.nifi.registry.extension.repo.ExtensionRepoBucket; +import org.apache.nifi.registry.extension.repo.ExtensionRepoExtensionMetadata; +import org.apache.nifi.registry.extension.repo.ExtensionRepoGroup; +import org.apache.nifi.registry.extension.repo.ExtensionRepoVersion; +import org.apache.nifi.registry.extension.repo.ExtensionRepoVersionSummary; +import org.apache.nifi.registry.flow.VersionedFlow; +import org.apache.nifi.registry.flow.VersionedFlowSnapshot; +import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata; +import org.apache.nifi.registry.revision.api.InvalidRevisionException; +import org.apache.nifi.registry.revision.entity.RevisableEntity; +import org.apache.nifi.registry.revision.entity.RevisableEntityService; +import org.apache.nifi.registry.revision.entity.RevisionInfo; +import org.apache.nifi.registry.security.authorization.AuthorizableLookup; +import org.apache.nifi.registry.security.authorization.RequestAction; +import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException; +import org.apache.nifi.registry.security.authorization.resource.Authorizable; +import org.apache.nifi.registry.security.authorization.resource.ResourceType; +import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils; +import org.apache.nifi.registry.service.AuthorizationService; +import org.apache.nifi.registry.service.RegistryService; +import org.apache.nifi.registry.service.extension.ExtensionService; +import org.apache.nifi.registry.web.link.LinkService; +import org.apache.nifi.registry.web.security.PermissionsService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Isolation; +import org.springframework.transaction.annotation.Transactional; + +import javax.ws.rs.core.Link; +import javax.ws.rs.core.StreamingOutput; +import javax.ws.rs.core.UriBuilder; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.SortedSet; +import java.util.UUID; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * A wrapper around the service layer that applies validation, authorization, and revision management to all services. + * + * All REST resources should access the service layer through this facade. + */ +@Service +@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Throwable.class) +public class StandardServiceFacade implements ServiceFacade { + + private static final String INVALID_REVISION_MSG = "The %s you attempted to %s with id '%s' is out of date with the server. " + + "You may need to refresh your client and try again."; + + public static final String USER_GROUP_ENTITY_TYPE = "User Group"; + public static final String USER_ENTITY_TYPE = "User"; + public static final String ACCESS_POLICY_ENTITY_TYPE = "Access Policy"; + public static final String VERSIONED_FLOW_ENTITY_TYPE = "Versioned Flow"; + public static final String BUCKET_ENTITY_TYPE = "Bucket"; + + private final RegistryService registryService; + private final ExtensionService extensionService; + private final AuthorizationService authorizationService; + private final AuthorizableLookup authorizableLookup; + private final RevisableEntityService entityService; + private final RevisionFeature revisionFeature; + private final PermissionsService permissionsService; + private final LinkService linkService; + + @Autowired + public StandardServiceFacade(final RegistryService registryService, + final ExtensionService extensionService, + final AuthorizationService authorizationService, + final AuthorizableLookup authorizableLookup, + final RevisableEntityService entityService, + final RevisionFeature revisionFeature, + final PermissionsService permissionsService, + final LinkService linkService) { + this.registryService = registryService; + this.extensionService = extensionService; + this.authorizationService = authorizationService; + this.authorizableLookup = authorizableLookup; + this.entityService = entityService; + this.revisionFeature = revisionFeature; + this.permissionsService = permissionsService; + this.linkService = linkService; + } + + private String currentUserIdentity() { + return NiFiUserUtils.getNiFiUserIdentity(); + } + + // ---------------------- Bucket methods ---------------------------------------------- + + @Override + public Bucket createBucket(final Bucket bucket) { + authorizeBucketsAccess(RequestAction.WRITE); + validateCreationOfRevisableEntity(bucket, BUCKET_ENTITY_TYPE); + validateIdentifierNotPresent(bucket, BUCKET_ENTITY_TYPE); + + bucket.setIdentifier(UUID.randomUUID().toString()); + + final Bucket createdBucket = createRevisableEntity(bucket, BUCKET_ENTITY_TYPE, currentUserIdentity(), + () -> registryService.createBucket(bucket)); + permissionsService.populateBucketPermissions(createdBucket); + linkService.populateLinks(createdBucket); + return createdBucket; + } + + @Override + public Bucket getBucket(final String bucketIdentifier) { + authorizeBucketAccess(RequestAction.READ, bucketIdentifier); + + final Bucket bucket = entityService.get(() -> registryService.getBucket(bucketIdentifier)); + permissionsService.populateBucketPermissions(bucket); + linkService.populateLinks(bucket); + return bucket; + } + + @Override + public List getBuckets() { + final Set authorizedBucketIds = getAuthorizedBucketIds(RequestAction.READ); + if (authorizedBucketIds == null || authorizedBucketIds.isEmpty()) { + // not authorized for any bucket, return empty list of items + return Collections.emptyList(); + } + + final List buckets = entityService.getEntities(() -> registryService.getBuckets(authorizedBucketIds)); + permissionsService.populateBucketPermissions(buckets); + linkService.populateLinks(buckets); + return buckets; + } + + @Override + public Bucket updateBucket(final Bucket bucket) { + authorizeBucketAccess(RequestAction.WRITE, bucket.getIdentifier()); + validateUpdateOfRevisableEntity(bucket, BUCKET_ENTITY_TYPE); + + // verify outside of the revisable update so ResourceNotFoundException will be thrown instead of InvalidRevisionException + registryService.verifyBucketExists(bucket.getIdentifier()); + + final Bucket updatedBucket = updateRevisableEntity(bucket, BUCKET_ENTITY_TYPE, currentUserIdentity(), + () -> registryService.updateBucket(bucket)); + permissionsService.populateBucketPermissions(updatedBucket); + linkService.populateLinks(updatedBucket); + return updatedBucket; + } + + @Override + public Bucket deleteBucket(final String bucketIdentifier, final RevisionInfo revisionInfo) { + authorizeBucketAccess(RequestAction.DELETE, bucketIdentifier); + validateDeleteOfRevisableEntity(bucketIdentifier, revisionInfo, BUCKET_ENTITY_TYPE); + + // verify outside of the revisable update so ResourceNotFoundException will be thrown instead of InvalidRevisionException + registryService.verifyBucketExists(bucketIdentifier); + + return deleteRevisableEntity(bucketIdentifier, BUCKET_ENTITY_TYPE, revisionInfo, + () -> registryService.deleteBucket(bucketIdentifier)); + } + + // ---------------------- BucketItem methods ---------------------------------------------- + + @Override + public List getBucketItems(final String bucketIdentifier) { + authorizeBucketAccess(RequestAction.READ, bucketIdentifier); + + final List items = registryService.getBucketItems(bucketIdentifier); + entityService.populateRevisions(items); + permissionsService.populateItemPermissions(items); + linkService.populateLinks(items); + return items; + } + + @Override + public List getBucketItems() { + final Set authorizedBucketIds = getAuthorizedBucketIds(RequestAction.READ); + if (authorizedBucketIds == null || authorizedBucketIds.isEmpty()) { + // not authorized for any bucket, return empty list of items + return new ArrayList<>(); + } + + final List items = registryService.getBucketItems(authorizedBucketIds); + entityService.populateRevisions(items); + permissionsService.populateItemPermissions(items); + linkService.populateLinks(items); + return items; + } + + // ---------------------- Flow methods ---------------------------------------------- + + @Override + public VersionedFlow createFlow(final String bucketIdentifier, final VersionedFlow versionedFlow) { + authorizeBucketAccess(RequestAction.WRITE, bucketIdentifier); + validateCreationOfRevisableEntity(versionedFlow, VERSIONED_FLOW_ENTITY_TYPE); + + // NOTE: Don't validate that identifier is null... + // NiFi has been sending an identifier, so we must maintain backwards compatibility + if (versionedFlow.getIdentifier() == null) { + versionedFlow.setIdentifier(UUID.randomUUID().toString()); + } + + final VersionedFlow createdFlow = createRevisableEntity(versionedFlow, VERSIONED_FLOW_ENTITY_TYPE, currentUserIdentity(), + () -> registryService.createFlow(bucketIdentifier, versionedFlow)); + permissionsService.populateItemPermissions(createdFlow); + linkService.populateLinks(createdFlow); + return createdFlow; + } + + @Override + public VersionedFlow getFlow(final String bucketIdentifier, final String flowIdentifier) { + authorizeBucketAccess(RequestAction.READ, bucketIdentifier); + + final VersionedFlow flow = entityService.get( + () -> registryService.getFlow(bucketIdentifier, flowIdentifier)); + permissionsService.populateItemPermissions(flow); + linkService.populateLinks(flow); + return flow; + } + + @Override + public VersionedFlow getFlow(final String flowIdentifier) { + final VersionedFlow flow = entityService.get(() -> registryService.getFlow(flowIdentifier)); + authorizeBucketAccess(RequestAction.READ, flow); + + permissionsService.populateItemPermissions(flow); + linkService.populateLinks(flow); + return flow; + } + + @Override + public List getFlows(final String bucketIdentifier) { + authorizeBucketAccess(RequestAction.READ, bucketIdentifier); + + final List flows = entityService.getEntities(() -> registryService.getFlows(bucketIdentifier)); + permissionsService.populateItemPermissions(flows); + linkService.populateLinks(flows); + return flows; + } + + @Override + public VersionedFlow updateFlow(final VersionedFlow versionedFlow) { + authorizeBucketAccess(RequestAction.WRITE, versionedFlow); + validateUpdateOfRevisableEntity(versionedFlow, VERSIONED_FLOW_ENTITY_TYPE); + + // verify outside of the revisable update so ResourceNotFoundException will be thrown instead of InvalidRevisionException + registryService.verifyFlowExists(versionedFlow.getIdentifier()); + + final VersionedFlow updatedFlow = updateRevisableEntity(versionedFlow, VERSIONED_FLOW_ENTITY_TYPE, currentUserIdentity(), + () -> registryService.updateFlow(versionedFlow)); + permissionsService.populateItemPermissions(updatedFlow); + linkService.populateLinks(updatedFlow); + return updatedFlow; + } + + @Override + public VersionedFlow deleteFlow(final String bucketIdentifier, final String flowIdentifier, final RevisionInfo revisionInfo) { + authorizeBucketAccess(RequestAction.DELETE, bucketIdentifier); + validateDeleteOfRevisableEntity(flowIdentifier, revisionInfo, VERSIONED_FLOW_ENTITY_TYPE); + + // verify outside of the revisable update so ResourceNotFoundException will be thrown instead of InvalidRevisionException + registryService.verifyFlowExists(flowIdentifier); + + return deleteRevisableEntity(flowIdentifier, VERSIONED_FLOW_ENTITY_TYPE, revisionInfo, + () -> registryService.deleteFlow(bucketIdentifier, flowIdentifier)); + } + + // ---------------------- Flow Snapshot methods ---------------------------------------------- + + @Override + public VersionedFlowSnapshot createFlowSnapshot(final VersionedFlowSnapshot flowSnapshot) { + authorizeBucketAccess(RequestAction.WRITE, flowSnapshot); + + final VersionedFlowSnapshot createdSnapshot = registryService.createFlowSnapshot(flowSnapshot); + populateLinksAndPermissions(createdSnapshot); + return createdSnapshot; + } + + @Override + public VersionedFlowSnapshot getFlowSnapshot(final String bucketIdentifier, final String flowIdentifier, final Integer version) { + authorizeBucketAccess(RequestAction.READ, bucketIdentifier); + + final VersionedFlowSnapshot snapshot = registryService.getFlowSnapshot(bucketIdentifier, flowIdentifier, version); + populateLinksAndPermissions(snapshot); + return snapshot; + } + + @Override + public VersionedFlowSnapshot getFlowSnapshot(final String flowIdentifier, final Integer version) { + final VersionedFlowSnapshotMetadata latestMetadata = registryService.getLatestFlowSnapshotMetadata(flowIdentifier); + authorizeBucketAccess(RequestAction.READ, latestMetadata); + + final String bucketIdentifier = latestMetadata.getBucketIdentifier(); + final VersionedFlowSnapshot snapshot = registryService.getFlowSnapshot(bucketIdentifier, flowIdentifier, version); + populateLinksAndPermissions(snapshot); + return snapshot; + } + + @Override + public VersionedFlowSnapshot getLatestFlowSnapshot(final String bucketIdentifier, final String flowIdentifier) { + authorizeBucketAccess(RequestAction.READ, bucketIdentifier); + + final VersionedFlowSnapshotMetadata latestMetadata = getLatestFlowSnapshotMetadata(bucketIdentifier, flowIdentifier); + final VersionedFlowSnapshot lastSnapshot = getFlowSnapshot(bucketIdentifier, flowIdentifier, latestMetadata.getVersion()); + populateLinksAndPermissions(lastSnapshot); + return lastSnapshot; + } + + @Override + public VersionedFlowSnapshot getLatestFlowSnapshot(final String flowIdentifier) { + final VersionedFlowSnapshotMetadata latestMetadata = registryService.getLatestFlowSnapshotMetadata(flowIdentifier); + authorizeBucketAccess(RequestAction.READ, latestMetadata); + + final String bucketIdentifier = latestMetadata.getBucketIdentifier(); + final Integer latestVersion = latestMetadata.getVersion(); + + final VersionedFlowSnapshot lastSnapshot = registryService.getFlowSnapshot(bucketIdentifier, flowIdentifier, latestVersion); + populateLinksAndPermissions(lastSnapshot); + return lastSnapshot; + } + + @Override + public SortedSet getFlowSnapshots(final String bucketIdentifier, final String flowIdentifier) { + authorizeBucketAccess(RequestAction.READ, bucketIdentifier); + + final SortedSet snapshots = registryService.getFlowSnapshots(bucketIdentifier, flowIdentifier); + linkService.populateLinks(snapshots); + return snapshots; + } + + @Override + public SortedSet getFlowSnapshots(final String flowIdentifier) { + final VersionedFlow flow = registryService.getFlow(flowIdentifier); + authorizeBucketAccess(RequestAction.READ, flow); + + final String bucketIdentifier = flow.getBucketIdentifier(); + final SortedSet snapshots = registryService.getFlowSnapshots(bucketIdentifier, flowIdentifier); + linkService.populateLinks(snapshots); + return snapshots; + } + + @Override + public VersionedFlowSnapshotMetadata getLatestFlowSnapshotMetadata(final String bucketIdentifier, final String flowIdentifier) { + authorizeBucketAccess(RequestAction.READ, bucketIdentifier); + + final VersionedFlowSnapshotMetadata latest = registryService.getLatestFlowSnapshotMetadata(bucketIdentifier, flowIdentifier); + linkService.populateLinks(latest); + return latest; + } + + @Override + public VersionedFlowSnapshotMetadata getLatestFlowSnapshotMetadata(final String flowIdentifier) { + final VersionedFlowSnapshotMetadata latest = registryService.getLatestFlowSnapshotMetadata(flowIdentifier); + authorizeBucketAccess(RequestAction.READ, latest); + + linkService.populateLinks(latest); + return latest; + } + + @Override + public VersionedFlowDifference getFlowDiff(final String bucketIdentifier, final String flowIdentifier, final Integer versionA, final Integer versionB) { + authorizeBucketAccess(RequestAction.READ, bucketIdentifier); + return registryService.getFlowDiff(bucketIdentifier, flowIdentifier, versionA, versionB); + } + + private void populateLinksAndPermissions(final VersionedFlowSnapshot snapshot) { + if (snapshot == null) { + return; + } + + if (snapshot.getSnapshotMetadata() != null) { + linkService.populateLinks(snapshot.getSnapshotMetadata()); + } + + if (snapshot.getFlow() != null) { + linkService.populateLinks(snapshot.getFlow()); + } + + if (snapshot.getBucket() != null) { + permissionsService.populateBucketPermissions(snapshot.getBucket()); + linkService.populateLinks(snapshot.getBucket()); + } + } + + // ---------------------- Bundle methods ---------------------------------------------- + + @Override + public List getBundles(final BundleFilterParams filterParams) { + final Set authorizedBucketIds = getAuthorizedBucketIds(RequestAction.READ); + if (authorizedBucketIds == null || authorizedBucketIds.isEmpty()) { + // not authorized for any bucket, return empty list of items + return new ArrayList<>(); + } + + final List bundles = extensionService.getBundles(authorizedBucketIds, filterParams); + permissionsService.populateItemPermissions(bundles); + linkService.populateLinks(bundles); + return bundles; + } + + @Override + public List getBundlesByBucket(final String bucketIdentifier) { + authorizeBucketAccess(RequestAction.READ, bucketIdentifier); + + final List bundles = extensionService.getBundlesByBucket(bucketIdentifier); + permissionsService.populateItemPermissions(bundles); + linkService.populateLinks(bundles); + return bundles; + } + + @Override + public Bundle getBundle(final String bundleIdentifier) { + final Bundle bundle = extensionService.getBundle(bundleIdentifier); + authorizeBucketAccess(RequestAction.READ, bundle); + + permissionsService.populateItemPermissions(bundle); + linkService.populateLinks(bundle); + return bundle; + } + + @Override + public Bundle deleteBundle(final String bundleIdentifier) { + final Bundle bundle = extensionService.getBundle(bundleIdentifier); + authorizeBucketAccess(RequestAction.READ, bundle); + authorizeBucketAccess(RequestAction.DELETE, bundle); + + final Bundle deletedBundle = extensionService.deleteBundle(bundle); + permissionsService.populateItemPermissions(deletedBundle); + linkService.populateLinks(deletedBundle); + return deletedBundle; + } + + // ---------------------- Bundle Version methods ---------------------------------------------- + + @Override + public BundleVersion createBundleVersion(final String bucketIdentifier, final BundleType bundleType, + final InputStream inputStream, final String clientSha256) throws IOException { + + authorizeBucketAccess(RequestAction.WRITE, bucketIdentifier); + + final BundleVersion createdBundleVersion = extensionService.createBundleVersion( + bucketIdentifier, bundleType, inputStream, clientSha256); + + linkService.populateLinks(createdBundleVersion.getVersionMetadata()); + linkService.populateLinks(createdBundleVersion.getBundle()); + linkService.populateLinks(createdBundleVersion.getBucket()); + + permissionsService.populateItemPermissions(createdBundleVersion.getBundle()); + + return createdBundleVersion; + } + + @Override + public SortedSet getBundleVersions(final BundleVersionFilterParams filterParams) { + final Set authorizedBucketIds = getAuthorizedBucketIds(RequestAction.READ); + if (authorizedBucketIds == null || authorizedBucketIds.isEmpty()) { + // not authorized for any bucket, return empty list of items + return Collections.emptySortedSet(); + } + + final SortedSet bundleVersions = extensionService.getBundleVersions(authorizedBucketIds, filterParams); + linkService.populateLinks(bundleVersions); + return bundleVersions; + } + + @Override + public SortedSet getBundleVersions(final String bundleIdentifier) { + final Bundle bundle = extensionService.getBundle(bundleIdentifier); + authorizeBucketAccess(RequestAction.READ, bundle); + + final SortedSet bundleVersions = extensionService.getBundleVersions(bundleIdentifier); + linkService.populateLinks(bundleVersions); + return bundleVersions; + } + + @Override + public BundleVersion getBundleVersion(final String bundleIdentifier, final String version) { + final Bundle bundle = extensionService.getBundle(bundleIdentifier); + authorizeBucketAccess(RequestAction.READ, bundle); + + final String bucketIdentifier = bundle.getBucketIdentifier(); + final BundleVersion bundleVersion = extensionService.getBundleVersion(bucketIdentifier, bundleIdentifier, version); + linkService.populateLinks(bundleVersion); + return bundleVersion; + } + + @Override + public StreamingContent getBundleVersionContent(final String bundleIdentifier, final String version) { + final Bundle bundle = extensionService.getBundle(bundleIdentifier); + authorizeBucketAccess(RequestAction.READ, bundle); + + final String bucketIdentifier = bundle.getBucketIdentifier(); + final BundleVersion bundleVersion = extensionService.getBundleVersion(bucketIdentifier, bundleIdentifier, version); + + final StreamingOutput streamingOutput = (output) -> extensionService.writeBundleVersionContent(bundleVersion, output); + return new StreamingContent(streamingOutput, bundleVersion.getFilename()); + } + + @Override + public BundleVersion deleteBundleVersion(final String bundleIdentifier, final String version) { + final Bundle bundle = extensionService.getBundle(bundleIdentifier); + authorizeBucketAccess(RequestAction.READ, bundle); + authorizeBucketAccess(RequestAction.DELETE, bundle); + + final String bucketIdentifier = bundle.getBucketIdentifier(); + final BundleVersion bundleVersion = extensionService.getBundleVersion(bucketIdentifier, bundleIdentifier, version); + + final BundleVersion deletedBundleVersion = extensionService.deleteBundleVersion(bundleVersion); + linkService.populateLinks(deletedBundleVersion); + return deletedBundleVersion; + } + + // ---------------------- Extension methods ---------------------------------------------- + + @Override + public SortedSet getExtensionMetadata(final ExtensionFilterParams filterParams) { + final Set authorizedBucketIds = getAuthorizedBucketIds(RequestAction.READ); + if (authorizedBucketIds == null || authorizedBucketIds.isEmpty()) { + // not authorized for any bucket, return empty list of items + return Collections.emptySortedSet(); + } + + final SortedSet metadata = extensionService.getExtensionMetadata(authorizedBucketIds, filterParams); + linkService.populateLinks(metadata); + return metadata; + } + + @Override + public SortedSet getExtensionMetadata(final ProvidedServiceAPI serviceAPI) { + final Set authorizedBucketIds = getAuthorizedBucketIds(RequestAction.READ); + if (authorizedBucketIds == null || authorizedBucketIds.isEmpty()) { + // not authorized for any bucket, return empty list of items + return Collections.emptySortedSet(); + } + + final SortedSet metadata = extensionService.getExtensionMetadata(authorizedBucketIds, serviceAPI); + linkService.populateLinks(metadata); + return metadata; + } + + @Override + public SortedSet getExtensionMetadata(final String bundleIdentifier, final String version) { + final Bundle bundle = extensionService.getBundle(bundleIdentifier); + authorizeBucketAccess(RequestAction.READ, bundle); + + final String bucketIdentifier = bundle.getBucketIdentifier(); + final BundleVersion bundleVersion = extensionService.getBundleVersion(bucketIdentifier, bundleIdentifier, version); + + final SortedSet extensions = extensionService.getExtensionMetadata(bundleVersion); + linkService.populateLinks(extensions); + return extensions; + } + + @Override + public Extension getExtension(final String bundleIdentifier, final String version, final String name) { + final Bundle bundle = extensionService.getBundle(bundleIdentifier); + authorizeBucketAccess(RequestAction.READ, bundle); + + final String bucketIdentifier = bundle.getBucketIdentifier(); + final BundleVersion bundleVersion = extensionService.getBundleVersion(bucketIdentifier, bundleIdentifier, version); + return extensionService.getExtension(bundleVersion, name); + } + + @Override + public StreamingOutput getExtensionDocs(final String bundleIdentifier, final String version, final String name) { + final Bundle bundle = extensionService.getBundle(bundleIdentifier); + authorizeBucketAccess(RequestAction.READ, bundle); + + final String bucketIdentifier = bundle.getBucketIdentifier(); + final BundleVersion bundleVersion = extensionService.getBundleVersion(bucketIdentifier, bundleIdentifier, version); + + final StreamingOutput streamingOutput = (output) -> extensionService.writeExtensionDocs(bundleVersion, name, output); + return streamingOutput; + } + + @Override + public StreamingOutput getAdditionalDetailsDocs(final String bundleIdentifier, final String version, final String name) { + final Bundle bundle = extensionService.getBundle(bundleIdentifier); + authorizeBucketAccess(RequestAction.READ, bundle); + + final String bucketIdentifier = bundle.getBucketIdentifier(); + final BundleVersion bundleVersion = extensionService.getBundleVersion(bucketIdentifier, bundleIdentifier, version); + + final StreamingOutput streamingOutput = (output) -> extensionService.writeAdditionalDetailsDocs(bundleVersion, name, output); + return streamingOutput; + } + + @Override + public SortedSet getExtensionTags() { + return extensionService.getExtensionTags(); + } + + // ---------------------- Extension Repository methods ---------------------------------------------- + + @Override + public SortedSet getExtensionRepoBuckets(final URI baseUri) { + final Set authorizedBucketIds = getAuthorizedBucketIds(RequestAction.READ); + if (authorizedBucketIds == null || authorizedBucketIds.isEmpty()) { + // not authorized for any bucket, return empty list of items + return Collections.emptySortedSet(); + } + + final SortedSet repoBuckets = extensionService.getExtensionRepoBuckets(authorizedBucketIds); + linkService.populateFullLinks(repoBuckets, baseUri); + return repoBuckets; + } + + @Override + public SortedSet getExtensionRepoGroups(final URI baseUri, final String bucketName) { + final Bucket bucket = registryService.getBucketByName(bucketName); + authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier()); + + final SortedSet repoGroups = extensionService.getExtensionRepoGroups(bucket); + linkService.populateFullLinks(repoGroups, baseUri); + return extensionService.getExtensionRepoGroups(bucket); + } + + @Override + public SortedSet getExtensionRepoArtifacts(final URI baseUri, final String bucketName, final String groupId) { + final Bucket bucket = registryService.getBucketByName(bucketName); + authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier()); + + final SortedSet repoArtifacts = extensionService.getExtensionRepoArtifacts(bucket, groupId); + linkService.populateFullLinks(repoArtifacts, baseUri); + return repoArtifacts; + } + + @Override + public SortedSet getExtensionRepoVersions(final URI baseUri, final String bucketName, final String groupId, + final String artifactId) { + final Bucket bucket = registryService.getBucketByName(bucketName); + authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier()); + + final SortedSet repoVersions = extensionService.getExtensionRepoVersions(bucket, groupId, artifactId); + linkService.populateFullLinks(repoVersions, baseUri); + return repoVersions; + } + + @Override + public ExtensionRepoVersion getExtensionRepoVersion(final URI baseUri, final String bucketName, final String groupId, + final String artifactId, final String version) { + final Bucket bucket = registryService.getBucketByName(bucketName); + authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier()); + + final BundleVersion bundleVersion = extensionService.getBundleVersion(bucket.getIdentifier(), groupId, artifactId, version); + + final String extensionsUri = generateResourceUri(baseUri, + "extension-repository", + bundleVersion.getBucket().getName(), + bundleVersion.getBundle().getGroupId(), + bundleVersion.getBundle().getArtifactId(), + bundleVersion.getVersionMetadata().getVersion(), + "extensions"); + + final String downloadUri = generateResourceUri(baseUri, + "extension-repository", + bundleVersion.getBucket().getName(), + bundleVersion.getBundle().getGroupId(), + bundleVersion.getBundle().getArtifactId(), + bundleVersion.getVersionMetadata().getVersion(), + "content"); + + final String sha256Uri = generateResourceUri(baseUri, + "extension-repository", + bundleVersion.getBucket().getName(), + bundleVersion.getBundle().getGroupId(), + bundleVersion.getBundle().getArtifactId(), + bundleVersion.getVersionMetadata().getVersion(), + "sha256"); + + final ExtensionRepoVersion repoVersion = new ExtensionRepoVersion(); + repoVersion.setExtensionsLink(Link.fromUri(extensionsUri).rel("extensions").build()); + repoVersion.setDownloadLink(Link.fromUri(downloadUri).rel("content").build()); + repoVersion.setSha256Link(Link.fromUri(sha256Uri).rel("sha256").build()); + repoVersion.setSha256Supplied(bundleVersion.getVersionMetadata().getSha256Supplied()); + return repoVersion; + } + + @Override + public StreamingContent getExtensionRepoVersionContent(final String bucketName, final String groupId, final String artifactId, + final String version) { + final Bucket bucket = registryService.getBucketByName(bucketName); + authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier()); + + final BundleVersion bundleVersion = extensionService.getBundleVersion(bucket.getIdentifier(), groupId, artifactId, version); + final StreamingOutput streamingOutput = (output) -> extensionService.writeBundleVersionContent(bundleVersion, output); + return new StreamingContent(streamingOutput, bundleVersion.getFilename()); + } + + @Override + public String getExtensionRepoVersionSha256(final String bucketName, final String groupId, final String artifactId, + final String version) { + final Bucket bucket = registryService.getBucketByName(bucketName); + authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier()); + + final BundleVersion bundleVersion = extensionService.getBundleVersion(bucket.getIdentifier(), groupId, artifactId, version); + final String sha256Hex = bundleVersion.getVersionMetadata().getSha256(); + return sha256Hex; + } + + @Override + public List getExtensionRepoExtensions(final URI baseUri, final String bucketName, final String groupId, + final String artifactId, final String version) { + final Bucket bucket = registryService.getBucketByName(bucketName); + authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier()); + + final BundleVersion bundleVersion = extensionService.getBundleVersion(bucket.getIdentifier(), groupId, artifactId, version); + final SortedSet extensions = extensionService.getExtensionMetadata(bundleVersion); + + final List extensionRepoExtensions = new ArrayList<>(extensions.size()); + extensions.forEach(e -> extensionRepoExtensions.add(new ExtensionRepoExtensionMetadata(e))); + linkService.populateFullLinks(extensionRepoExtensions, baseUri); + return extensionRepoExtensions; + } + + @Override + public Extension getExtensionRepoExtension(final URI baseUri, final String bucketName, final String groupId, + final String artifactId, final String version, final String extensionName) { + final Bucket bucket = registryService.getBucketByName(bucketName); + authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier()); + + final BundleVersion bundleVersion = extensionService.getBundleVersion(bucket.getIdentifier(), groupId, artifactId, version); + final Extension extension = extensionService.getExtension(bundleVersion, extensionName); + return extension; + } + + @Override + public StreamingOutput getExtensionRepoExtensionDocs(final URI baseUri, final String bucketName, final String groupId, + final String artifactId, final String version, final String extensionName) { + final Bucket bucket = registryService.getBucketByName(bucketName); + authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier()); + + final BundleVersion bundleVersion = extensionService.getBundleVersion(bucket.getIdentifier(), groupId, artifactId, version); + final StreamingOutput streamingOutput = (output) -> extensionService.writeExtensionDocs(bundleVersion, extensionName, output); + return streamingOutput; + } + + @Override + public StreamingOutput getExtensionRepoExtensionAdditionalDocs(final URI baseUri, final String bucketName, final String groupId, + final String artifactId, final String version, final String extensionName) { + final Bucket bucket = registryService.getBucketByName(bucketName); + authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier()); + + final BundleVersion bundleVersion = extensionService.getBundleVersion(bucket.getIdentifier(), groupId, artifactId, version); + final StreamingOutput streamingOutput = (output) -> extensionService.writeAdditionalDetailsDocs(bundleVersion, extensionName, output); + return streamingOutput; + } + + + // ---------------------- Field methods ---------------------------------------------- + + @Override + public Set getBucketFields() { + return registryService.getBucketFields(); + } + + @Override + public Set getBucketItemFields() { + return registryService.getBucketItemFields(); + } + + @Override + public Set getFlowFields() { + return registryService.getFlowFields(); + } + + // ---------------------- User methods ---------------------------------------------- + + @Override + public User createUser(final User user) { + verifyAuthorizerSupportsConfigurableUserGroups(); + authorizeTenantsAccess(RequestAction.WRITE); + validateCreationOfRevisableEntity(user, USER_ENTITY_TYPE); + validateIdentifierNotPresent(user, USER_ENTITY_TYPE); + + user.setIdentifier(UUID.randomUUID().toString()); + return createRevisableEntity(user, USER_ENTITY_TYPE, currentUserIdentity(), () -> authorizationService.createUser(user)); + } + + @Override + public List getUsers() { + verifyAuthorizerIsManaged(); + authorizeTenantsAccess(RequestAction.READ); + return entityService.getEntities(() -> authorizationService.getUsers()); + } + + @Override + public User getUser(final String identifier) { + verifyAuthorizerIsManaged(); + authorizeTenantsAccess(RequestAction.READ); + return entityService.get(() -> authorizationService.getUser(identifier)); + } + + @Override + public User updateUser(final User user) { + verifyAuthorizerSupportsConfigurableUserGroups(); + authorizeTenantsAccess(RequestAction.WRITE); + validateUpdateOfRevisableEntity(user, USER_ENTITY_TYPE); + + // verify outside of the revisable update so ResourceNotFoundException will be thrown instead of InvalidRevisionException + authorizationService.verifyUserExists(user.getIdentifier()); + + return updateRevisableEntity(user, USER_ENTITY_TYPE, currentUserIdentity(), () -> authorizationService.updateUser(user)); + } + + @Override + public User deleteUser(final String identifier, final RevisionInfo revisionInfo) { + verifyAuthorizerSupportsConfigurableUserGroups(); + authorizeTenantsAccess(RequestAction.DELETE); + validateDeleteOfRevisableEntity(identifier, revisionInfo, USER_ENTITY_TYPE); + + // verify outside of the revisable update so ResourceNotFoundException will be thrown instead of InvalidRevisionException + authorizationService.verifyUserExists(identifier); + + return deleteRevisableEntity(identifier, USER_ENTITY_TYPE, revisionInfo, () -> authorizationService.deleteUser(identifier)); + } + + // ---------------------- UserGroup methods ---------------------------------------------- + + @Override + public UserGroup createUserGroup(final UserGroup userGroup) { + verifyAuthorizerSupportsConfigurableUserGroups(); + authorizeTenantsAccess(RequestAction.WRITE); + validateCreationOfRevisableEntity(userGroup, USER_GROUP_ENTITY_TYPE); + validateIdentifierNotPresent(userGroup, USER_GROUP_ENTITY_TYPE); + + userGroup.setIdentifier(UUID.randomUUID().toString()); + return createRevisableEntity(userGroup, USER_GROUP_ENTITY_TYPE, currentUserIdentity(), + () -> authorizationService.createUserGroup(userGroup)); + } + + @Override + public List getUserGroups() { + verifyAuthorizerIsManaged(); + authorizeTenantsAccess(RequestAction.READ); + return entityService.getEntities(() -> authorizationService.getUserGroups()); + } + + @Override + public UserGroup getUserGroup(final String identifier) { + verifyAuthorizerIsManaged(); + authorizeTenantsAccess(RequestAction.READ); + return entityService.get(() -> authorizationService.getUserGroup(identifier)); + } + + @Override + public UserGroup updateUserGroup(final UserGroup userGroup) { + verifyAuthorizerSupportsConfigurableUserGroups(); + authorizeTenantsAccess(RequestAction.WRITE); + validateUpdateOfRevisableEntity(userGroup, USER_GROUP_ENTITY_TYPE); + + // verify outside of the revisable update so ResourceNotFoundException will be thrown instead of InvalidRevisionException + authorizationService.verifyUserGroupExists(userGroup.getIdentifier()); + + return updateRevisableEntity(userGroup, USER_GROUP_ENTITY_TYPE, currentUserIdentity(), + () -> authorizationService.updateUserGroup(userGroup)); + } + + @Override + public UserGroup deleteUserGroup(final String identifier, final RevisionInfo revisionInfo) { + verifyAuthorizerSupportsConfigurableUserGroups(); + authorizeTenantsAccess(RequestAction.DELETE); + validateDeleteOfRevisableEntity(identifier, revisionInfo, USER_GROUP_ENTITY_TYPE); + + // verify outside of the revisable update so ResourceNotFoundException will be thrown instead of InvalidRevisionException + authorizationService.verifyUserGroupExists(identifier); + + return deleteRevisableEntity(identifier, USER_GROUP_ENTITY_TYPE, revisionInfo, + () -> authorizationService.deleteUserGroup(identifier)); + } + + // ---------------------- AccessPolicy methods ---------------------------------------------- + + @Override + public AccessPolicy createAccessPolicy(final AccessPolicy accessPolicy) { + verifyAuthorizerSupportsConfigurablePolicies(); + authorizePoliciesAccess(RequestAction.WRITE); + validateCreationOfRevisableEntity(accessPolicy, ACCESS_POLICY_ENTITY_TYPE); + validateIdentifierNotPresent(accessPolicy, ACCESS_POLICY_ENTITY_TYPE); + + accessPolicy.setIdentifier(UUID.randomUUID().toString()); + return createRevisableEntity(accessPolicy, ACCESS_POLICY_ENTITY_TYPE, currentUserIdentity(), + () -> authorizationService.createAccessPolicy(accessPolicy)); + } + + @Override + public AccessPolicy getAccessPolicy(final String identifier) { + verifyAuthorizerIsManaged(); + authorizePoliciesAccess(RequestAction.READ); + return entityService.get(() -> authorizationService.getAccessPolicy(identifier)); + } + + @Override + public AccessPolicy getAccessPolicy(final String resource, final RequestAction action) { + verifyAuthorizerIsManaged(); + authorizePoliciesAccess(RequestAction.READ); + return entityService.get(() -> authorizationService.getAccessPolicy(resource, action)); + } + + @Override + public List getAccessPolicies() { + verifyAuthorizerIsManaged(); + authorizePoliciesAccess(RequestAction.READ); + return entityService.getEntities(() -> authorizationService.getAccessPolicies()); + } + + @Override + public AccessPolicy updateAccessPolicy(final AccessPolicy accessPolicy) { + verifyAuthorizerSupportsConfigurablePolicies(); + authorizePoliciesAccess(RequestAction.WRITE); + validateUpdateOfRevisableEntity(accessPolicy, ACCESS_POLICY_ENTITY_TYPE); + + // verify outside of the revisable update so ResourceNotFoundException will be thrown instead of InvalidRevisionException + authorizationService.verifyAccessPolicyExists(accessPolicy.getIdentifier()); + + return updateRevisableEntity(accessPolicy, ACCESS_POLICY_ENTITY_TYPE, currentUserIdentity(), + () -> authorizationService.updateAccessPolicy(accessPolicy)); + } + + @Override + public AccessPolicy deleteAccessPolicy(final String identifier, final RevisionInfo revisionInfo) { + verifyAuthorizerSupportsConfigurablePolicies(); + authorizePoliciesAccess(RequestAction.DELETE); + validateDeleteOfRevisableEntity(identifier, revisionInfo, ACCESS_POLICY_ENTITY_TYPE); + + // verify outside of the revisable update so ResourceNotFoundException will be thrown instead of InvalidRevisionException + authorizationService.verifyAccessPolicyExists(identifier); + + return deleteRevisableEntity(identifier, ACCESS_POLICY_ENTITY_TYPE, revisionInfo, + () -> authorizationService.deleteAccessPolicy(identifier)); + } + + @Override + public List getResources() { + authorizePoliciesAccess(RequestAction.READ); + return authorizationService.getResources(); + } + + // ---------------------- Permission methods ----------------------------- + + @Override + public CurrentUser getCurrentUser() { + return authorizationService.getCurrentUser(); + } + + + // ---------------------- Authorization methods ----------------------------- + + private void verifyAuthorizerIsManaged() { + authorizationService.verifyAuthorizerIsManaged(); + } + + private void verifyAuthorizerSupportsConfigurablePolicies() { + authorizationService.verifyAuthorizerSupportsConfigurablePolicies(); + } + + private void verifyAuthorizerSupportsConfigurableUserGroups() { + authorizationService.verifyAuthorizerSupportsConfigurableUserGroups(); + } + + // ---------------------- Configuration methods ----------------------------- + + @Override + public RegistryConfiguration getRegistryConfiguration() { + final RegistryConfiguration config = new RegistryConfiguration(); + + boolean hasAnyConfigurationAccess = false; + AccessDeniedException lastAccessDeniedException = null; + try { + final Authorizable policyAuthorizer = authorizableLookup.getPoliciesAuthorizable(); + authorizationService.authorize(policyAuthorizer, RequestAction.READ); + config.setSupportsManagedAuthorizer(authorizationService.isManagedAuthorizer()); + config.setSupportsConfigurableAuthorizer(authorizationService.isConfigurableAccessPolicyProvider()); + hasAnyConfigurationAccess = true; + } catch (AccessDeniedException e) { + lastAccessDeniedException = e; + } + + try { + authorizationService.authorize(authorizableLookup.getTenantsAuthorizable(), RequestAction.READ); + config.setSupportsConfigurableUsersAndGroups(authorizationService.isConfigurableUserGroupProvider()); + hasAnyConfigurationAccess = true; + } catch (AccessDeniedException e) { + lastAccessDeniedException = e; + } + + if (!hasAnyConfigurationAccess) { + // If the user doesn't have access to any configuration, then throw the exception. + // Otherwise, return what they can access. + throw lastAccessDeniedException; + } + + return config; + } + + // ---------------------- Helper methods ------------------------------------- + + private void authorizeBucketsAccess(RequestAction actionType) throws AccessDeniedException { + final Authorizable bucketsAuthorizable = authorizableLookup.getBucketsAuthorizable(); + authorizationService.authorize(bucketsAuthorizable, actionType); + } + + private void authorizeBucketAccess(final RequestAction actionType, final String bucketIdentifier) { + if (StringUtils.isBlank(bucketIdentifier)) { + throw new IllegalArgumentException("Unable to authorize access because bucket identifier is null or blank"); + } + + final Authorizable bucketAuthorizable = authorizableLookup.getBucketAuthorizable(bucketIdentifier); + authorizationService.authorize(bucketAuthorizable, actionType); + } + + private void authorizeBucketAccess(final RequestAction action, final Bundle bundle) { + // this should never happen, but if somehow the back-end didn't populate the bucket id let's make sure the bundle isn't returned + if (bundle == null) { + throw new IllegalStateException("Unable to authorize access because bucket identifier is null or blank"); + } + + authorizeBucketAccess(action, bundle.getBucketIdentifier()); + } + + private void authorizeBucketAccess(final RequestAction action, final VersionedFlow flow) { + // this should never happen, but if somehow the back-end didn't populate the bucket id let's make sure the flow isn't returned + if (flow == null) { + throw new IllegalStateException("Unable to authorize access because bucket identifier is null or blank"); + } + + authorizeBucketAccess(action, flow.getBucketIdentifier()); + } + + private void authorizeBucketAccess(final RequestAction action, final VersionedFlowSnapshot flowSnapshot) { + // this should never happen, but if somehow the back-end didn't populate the bucket id let's make sure the flow snapshot isn't returned + if (flowSnapshot == null || flowSnapshot.getSnapshotMetadata() == null) { + throw new IllegalStateException("Unable to authorize access because bucket identifier is null or blank"); + } + + authorizeBucketAccess(action, flowSnapshot.getSnapshotMetadata().getBucketIdentifier()); + } + + private void authorizeBucketAccess(final RequestAction action, final VersionedFlowSnapshotMetadata flowSnapshotMetadata) { + // this should never happen, but if somehow the back-end didn't populate the bucket id let's make sure the flow snapshot isn't returned + if (flowSnapshotMetadata == null) { + throw new IllegalStateException("Unable to authorize access because bucket identifier is null or blank"); + } + + authorizeBucketAccess(action, flowSnapshotMetadata.getBucketIdentifier()); + } + + private Set getAuthorizedBucketIds(final RequestAction actionType) { + return authorizationService.getAuthorizedResources(actionType, ResourceType.Bucket) + .stream() + .map(StandardServiceFacade::extractBucketIdFromResource) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toSet()); + } + + private static String extractBucketIdFromResource(Resource resource) { + if (resource == null || resource.getIdentifier() == null || !resource.getIdentifier().startsWith("/buckets/")) { + return null; + } + + final String[] pathComponents = resource.getIdentifier().split("/"); + if (pathComponents.length < 3) { + return null; + } + return pathComponents[2]; + } + + private String generateResourceUri(final URI baseUri, final String... path) { + final URI fullUri = UriBuilder.fromUri(baseUri).segment(path).build(); + return fullUri.toString(); + } + + private void authorizePoliciesAccess(final RequestAction actionType) { + final Authorizable policiesAuthorizable = authorizableLookup.getPoliciesAuthorizable(); + authorizationService.authorize(policiesAuthorizable, actionType); + } + + private void authorizeTenantsAccess(RequestAction actionType) { + final Authorizable tenantsAuthorizable = authorizableLookup.getTenantsAuthorizable(); + authorizationService.authorize(tenantsAuthorizable, actionType); + } + + // ---------------------- Revision Helper Methods ------------------------------------- + + private void validateCreationOfRevisableEntity(final RevisableEntity entity, final String entityTypeName) { + if (entity == null) { + throw new IllegalArgumentException(entityTypeName + " cannot be null"); + } + + // skip checking revision if feature is disabled + if (!revisionFeature.isEnabled()) { + return; + } + + // NOT: restore identifier check here when we no longer needs backwards compatibility + + if (entity.getRevision() == null + || entity.getRevision().getVersion() == null + || entity.getRevision().getVersion().longValue() != 0) { + throw new IllegalArgumentException("A revision of 0 must be specified when creating a new " + entityTypeName + "."); + } + } + + /** + * NOTE: This logic should be moved back to validateCreationOfRevisableEntity once we no longer need to maintain + * backwards compatibility (i.e. on a major release like 1.0.0). + * + * Currently NiFi has been sending an identifier when creating a flow, so we need to continue to allow that. + */ + private void validateIdentifierNotPresent(final RevisableEntity entity, final String entityTypeName) { + if (entity.getIdentifier() != null) { + throw new IllegalArgumentException(entityTypeName + " identifier cannot be specified when creating a new " + + entityTypeName.toLowerCase() + "."); + } + } + + private void validateUpdateOfRevisableEntity(final RevisableEntity entity, final String entityTypeName) { + if (entity == null) { + throw new IllegalArgumentException(entityTypeName + " cannot be null"); + } + + // skip checking revision if feature is disabled + if (!revisionFeature.isEnabled()) { + return; + } + + if (entity.getRevision() == null || entity.getRevision().getVersion() == null) { + throw new IllegalArgumentException("Revision info must be specified."); + } + } + + private void validateDeleteOfRevisableEntity(final String identifier, final RevisionInfo revision, final String entityTypeName) { + if (identifier == null || identifier.trim().isEmpty()) { + throw new IllegalArgumentException(entityTypeName + " identifier is required"); + } + + // skip checking revision if feature is disabled + if (!revisionFeature.isEnabled()) { + return; + } + + if (revision == null || revision.getVersion() == null) { + throw new IllegalArgumentException("Revision info must be specified."); + } + } + + private T createRevisableEntity(final T requestEntity, final String entityTypeName, + final String creatorIdentity, final Supplier createEntity) { + + // skip using the entity service if revision feature is disabled + if (!revisionFeature.isEnabled()) { + final T entity = createEntity.get(); + if (entity.getRevision() == null) { + entity.setRevision(new RevisionInfo(null, 0L)); + } + return entity; + } else { + try { + return entityService.create(requestEntity, creatorIdentity, createEntity); + } catch (InvalidRevisionException e) { + final String msg = String.format(INVALID_REVISION_MSG, entityTypeName, "create", requestEntity.getIdentifier()); + throw new InvalidRevisionException(msg, e); + } + } + } + + private T updateRevisableEntity(final T requestEntity, final String entityTypeName, + final String updaterIdentity, final Supplier updateEntity) { + + // skip using the entity service if revision feature is disabled + if (!revisionFeature.isEnabled()) { + final T entity = updateEntity.get(); + if (entity.getRevision() == null) { + entity.setRevision(new RevisionInfo(null, 0L)); + } + return entity; + } else { + try { + return entityService.update(requestEntity, updaterIdentity, updateEntity); + } catch (InvalidRevisionException e) { + final String msg = String.format(INVALID_REVISION_MSG, entityTypeName, "update", requestEntity.getIdentifier()); + throw new InvalidRevisionException(msg, e); + } + } + } + + private T deleteRevisableEntity(final String entityIdentifier, final String entityTypeName, + final RevisionInfo revisionInfo, final Supplier deleteEntity) { + // skip using the entity service if revision feature is disabled + if (!revisionFeature.isEnabled()) { + final T entity = deleteEntity.get(); + if (entity.getRevision() == null) { + entity.setRevision(new RevisionInfo(null, 0L)); + } + return entity; + } else { + try { + return entityService.delete(entityIdentifier, revisionInfo, deleteEntity); + } catch (InvalidRevisionException e) { + final String msg = String.format(INVALID_REVISION_MSG, entityTypeName, "delete", entityIdentifier); + throw new InvalidRevisionException(msg, e); + } + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StreamingContent.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StreamingContent.java new file mode 100644 index 0000000000..ed4d3e8fff --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/service/StreamingContent.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.service; + +import javax.ws.rs.core.StreamingOutput; + +public class StreamingContent { + + private final StreamingOutput output; + + private final String filename; + + public StreamingContent(final StreamingOutput output, final String filename) { + this.output = output; + this.filename = filename; + } + + public StreamingOutput getOutput() { + return output; + } + + public String getFilename() { + return filename; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/META-INF/LICENSE b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/META-INF/LICENSE new file mode 100644 index 0000000000..49e3a0c792 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/META-INF/LICENSE @@ -0,0 +1,352 @@ + + Apache License + Version 2.0, January 2004 + https://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: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) 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 + + (d) 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 + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + +This product bundles 'asm' which is available under a 3-Clause BSD style license. +For details see https://asm.ow2.org/asmdex-license.html + + Copyright (c) 2012 France Télécom + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + THE POSSIBILITY OF SUCH DAMAGE. + +The binary distribution of this product bundles 'Antlr 3' which is available +under a "3-clause BSD" license. For details see https://www.antlr3.org/license.html + + Copyright (c) 2010 Terence Parr + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + Neither the name of the author nor the names of its contributors may be used + to endorse or promote products derived from this software without specific + prior written permission. + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + THE POSSIBILITY OF SUCH DAMAGE. + +The binary distribution of this product bundles 'Bouncy Castle JDK 1.5' +under an MIT style license. + + Copyright (c) 2000 - 2015 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org) + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +The binary distribution of this product bundles 'Slf4j' which is available under +an MIT license. + + Copyright (c) 2004-2013 QOS.ch + All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +The binary distribution of this product bundles 'dom4j' which is available under +a "3-Clause BSD" license. For details: https://github.com/dom4j/dom4j/blob/master/LICENSE + + Copyright 2001-2016 (C) MetaStuff, Ltd. and DOM4J contributors. All Rights Reserved. + + Redistribution and use of this software and associated documentation + ("Software"), with or without modification, are permitted provided + that the following conditions are met: + + 1. Redistributions of source code must retain copyright + statements and notices. Redistributions must also contain a + copy of this document. + + 2. Redistributions in binary form must reproduce the + above copyright notice, this list of conditions and the + following disclaimer in the documentation and/or other + materials provided with the distribution. + + 3. The name "DOM4J" must not be used to endorse or promote + products derived from this Software without prior written + permission of MetaStuff, Ltd. For written permission, + please contact dom4j-info@metastuff.com. + + 4. Products derived from this Software may not be called "DOM4J" + nor may "DOM4J" appear in their names without prior written + permission of MetaStuff, Ltd. DOM4J is a registered + trademark of MetaStuff, Ltd. + + 5. Due credit should be given to the DOM4J Project - https://dom4j.github.io/ + + THIS SOFTWARE IS PROVIDED BY METASTUFF, LTD. AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT + NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + METASTUFF, LTD. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/META-INF/NOTICE b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/META-INF/NOTICE new file mode 100644 index 0000000000..6abceec531 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/META-INF/NOTICE @@ -0,0 +1,184 @@ +nifi-registry-web-api +Copyright 2014-2017 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (https://www.apache.org/). + +=========================================== +Apache Software License v2 +=========================================== + +The following binary components are provided under the Apache Software License v2 + + (ASLv2) Apache Commons Lang + The following NOTICE information applies: + Apache Commons Lang + Copyright 2001-2017 The Apache Software Foundation + + This product includes software from the Spring Framework, + under the Apache License 2.0 (see: StringUtils.containsWhitespace()) + + (ASLv2) Jackson JSON processor + The following NOTICE information applies: + # Jackson JSON processor + + Jackson is a high-performance, Free/Open Source JSON processing library. + It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has + been in development since 2007. + It is currently developed by a community of developers, as well as supported + commercially by FasterXML.com. + + ## Licensing + + Jackson core and extension components may licensed under different licenses. + To find the details that apply to this artifact see the accompanying LICENSE file. + For more information, including possible other licensing options, contact + FasterXML.com (https://fasterxml.com). + + ## Credits + + A list of contributors may be found from CREDITS file, which is included + in some artifacts (usually source distributions); but is always available + from the source code management (SCM) system project uses. + + (ASLv2) Classmate + The following NOTICE information applies + Java ClassMate library was originally written by Tatu Saloranta (tatu.saloranta@iki.fi) + + Other developers who have contributed code are: + + * Brian Langel + + (ASLv2) Apache Commons IO + The following NOTICE information applies: + Apache Commons IO + Copyright 2002-2016 The Apache Software Foundation + + (ASLv2) Apache log4j + The following NOTICE information applies: + Apache log4j + Copyright 2010 The Apache Software Foundation + + (ASLv2) Spring Framework + The following NOTICE information applies: + Spring Framework 5.0.2.RELEASE + Copyright (c) 2002-2017 Pivotal, Inc. + + (ASLv2) Spring Security + The following NOTICE information applies: + Spring Framework 5.0.5.RELEASE + Copyright (c) 2002-2017 Pivotal, Inc. + + This product includes software developed by Spring Security + Project (https://www.springframework.org/security). + + (ASLv2) Spring LDAP + The following NOTICE information applies: + Spring LDAP 2.3.2.RELEASE + Copyright (c) 2002-2017 Pivotal, Inc. + + This product includes software developed by the Spring LDAP + Project (https://www.springframework.org/ldap). + + (ASLv2) Apache Tomcat Embed EL + The following NOTICE information applies: + Apache Tomcat + Copyright 1999-2017 The Apache Software Foundation + + This product includes software developed at + The Apache Software Foundation (https://www.apache.org/). + + This software contains code derived from netty-native + developed by the Netty project + (https://netty.io, https://github.com/netty/netty-tcnative/) + and from finagle-native developed at Twitter + (https://github.com/twitter/finagle). + + The Windows Installer is built with the Nullsoft + Scriptable Install System (NSIS), which is + open source software. The original software and + related information is available at + https://nsis.sourceforge.net. + + Java compilation software for JSP pages is provided by the Eclipse + JDT Core Batch Compiler component, which is open source software. + The original software and related information is available at + https://www.eclipse.org/jdt/core/. + + For portions of the Tomcat JNI OpenSSL API and the OpenSSL JSSE integration + The org.apache.tomcat.jni and the org.apache.tomcat.net.openssl packages + are derivative work originating from the Netty project and the finagle-native + project developed at Twitter + * Copyright 2014 The Netty Project + * Copyright 2014 Twitter + + The original XML Schemas for Java EE Deployment Descriptors: + - javaee_5.xsd + - javaee_web_services_1_2.xsd + - javaee_web_services_client_1_2.xsd + - javaee_6.xsd + - javaee_web_services_1_3.xsd + - javaee_web_services_client_1_3.xsd + - jsp_2_2.xsd + - web-app_3_0.xsd + - web-common_3_0.xsd + - web-fragment_3_0.xsd + - javaee_7.xsd + - javaee_web_services_1_4.xsd + - javaee_web_services_client_1_4.xsd + - jsp_2_3.xsd + - web-app_3_1.xsd + - web-common_3_1.xsd + - web-fragment_3_1.xsd + - javaee_8.xsd + - web-app_4_0.xsd + - web-common_4_0.xsd + - web-fragment_4_0.xsd + + may be obtained from: + https://www.oracle.com/webfolder/technetwork/jsc/xml/ns/javaee/index.html + +************************ +Common Development and Distribution License 1.1 +************************ + +The following binary components are provided under the Common Development and Distribution License 1.1. See project link for details. + + (CDDL 1.1) (GPL2 w/ CPE) javax.annotation API (javax.annotation:javax.annotation-api:jar:1.2 - https://jcp.org/en/jsr/detail?id=250) + (CDDL 1.1) (GPL2 w/ CPE) aopalliance-repackaged (org.glassfish.hk2.external:aopalliance-repackaged:jar:2.5.0-b42 - https://javaee.github.io/glassfish/) + (CDDL 1.1) (GPL2 w/ CPE) asm-all-repackaged (org.glassfish.hk2.external:asm-all-repackaged:jar:2.5.0-b42 - https://javaee.github.io/glassfish/) + (CDDL 1.1) (GPL2 w/ CPE) class-model (org.glassfish.hk2:class-model:jar:2.5.0-b42 - https://javaee.github.io/glassfish/) + (CDDL 1.1) (GPL2 w/ CPE) config-types (org.glassfish.hk2:config-types:jar:2.5.0-b42 - https://javaee.github.io/glassfish/) + (CDDL 1.1) (GPL2 w/ CPE) hk2 (org.glassfish.hk2:hk2:jar:2.5.0-b42 - https://javaee.github.io/glassfish/) + (CDDL 1.1) (GPL2 w/ CPE) hk2-api (org.glassfish.hk2:hk2-api:jar:2.5.0-b42 - https://javaee.github.io/glassfish/) + (CDDL 1.1) (GPL2 w/ CPE) hk2-utils (org.glassfish.hk2:hk2-utils:jar:2.5.0-b42 - https://javaee.github.io/glassfish/) + (CDDL 1.1) (GPL2 w/ CPE) hk2-locator (org.glassfish.hk2:hk2-locator:jar:2.5.0-b42 - https://javaee.github.io/glassfish/) + (CDDL 1.1) (GPL2 w/ CPE) hk2-config (org.glassfish.hk2:hk2-config:jar:2.5.0-b42 - https://javaee.github.io/glassfish/) + (CDDL 1.1) (GPL2 w/ CPE) hk2-core (org.glassfish.hk2:hk2-core:jar:2.5.0-b42 - https://javaee.github.io/glassfish/) + (CDDL 1.1) (GPL2 w/ CPE) hk2-runlevel (org.glassfish.hk2:hk2-runlevel:jar:2.5.0-b42 - https://javaee.github.io/glassfish/) + (CDDL 1.1) (GPL2 w/ CPE) spring-bridge (org.glassfish.hk2:spring-bridge:jar:2.5.0-b42 - https://javaee.github.io/glassfish/) + (CDDL 1.1) (GPL2 w/ CPE) javax.inject:1 as OSGi bundle (org.glassfish.hk2.external:javax.inject:jar:2.4.0-b25 - https://hk2.java.net/external/javax.inject) + (CDDL 1.1) (GPL2 w/ CPE) javax.ws.rs-api (javax.ws.rs:javax.ws.rs-api:jar:2.1 - https://jax-rs-spec.java.net) + (CDDL 1.1) (GPL2 w/ CPE) javax.el (org.glassfish:javax.el:jar:3.0.1-b08 - https://github.com/javaee/el-spec) + (CDDL 1.1) (GPL2 w/ CPE) jersey-bean-validation (org.glassfish.jersey.ext:jersey-bean-validation:jar:2.29.1 - https://jersey.github.io/) + (CDDL 1.1) (GPL2 w/ CPE) jersey-client (org.glassfish.jersey.core:jersey-client:jar:2.27 - https://jersey.github.io/) + (CDDL 1.1) (GPL2 w/ CPE) jersey-common (org.glassfish.jersey.core:jersey-common:jar:2.29.1 - https://jersey.github.io/) + (CDDL 1.1) (GPL2 w/ CPE) jersey-container-servlet-core (org.glassfish.jersey.containers:jersey-container-servlet-core:jar:2.29.1 - https://jersey.github.io/) + (CDDL 1.1) (GPL2 w/ CPE) jersey-entity-filtering (org.glassfish.jersey.ext:jersey-entity-filtering:jar:2.29.1 - https://jersey.github.io/) + (CDDL 1.1) (GPL2 w/ CPE) jersey-hk2 (org.glassfish.jersey.inject:jersey-hk2:jar:2.27 - https://jersey.github.io/) + (CDDL 1.1) (GPL2 w/ CPE) jersey-media-jaxb (org.glassfish.jersey.media:jersey-media-jaxb:jar:2.29.1 - https://jersey.github.io/) + (CDDL 1.1) (GPL2 w/ CPE) jersey-media-json-jackson (org.glassfish.jersey.media:jersey-media-json-jackson:jar:2.29.1 - https://jersey.github.io/) + (CDDL 1.1) (GPL2 w/ CPE) jersey-server (org.glassfish.jersey.core:jersey-server:jar:2.29.1 - https://jersey.github.io/) + (CDDL 1.1) (GPL2 w/ CPE) jersey-spring4 (org.glassfish.jersey.ext:jersey-spring4:jar:2.27 - https://jersey.github.io/) + (CDDL 1.1) (GPL2 w/ CPE) OSGi resource locator bundle (org.glassfish.hk2:osgi-resource-locator:jar:1.0.1 - https://glassfish.org/osgi-resource-locator) + +************************ +Eclipse Public License 1.0 +************************ + +The following binary components are provided under the Eclipse Public License 1.0. See project link for details. + + (EPL 1.0)(MPL 2.0) H2 Database (com.h2database:h2:jar:1.3.176 - https://www.h2database.com/html/license.html) + (EPL 1.0)(LGPL 2.1) Logback Classic (ch.qos.logback:logback-classic:jar:1.2.3 - https://logback.qos.ch/) + (EPL 1.0)(LGPL 2.1) Logback Core (ch.qos.logback:logback-core:jar:1.2.3 - https://logback.qos.ch/) + (EPL 1.0) AspectJ Weaver (org.aspectj:aspectjweaver:jar:1.8.13 - https://www.eclipse.org/aspectj/) diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authentication.IdentityProvider b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authentication.IdentityProvider new file mode 100644 index 0000000000..ea80a03bd2 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authentication.IdentityProvider @@ -0,0 +1,15 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +org.apache.nifi.registry.web.security.authentication.kerberos.KerberosIdentityProvider \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/banner.txt b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/banner.txt new file mode 100644 index 0000000000..6fb0ffc38e --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/banner.txt @@ -0,0 +1,8 @@ + + Apache NiFi _ _ + _ __ ___ __ _(_)___| |_ _ __ _ _ +| '__/ _ \/ _` | / __| __| '__| | | | +| | | __/ (_| | \__ \ |_| | | |_| | +|_| \___|\__, |_|___/\__|_| \__, | +==========|___/================|___/= + v${application.version} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/images/bgNifiLogo.png b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/images/bgNifiLogo.png new file mode 100644 index 0000000000..1e12feec30 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/images/bgNifiLogo.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/images/nifi16.ico b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/images/nifi16.ico new file mode 100644 index 0000000000..12da8104a3 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/images/nifi16.ico differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/swagger/security-definitions.json b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/swagger/security-definitions.json new file mode 100644 index 0000000000..411fb3bb83 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/swagger/security-definitions.json @@ -0,0 +1,12 @@ +{ + "BasicAuth": { + "type": "basic", + "description": "HTTP Basic Auth" + }, + "Authorization": { + "type": "apiKey", + "name": "Authorization", + "in": "header", + "description": "NiFi Registry Auth Token (JWT)" + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/endpoint.hbs b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/endpoint.hbs new file mode 100644 index 0000000000..1394136c92 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/endpoint.hbs @@ -0,0 +1,61 @@ +{{!-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You 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. +--}} +
+ + {{#post}} +
+
+
POST
+
+
{{summary}}
+
+
+ {{> operation}} +
+ {{/post}} + {{#get}} +
+
+
GET
+
+
{{summary}}
+
+
+ {{> operation}} +
+ {{/get}} + {{#put}} +
+
+
PUT
+
+
{{summary}}
+
+
+ {{> operation}} +
+ {{/put}} + {{#delete}} +
+
+
DELETE
+
+
{{summary}}
+
+
+ {{> operation}} +
+ {{/delete}} +
\ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/example.hbs b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/example.hbs new file mode 100644 index 0000000000..26a4283ec7 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/example.hbs @@ -0,0 +1,18 @@ +{{!-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You 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. +--}}{{!-- formatting here matters... in whitespace: pre. this is not comprehensive but sufficent for our examples --}} +{{#each properties}} {{#ifeq type "string"}}"{{@key}}": "value"{{/ifeq}}{{#ifeq type "boolean"}}"{{@key}}": true{{/ifeq}}{{#ifeq type "integer"}}"{{@key}}": 0{{/ifeq}}{{#ifeq type "number"}}"{{@key}}": 0.0{{/ifeq}}{{#if $ref}}"{{@key}}": {{/if}}{{#ifeq type "array"}}"{{@key}}": [{{#if items.$ref}}{{else}}"value"{{/if}}]{{/ifeq}}{{#ifeq type "object"}}"{{@key}}": { + "name": {{#if additionalProperties.$ref}}{{else}}{{#ifeq additionalProperties.type "integer"}}0{{else}}"value"{{/ifeq}}{{/if}} + }{{/ifeq}}, +{{/each}} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/index.html.hbs b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/index.html.hbs new file mode 100644 index 0000000000..aca97fed1c --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/index.html.hbs @@ -0,0 +1,550 @@ + + + + + {{info.title}}-{{info.version}} + + + + + + + + +
+ +
{{basePath}}
+
{{info.title}} {{info.version}}
+
+
+
+
{{info.description}}
+
+
+ +
Bucket endpoints
+
+
+ +
+
+
+ +
Item endpoints
+
+
+ +
+
+
+ +
Flow endpoints
+
+
+ +
+
+
+ +
Bundle endpoints
+
+
+ +
+
+
+ +
Extension endpoints
+
+
+ +
+
+
+ +
Extension Repository endpoints
+
+
+ +
+
+
+ +
Tenant endpoints
+
+
+ +
+
+
+ +
Policy endpoints
+
+
+ +
+
+
+ +
Access endpoints
+
+
+ +
+ + + {{#each definitions}} + {{> type}} + {{/each}} + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/operation.hbs b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/operation.hbs new file mode 100644 index 0000000000..64bd582fa6 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/operation.hbs @@ -0,0 +1,110 @@ +{{!-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You 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. +--}} + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/type.hbs b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/type.hbs new file mode 100644 index 0000000000..f6f117b586 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/type.hbs @@ -0,0 +1,57 @@ +{{!-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You 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. +--}} + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/groovy/org/apache/nifi/registry/security/authorization/ResourceAuthorizationFilterSpec.groovy b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/groovy/org/apache/nifi/registry/security/authorization/ResourceAuthorizationFilterSpec.groovy new file mode 100644 index 0000000000..806f73c03a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/groovy/org/apache/nifi/registry/security/authorization/ResourceAuthorizationFilterSpec.groovy @@ -0,0 +1,172 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.security.authorization + +import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException +import org.apache.nifi.registry.security.authorization.resource.Authorizable +import org.apache.nifi.registry.security.authorization.resource.ResourceType +import org.apache.nifi.registry.service.AuthorizationService +import org.apache.nifi.registry.service.RegistryService +import org.apache.nifi.registry.web.security.authorization.HttpMethodAuthorizationRules +import org.apache.nifi.registry.web.security.authorization.ResourceAuthorizationFilter +import org.apache.nifi.registry.web.security.authorization.StandardHttpMethodAuthorizationRules +import org.springframework.http.HttpMethod +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse +import spock.lang.Specification + +import javax.servlet.FilterChain +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +class ResourceAuthorizationFilterSpec extends Specification { + + RegistryService registryService = Mock(RegistryService) + AuthorizableLookup authorizableLookup = new StandardAuthorizableLookup(registryService) + AuthorizationService mockAuthorizationService = Mock(AuthorizationService) + FilterChain mockFilterChain = Mock(FilterChain) + ResourceAuthorizationFilter.Builder resourceAuthorizationFilterBuilder + + // runs before every feature method + def setup() { + mockAuthorizationService.getAuthorizableLookup() >> authorizableLookup + resourceAuthorizationFilterBuilder = ResourceAuthorizationFilter.builder().setAuthorizationService(mockAuthorizationService) + } + + // runs after every feature method + def cleanup() { + //mockAuthorizationService = null + //mockFilterChain = null + resourceAuthorizationFilterBuilder = null + } + + // runs before the first feature method + def setupSpec() {} + + // runs after the last feature method + def cleanupSpec() {} + + + def "unsecured requests are allowed without an authorization check"() { + + setup: + def resourceAuthorizationFilter = resourceAuthorizationFilterBuilder.addResourceType(ResourceType.Actuator).build() + def httpServletRequest = createUnsecuredRequest() + def httpServletResponse = createResponse() + + when: "doFilter() is called" + resourceAuthorizationFilter.doFilter(httpServletRequest, httpServletResponse, mockFilterChain) + + then: "response is forwarded without authorization check" + 0 * mockAuthorizationService._ + 1 * mockFilterChain.doFilter(_ as HttpServletRequest, _ as HttpServletResponse) + + } + + + def "secure requests to an unguarded resource are allowed without an authorization check"() { + + setup: + def resourceAuthorizationFilter = resourceAuthorizationFilterBuilder.addResourceType(ResourceType.Actuator).build() + def httpServletRequest = createSecureRequest(HttpMethod.POST, ResourceType.Bucket) + def httpServletResponse = createResponse() + + when: "doFilter() is called" + resourceAuthorizationFilter.doFilter(httpServletRequest, httpServletResponse, mockFilterChain) + + then: "response is forwarded without authorization check" + 0 * mockAuthorizationService._ + 1 * mockFilterChain.doFilter(_ as HttpServletRequest, _ as HttpServletResponse) + + } + + + def "secure requests to an unguarded HTTP method are allowed without an authorization check"() { + + setup: + HttpMethodAuthorizationRules rules = new StandardHttpMethodAuthorizationRules(EnumSet.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE)) + def resourceAuthorizationFilter = resourceAuthorizationFilterBuilder.addResourceType(ResourceType.Actuator, rules).build() + def httpServletRequest = createSecureRequest(HttpMethod.GET, ResourceType.Actuator) + def httpServletResponse = createResponse() + + when: "doFilter() is called" + resourceAuthorizationFilter.doFilter(httpServletRequest, httpServletResponse, mockFilterChain) + + then: "response is forwarded without authorization check" + 0 * mockAuthorizationService._ + 1 * mockFilterChain.doFilter(_ as HttpServletRequest, _ as HttpServletResponse) + + } + + + def "secure requests matching resource configuration rules perform authorization check"() { + + setup: + // Stubbing setup for mockAuthorizationService is done in the then block as we are also verifying interactions with mock + def resourceAuthorizationFilter = resourceAuthorizationFilterBuilder.addResourceType(ResourceType.Actuator).build() + def authorizedRequest = createSecureRequest(HttpMethod.GET, ResourceType.Actuator) + def unauthorizedRequest = createSecureRequest(HttpMethod.POST, ResourceType.Actuator) + def httpServletResponse = createResponse() + + + when: "doFilter() is called with an authorized request" + resourceAuthorizationFilter.doFilter(authorizedRequest, httpServletResponse, mockFilterChain) + + then: "response is forwarded after authorization check" + 1 * mockAuthorizationService.authorize(_ as Authorizable, RequestAction.READ) >> { allowAccess() } + 1 * mockFilterChain.doFilter(_ as HttpServletRequest, _ as HttpServletResponse) + + + when: "doFilter() is called with an unauthorized request" + resourceAuthorizationFilter.doFilter(unauthorizedRequest, httpServletResponse, mockFilterChain) + + then: "authorization check is performed and response is not forwarded" + 1 * mockAuthorizationService.authorize(_ as Authorizable, RequestAction.WRITE) >> { denyAccess() } + 0 * mockFilterChain.doFilter(*_) + + } + + static private HttpServletRequest createUnsecuredRequest() { + HttpServletRequest req = new MockHttpServletRequest() + req.setScheme("http") + req.setSecure(false) + return req + } + + static private HttpServletRequest createSecureRequest(HttpMethod httpMethod, ResourceType resourceType) { + HttpServletRequest req = new MockHttpServletRequest() + req.setMethod(httpMethod.name()) + req.setScheme("https") + req.setServletPath(resourceType.getValue()) + req.setSecure(true) + return req + } + + static private HttpServletResponse createResponse() { + HttpServletResponse res = new MockHttpServletResponse() + return res + } + + static private void allowAccess() { + // Do nothing (no thrown exception indicates access is allowed + } + + static private void denyAccess() { + throw new AccessDeniedException("This is an expected AccessDeniedException.") + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/groovy/org/apache/nifi/registry/web/security/authentication/oidc/StandardOidcIdentityProviderGroovyTest.groovy b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/groovy/org/apache/nifi/registry/web/security/authentication/oidc/StandardOidcIdentityProviderGroovyTest.groovy new file mode 100644 index 0000000000..8381bdac57 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/groovy/org/apache/nifi/registry/web/security/authentication/oidc/StandardOidcIdentityProviderGroovyTest.groovy @@ -0,0 +1,580 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.security.authentication.oidc + +import com.nimbusds.jwt.JWT +import com.nimbusds.jwt.JWTClaimsSet +import com.nimbusds.jwt.PlainJWT +import com.nimbusds.oauth2.sdk.AuthorizationCode +import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant +import com.nimbusds.oauth2.sdk.auth.ClientAuthentication +import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod +import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic +import com.nimbusds.oauth2.sdk.auth.ClientSecretPost +import com.nimbusds.oauth2.sdk.auth.Secret +import com.nimbusds.oauth2.sdk.http.HTTPRequest +import com.nimbusds.oauth2.sdk.http.HTTPResponse +import com.nimbusds.oauth2.sdk.id.ClientID +import com.nimbusds.oauth2.sdk.id.Issuer +import com.nimbusds.oauth2.sdk.token.AccessToken +import com.nimbusds.oauth2.sdk.token.BearerAccessToken +import com.nimbusds.oauth2.sdk.token.RefreshToken +import com.nimbusds.openid.connect.sdk.Nonce +import com.nimbusds.openid.connect.sdk.OIDCTokenResponse +import com.nimbusds.openid.connect.sdk.SubjectType +import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet +import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata +import com.nimbusds.openid.connect.sdk.token.OIDCTokens +import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator +import groovy.json.JsonOutput +import groovy.json.JsonSlurper +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.SignatureAlgorithm +import org.apache.commons.lang3.StringUtils +import org.apache.nifi.registry.properties.NiFiRegistryProperties +import org.apache.nifi.registry.security.key.Key +import org.apache.nifi.registry.security.key.KeyService +import org.apache.nifi.registry.web.security.authentication.jwt.JwtService +import org.junit.After +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +@RunWith(JUnit4.class) +class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase { + private static final Logger logger = LoggerFactory.getLogger(StandardOidcIdentityProviderGroovyTest.class) + + private static final Key SIGNING_KEY = new Key(id: 1, identity: "signingKey", key: "mock-signing-key-value") + + /* + Unlike NiFiProperties, NiFiRegistryProperties extends java.util.Properties, which ultimately implements java.util.Map<>, so map coercion cannot be used here. Setting the raw properties does allow for the same outcomes. + */ + private static final Map DEFAULT_NIFI_PROPERTIES = [ + (NiFiRegistryProperties.SECURITY_USER_OIDC_DISCOVERY_URL) : "https://localhost/oidc", + (NiFiRegistryProperties.SECURITY_IDENTITY_PROVIDER) : "", // Makes isLoginIdentityProviderEnabled => false + (NiFiRegistryProperties.SECURITY_USER_OIDC_CONNECT_TIMEOUT) : "1000", + (NiFiRegistryProperties.SECURITY_USER_OIDC_READ_TIMEOUT) : "1000", + (NiFiRegistryProperties.SECURITY_USER_OIDC_CLIENT_ID) : "expected_client_id", + (NiFiRegistryProperties.SECURITY_USER_OIDC_CLIENT_SECRET) : "expected_client_secret", + (NiFiRegistryProperties.SECURITY_USER_OIDC_CLAIM_IDENTIFYING_USER): "username" + ] + + // Mock collaborators + private static NiFiRegistryProperties mockNiFiRegistryProperties + private static JwtService mockJwtService = [:] as JwtService + + @BeforeClass + static void setUpOnce() throws Exception { + logger.metaClass.methodMissing = { String name, args -> + logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}") + } + } + + @Before + void setUp() throws Exception { + mockNiFiRegistryProperties = buildNiFiRegistryProperties() + } + + @After + void teardown() throws Exception { + } + + private static NiFiRegistryProperties buildNiFiRegistryProperties(Map props = [:]) { + Map combinedProps = DEFAULT_NIFI_PROPERTIES + props + new NiFiRegistryProperties(combinedProps) + } + + private static JwtService buildJwtService() { + def mockJS = new JwtService([:] as KeyService) { + @Override + String generateSignedToken(String identity, String preferredUsername, String issuer, String audience, long expirationMillis) { + signNiFiToken(identity, preferredUsername, issuer, audience, expirationMillis) + } + } + mockJS + } + + private static String signNiFiToken(String identity, String preferredUsername, String issuer, String audience, long expirationMillis) { + String USERNAME_CLAIM = "username" + String KEY_ID_CLAIM = "keyId" + Calendar expiration = Calendar.getInstance() + expiration.setTimeInMillis(System.currentTimeMillis() + 10_000) + String username = identity + + return Jwts.builder().setSubject(identity) + .setIssuer(issuer) + .setAudience(audience) + .claim(USERNAME_CLAIM, username) + .claim(KEY_ID_CLAIM, SIGNING_KEY.getId()) + .setExpiration(expiration.getTime()) + .setIssuedAt(Calendar.getInstance().getTime()) + .signWith(SignatureAlgorithm.HS256, SIGNING_KEY.key.getBytes("UTF-8")).compact() + } + + @Test + void testShouldGetAvailableClaims() { + // Arrange + final Map EXPECTED_CLAIMS = [ + "iss" : "https://accounts.google.com", + "azp" : "1013352044499-05pb1ssdfuihsdfsdsdfdi8r2vike88m.apps.googleusercontent.com", + "aud" : "1013352044499-05pb1ssdfuihsdfsdsdfdi8r2vike88m.apps.googleusercontent.com", + "sub" : "10703475345439756345540", + "email" : "person@nifi.apache.org", + "email_verified": "true", + "at_hash" : "JOGISUDHFiyGHDSFwV5Fah2A", + "iat" : "1590022674", + "exp" : "1590026274", + "empty_claim" : "" + ] + + final List POPULATED_CLAIM_NAMES = EXPECTED_CLAIMS.findAll { k, v -> StringUtils.isNotBlank(v) }.keySet().sort() + + JWTClaimsSet mockJWTClaimsSet = new JWTClaimsSet(EXPECTED_CLAIMS) + + // Act + def definedClaims = StandardOidcIdentityProvider.getAvailableClaims(mockJWTClaimsSet) + logger.info("Defined claims: ${definedClaims}") + + // Assert + assert definedClaims == POPULATED_CLAIM_NAMES + } + + @Test + void testShouldCreateClientAuthenticationFromPost() { + // Arrange + StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockJwtService, mockNiFiRegistryProperties) + + Issuer mockIssuer = new Issuer("https://localhost/oidc") + URI mockURI = new URI("https://localhost/oidc") + OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer, [SubjectType.PUBLIC], mockURI) + + soip.oidcProviderMetadata = metadata + + // Set Authorization Method + soip.oidcProviderMetadata["tokenEndpointAuthMethods"] = [ClientAuthenticationMethod.CLIENT_SECRET_POST] + final List mockAuthMethod = soip.oidcProviderMetadata["tokenEndpointAuthMethods"] + logger.info("Provided Auth Method: ${mockAuthMethod}") + + // Define expected values + final ClientID CLIENT_ID = new ClientID("expected_client_id") + final Secret CLIENT_SECRET = new Secret("expected_client_secret") + + // Inject into OIP + soip.clientId = CLIENT_ID + soip.clientSecret = CLIENT_SECRET + + final ClientAuthentication EXPECTED_CLIENT_AUTHENTICATION = new ClientSecretPost(CLIENT_ID, CLIENT_SECRET) + + // Act + def clientAuthentication = soip.createClientAuthentication() + logger.info("Client Auth properties: ${clientAuthentication.getProperties()}") + + // Assert + assert clientAuthentication.getClientID() == EXPECTED_CLIENT_AUTHENTICATION.getClientID() + logger.info("Client secret: ${(clientAuthentication as ClientSecretPost).clientSecret.value}") + assert ((ClientSecretPost) clientAuthentication).getClientSecret() == ((ClientSecretPost) EXPECTED_CLIENT_AUTHENTICATION).getClientSecret() + } + + @Test + void testShouldCreateClientAuthenticationFromBasic() { + // Arrange + // Mock collaborators + StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockJwtService, mockNiFiRegistryProperties) + + Issuer mockIssuer = new Issuer("https://localhost/oidc") + URI mockURI = new URI("https://localhost/oidc") + OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer, [SubjectType.PUBLIC], mockURI) + soip.oidcProviderMetadata = metadata + + // Set Auth Method + soip.oidcProviderMetadata["tokenEndpointAuthMethods"] = [ClientAuthenticationMethod.CLIENT_SECRET_BASIC] + final List mockAuthMethod = soip.oidcProviderMetadata["tokenEndpointAuthMethods"] + logger.info("Provided Auth Method: ${mockAuthMethod}") + + // Define expected values + final ClientID CLIENT_ID = new ClientID("expected_client_id") + final Secret CLIENT_SECRET = new Secret("expected_client_secret") + + // Inject into OIP + soip.clientId = CLIENT_ID + soip.clientSecret = CLIENT_SECRET + + final ClientAuthentication EXPECTED_CLIENT_AUTHENTICATION = new ClientSecretBasic(CLIENT_ID, CLIENT_SECRET) + + // Act + def clientAuthentication = soip.createClientAuthentication() + logger.info("Client authentication properties: ${clientAuthentication.properties}") + + // Assert + assert clientAuthentication.getClientID() == EXPECTED_CLIENT_AUTHENTICATION.getClientID() + assert clientAuthentication.getMethod() == EXPECTED_CLIENT_AUTHENTICATION.getMethod() + logger.info("Client secret: ${(clientAuthentication as ClientSecretBasic).clientSecret.value}") + assert (clientAuthentication as ClientSecretBasic).getClientSecret() == EXPECTED_CLIENT_AUTHENTICATION.clientSecret + } + + @Test + void testShouldCreateTokenHTTPRequest() { + // Arrange + StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockJwtService, mockNiFiRegistryProperties) + + // Mock AuthorizationGrant + Issuer mockIssuer = new Issuer("https://localhost/oidc") + URI mockURI = new URI("https://localhost/oidc") + AuthorizationCode mockCode = new AuthorizationCode("ABCDE") + def mockAuthGrant = new AuthorizationCodeGrant(mockCode, mockURI) + + OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer, [SubjectType.PUBLIC], mockURI) + soip.oidcProviderMetadata = metadata + + // Set OIDC Provider metadata attributes + final ClientID CLIENT_ID = new ClientID("expected_client_id") + final Secret CLIENT_SECRET = new Secret("expected_client_secret") + + // Inject into OIP + soip.clientId = CLIENT_ID + soip.clientSecret = CLIENT_SECRET + soip.oidcProviderMetadata["tokenEndpointAuthMethods"] = [ClientAuthenticationMethod.CLIENT_SECRET_BASIC] + soip.oidcProviderMetadata["tokenEndpointURI"] = new URI("https://localhost/token") + + // Mock ClientAuthentication + def clientAuthentication = soip.createClientAuthentication() + + // Act + def httpRequest = soip.createTokenHTTPRequest(mockAuthGrant, clientAuthentication) + logger.info("HTTP Request: ${httpRequest.dump()}") + logger.info("Query: ${URLDecoder.decode(httpRequest.query, "UTF-8")}") + + // Assert + assert httpRequest.getMethod().name() == "POST" + assert httpRequest.query =~ "code=${mockCode.value}" + String encodedUri = URLEncoder.encode("https://localhost/oidc", "UTF-8") + assert httpRequest.query =~ "redirect_uri=${encodedUri}&grant_type=authorization_code" + } + + @Test + void testShouldLookupIdentityInUserInfo() { + // Arrange + StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockJwtService, mockNiFiRegistryProperties) + + Issuer mockIssuer = new Issuer("https://localhost/oidc") + URI mockURI = new URI("https://localhost/oidc") + + OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer, [SubjectType.PUBLIC], mockURI) + soip.oidcProviderMetadata = metadata + + final String EXPECTED_IDENTITY = "my_username" + + def responseBody = [username: EXPECTED_IDENTITY, sub: "testSub"] + HTTPRequest mockUserInfoRequest = mockHttpRequest(responseBody, 200, "HTTP OK") + + // Act + String identity = soip.lookupIdentityInUserInfo(mockUserInfoRequest) + logger.info("Identity: ${identity}") + + // Assert + assert identity == EXPECTED_IDENTITY + } + + @Test + void testLookupIdentityUserInfoShouldHandleMissingIdentity() { + // Arrange + StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockJwtService, mockNiFiRegistryProperties) + + Issuer mockIssuer = new Issuer("https://localhost/oidc") + URI mockURI = new URI("https://localhost/oidc") + + OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer, [SubjectType.PUBLIC], mockURI) + soip.oidcProviderMetadata = metadata + + def responseBody = [username: "", sub: "testSub"] + HTTPRequest mockUserInfoRequest = mockHttpRequest(responseBody, 200, "HTTP NO USER") + + // Act + def msg = shouldFail(IllegalStateException) { + String identity = soip.lookupIdentityInUserInfo(mockUserInfoRequest) + logger.info("Identity: ${identity}") + } + logger.expected(msg) + + // Assert + assert msg =~ "Unable to extract identity from the UserInfo token using the claim 'username'." + } + + @Test + void testLookupIdentityUserInfoShouldHandle500() { + // Arrange + StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockJwtService, mockNiFiRegistryProperties) + + Issuer mockIssuer = new Issuer("https://localhost/oidc") + URI mockURI = new URI("https://localhost/oidc") + + OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer, [SubjectType.PUBLIC], mockURI) + soip.oidcProviderMetadata = metadata + + def errorBody = [error : "Failure to authenticate", + error_description: "The provided username and password were not correct", + error_uri : "https://localhost/oidc/error"] + HTTPRequest mockUserInfoRequest = mockHttpRequest(errorBody, 500, "HTTP ERROR") + + // Act + def msg = shouldFail(RuntimeException) { + String identity = soip.lookupIdentityInUserInfo(mockUserInfoRequest) + logger.info("Identity: ${identity}") + } + logger.expected(msg) + + // Assert + assert msg =~ "An error occurred while invoking the UserInfo endpoint: The provided username and password were not correct" + } + + @Test + void testShouldConvertOIDCTokenToNiFiToken() { + // Arrange + StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator(["getOidcClaimIdentifyingUser": "email"]) + + OIDCTokenResponse mockResponse = mockOIDCTokenResponse() + logger.info("OIDC Token Response: ${mockResponse.dump()}") + + // Act + String nifiToken = soip.convertOIDCTokenToNiFiToken(mockResponse) + logger.info("NiFi token: ${nifiToken}") + + // Assert + + // Split JWT into components and decode Base64 to JSON + def (String headerB64, String payloadB64, String signatureB64) = nifiToken.tokenize("\\.") + logger.info("Header: ${headerB64} | Payload: ${payloadB64} | Signature: ${signatureB64}") + String headerJson = new String(Base64.decoder.decode(headerB64), "UTF-8") + String payloadJson = new String(Base64.decoder.decode(payloadB64), "UTF-8") + // String signatureJson = new String(Base64.decoder.decode(signatureB64), "UTF-8") + + // Parse JSON into objects + def slurper = new JsonSlurper() + def header = slurper.parseText(headerJson) + logger.info("Header: ${header}") + + assert header.alg == "HS256" + + def payload = slurper.parseText(payloadJson) + logger.info("Payload: ${payload}") + + assert payload.username == "person@nifi.apache.org" + assert payload.keyId == "1" + assert payload.exp <= System.currentTimeMillis() + 10_000 + } + + @Test + void testConvertOIDCTokenToNiFiTokenShouldHandleBlankIdentity() { + // Arrange + StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator(["getOidcClaimIdentifyingUser": "non-existent-claim"]) + + OIDCTokenResponse mockResponse = mockOIDCTokenResponse() + logger.info("OIDC Token Response: ${mockResponse.dump()}") + + // Act + String nifiToken = soip.convertOIDCTokenToNiFiToken(mockResponse) + logger.info("NiFi token: ${nifiToken}") + + // Assert + // Split JWT into components and decode Base64 to JSON + def (String headerB64, String payloadB64, String signatureB64) = nifiToken.tokenize("\\.") + logger.info("Header: ${headerB64} | Payload: ${payloadB64} | Signature: ${signatureB64}") + String headerJson = new String(Base64.decoder.decode(headerB64), "UTF-8") + String payloadJson = new String(Base64.decoder.decode(payloadB64), "UTF-8") + // String signatureJson = new String(Base64.decoder.decode(signatureB64), "UTF-8") + + // Parse JSON into objects + def slurper = new JsonSlurper() + def header = slurper.parseText(headerJson) + logger.info("Header: ${header}") + + assert header.alg == "HS256" + + def payload = slurper.parseText(payloadJson) + logger.info("Payload: ${payload}") + + assert payload.username == "person@nifi.apache.org" + assert payload.keyId == "1" + assert payload.exp <= System.currentTimeMillis() + 10_000 + } + + @Test + void testConvertOIDCTokenToNiFiTokenShouldHandleBlankIdentityAndNoEmailClaim() { + // Arrange + StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator(["getOidcClaimIdentifyingUser": "non-existent-claim"]) + + OIDCTokenResponse mockResponse = mockOIDCTokenResponse(["email": null]) + logger.info("OIDC Token Response: ${mockResponse.dump()}") + + // Act + def msg = shouldFail(ConnectException) { + String nifiToken = soip.convertOIDCTokenToNiFiToken(mockResponse) + logger.info("NiFi token: ${nifiToken}") + } + + // Assert + assert msg =~ "Connection refused" + } + + @Test + void testShouldAuthorizeClient() { + // Arrange + // Build ID Provider with mock token endpoint URI to make a connection + StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator([:]) + + // Mock the JWT + def jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik5pRmkgT0lEQyBVbml0IFRlc3RlciIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MzM5MDIyLCJpc3MiOiJuaWZpX3VuaXRfdGVzdF9hdXRob3JpdHkiLCJhdWQiOiJhbGwiLCJ1c2VybmFtZSI6Im9pZGNfdGVzdCIsImVtYWlsIjoib2lkY190ZXN0QG5pZmkuYXBhY2hlLm9yZyJ9.b4NIl0RONKdVLOH0D1eObdwAEX8qX-ExqB8KuKSZFLw" + + def responseBody = [id_token: jwt, access_token: "some.access.token", refresh_token: "some.refresh.token", token_type: "bearer"] + HTTPRequest mockTokenRequest = mockHttpRequest(responseBody, 200, "HTTP OK") + + // Act + def nifiToken = soip.authorizeClient(mockTokenRequest) + logger.info("NiFi Token: ${nifiToken.dump()}") + + // Assert + assert nifiToken + } + + @Test + void testAuthorizeClientShouldHandleError() { + // Arrange + // Build ID Provider with mock token endpoint URI to make a connection + StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator([:]) + + // Mock the JWT + def jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik5pRmkgT0lEQyBVbml0IFRlc3RlciIsImlhdCI6MTUxNjIzOTAyMiwiZXhwIjoxNTE2MzM5MDIyLCJpc3MiOiJuaWZpX3VuaXRfdGVzdF9hdXRob3JpdHkiLCJhdWQiOiJhbGwiLCJ1c2VybmFtZSI6Im9pZGNfdGVzdCIsImVtYWlsIjoib2lkY190ZXN0QG5pZmkuYXBhY2hlLm9yZyJ9.b4NIl0RONKdVLOH0D1eObdwAEX8qX-ExqB8KuKSZFLw" + + def responseBody = [id_token: jwt, access_token: "some.access.token", refresh_token: "some.refresh.token", token_type: "bearer"] + HTTPRequest mockTokenRequest = mockHttpRequest(responseBody, 500, "HTTP SERVER ERROR") + + // Act + def msg = shouldFail(RuntimeException) { + def nifiToken = soip.authorizeClient(mockTokenRequest) + logger.info("NiFi token: ${nifiToken}") + } + + // Assert + assert msg =~ "An error occurred while invoking the Token endpoint: null" + } + + + private StandardOidcIdentityProvider buildIdentityProviderWithMockTokenValidator(Map additionalProperties = [:]) { + JwtService mockJS = buildJwtService() + NiFiRegistryProperties mockNFP = buildNiFiRegistryProperties(additionalProperties) + StandardOidcIdentityProvider soip = new StandardOidcIdentityProvider(mockJS, mockNFP) + + // Mock OIDC provider metadata + Issuer mockIssuer = new Issuer("mockIssuer") + URI mockURI = new URI("https://localhost/oidc") + OIDCProviderMetadata metadata = new OIDCProviderMetadata(mockIssuer, [SubjectType.PUBLIC], mockURI) + soip.oidcProviderMetadata = metadata + + // Set OIDC Provider metadata attributes + final ClientID CLIENT_ID = new ClientID("expected_client_id") + final Secret CLIENT_SECRET = new Secret("expected_client_secret") + + // Inject into OIP + soip.clientId = CLIENT_ID + soip.clientSecret = CLIENT_SECRET + soip.oidcProviderMetadata["tokenEndpointAuthMethods"] = [ClientAuthenticationMethod.CLIENT_SECRET_BASIC] + soip.oidcProviderMetadata["tokenEndpointURI"] = new URI("https://localhost/oidc/token") + soip.oidcProviderMetadata["userInfoEndpointURI"] = new URI("https://localhost/oidc/userInfo") + + // Mock token validator + IDTokenValidator mockTokenValidator = new IDTokenValidator(mockIssuer, CLIENT_ID) { + @Override + IDTokenClaimsSet validate(JWT jwt, Nonce nonce) { + return new IDTokenClaimsSet(jwt.getJWTClaimsSet()) + } + } + soip.tokenValidator = mockTokenValidator + soip + } + + private OIDCTokenResponse mockOIDCTokenResponse(Map additionalClaims = [:]) { + final Map claims = [ + "iss" : "https://accounts.google.com", + "azp" : "1013352044499-05pb1ssdfuihsdfsdsdfdi8r2vike88m.apps.googleusercontent.com", + "aud" : "1013352044499-05pb1ssdfuihsdfsdsdfdi8r2vike88m.apps.googleusercontent.com", + "sub" : "10703475345439756345540", + "email" : "person@nifi.apache.org", + "email_verified": "true", + "at_hash" : "JOGISUDHFiyGHDSFwV5Fah2A", + "iat" : 1590022674, + "exp" : 1590026274 + ] + additionalClaims + + // Create Claims Set + JWTClaimsSet mockJWTClaimsSet = new JWTClaimsSet(claims) + + // Create JWT + JWT mockJwt = new PlainJWT(mockJWTClaimsSet) + + // Mock access tokens + AccessToken mockAccessToken = new BearerAccessToken() + RefreshToken mockRefreshToken = new RefreshToken() + + // Create OIDC Tokens + OIDCTokens mockOidcTokens = new OIDCTokens(mockJwt, mockAccessToken, mockRefreshToken) + + // Create OIDC Token Response + OIDCTokenResponse mockResponse = new OIDCTokenResponse(mockOidcTokens) + mockResponse + } + + + /** + * Forms an {@link HTTPRequest} object which returns a static response when {@code send( )} is called. + * + * @param body the JSON body in Map form + * @param statusCode the HTTP status code + * @param status the HTTP status message + * @param headers an optional map of HTTP response headers + * @param method the HTTP method to mock + * @param url the endpoint URL + * @return the static HTTP response + */ + private static HTTPRequest mockHttpRequest(def body, + int statusCode = 200, + String status = "HTTP Response", + Map headers = [:], + HTTPRequest.Method method = HTTPRequest.Method.GET, + URL url = new URL("https://localhost/oidc")) { + new HTTPRequest(method, url) { + HTTPResponse send() { + HTTPResponse mockResponse = new HTTPResponse(statusCode) + mockResponse.setStatusMessage(status) + (["Content-Type": "application/json"] + headers).each { String h, String v -> mockResponse.setHeader(h, v) } + def responseBody = body + mockResponse.setContent(JsonOutput.toJson(responseBody)) + mockResponse + } + } + } + + class MockOIDCProviderMetadata extends OIDCProviderMetadata { + + MockOIDCProviderMetadata() { + super([:] as Issuer, [SubjectType.PUBLIC] as List, new URI("https://localhost")) + } + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/NiFiRegistryTestApiApplication.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/NiFiRegistryTestApiApplication.java new file mode 100644 index 0000000000..ac56d1bf7d --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/NiFiRegistryTestApiApplication.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry; + +import org.apache.nifi.registry.db.DataSourceFactory; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; + +import java.util.TimeZone; + +@SpringBootApplication +@ComponentScan( + excludeFilters = { + @ComponentScan.Filter( + type = FilterType.ASSIGNABLE_TYPE, + value = SpringBootServletInitializer.class), // Avoid loading NiFiRegistryApiApplication + @ComponentScan.Filter( + type = FilterType.ASSIGNABLE_TYPE, + value = DataSourceFactory.class), // Avoid loading DataSourceFactory + @ComponentScan.Filter( + type = FilterType.REGEX, + pattern = "org\\.apache\\.nifi\\.registry\\.NiFiRegistryPropertiesFactory"), // Avoid loading NiFiRegistryPropertiesFactory + }) +public class NiFiRegistryTestApiApplication extends SpringBootServletInitializer { + + // Since H2 uses the JVM's timezone, setting UTC here ensures that the JVM has a consistent timezone set + // before the H2 DB is created, regardless of platform (i.e. local build vs Travis) + static { + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/SecureLdapTestApiApplication.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/SecureLdapTestApiApplication.java new file mode 100644 index 0000000000..74d0730ff4 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/SecureLdapTestApiApplication.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry; + +import org.apache.nifi.registry.db.DataSourceFactory; +import org.apache.nifi.registry.security.authorization.AuthorizerFactory; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; + +@SpringBootApplication +@ComponentScan( + basePackages = "org.apache.nifi.registry", + excludeFilters = { + @ComponentScan.Filter( + type = FilterType.ASSIGNABLE_TYPE, + value = SpringBootServletInitializer.class), // Avoid loading NiFiRegistryApiApplication + @ComponentScan.Filter( + type = FilterType.ASSIGNABLE_TYPE, + value = DataSourceFactory.class), // Avoid loading DataSourceFactory + @ComponentScan.Filter( + type = FilterType.ASSIGNABLE_TYPE, + value = AuthorizerFactory.class), // Avoid loading AuthorizerFactory.getAuthorizer(), as we need to add it again with test-specific @DependsOn annotation + @ComponentScan.Filter( + type = FilterType.REGEX, + pattern = "org\\.apache\\.nifi\\.registry\\.NiFiRegistryPropertiesFactory"), // Avoid loading NiFiRegistryPropertiesFactory + }) +public class SecureLdapTestApiApplication extends SpringBootServletInitializer { + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/JettyITServerCustomizer.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/JettyITServerCustomizer.java new file mode 100644 index 0000000000..15bc848eff --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/JettyITServerCustomizer.java @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web; + + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; +import org.springframework.boot.web.server.Ssl; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.stereotype.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This customizer fixes integration tests. The customizer is the only way we can pass config from Spring Boot to Jetty. + * It sets the endpointIdentificationAlgorithm to null, which stops the Jetty server attempting to validate a hostname in the client certificate's SAN. + **/ +@Component +public class JettyITServerCustomizer implements WebServerFactoryCustomizer { + + private static final Logger LOGGER = LoggerFactory.getLogger(JettyITServerCustomizer.class); + + @Autowired + private ServerProperties serverProperties; + + private static final int HEADER_BUFFER_SIZE = 16 * 1024; // 16kb + + @Override + public void customize(final JettyServletWebServerFactory factory) { + LOGGER.info("Customizing Jetty server for integration tests..."); + + factory.addServerCustomizers((server) -> { + final Ssl sslProperties = serverProperties.getSsl(); + if (sslProperties != null) { + createSslContextFactory(sslProperties); + ServerConnector con = (ServerConnector) server.getConnectors()[0]; + int existingConnectorPort = con.getLocalPort(); + + // create the http configuration + final HttpConfiguration httpConfiguration = new HttpConfiguration(); + httpConfiguration.setRequestHeaderSize(HEADER_BUFFER_SIZE); + httpConfiguration.setResponseHeaderSize(HEADER_BUFFER_SIZE); + + // add some secure config + final HttpConfiguration httpsConfiguration = new HttpConfiguration(httpConfiguration); + httpsConfiguration.setSecureScheme("https"); + httpsConfiguration.setSecurePort(existingConnectorPort); + httpsConfiguration.addCustomizer(new SecureRequestCustomizer()); + + // build the connector with the endpoint identification algorithm set to null + final ServerConnector httpsConnector = new ServerConnector(server, + new SslConnectionFactory(createSslContextFactory(sslProperties), "http/1.1"), + new HttpConnectionFactory(httpsConfiguration)); + server.removeConnector(con); + server.addConnector(httpsConnector); + } + }); + + LOGGER.info("JettyServer is customized"); + } + + private SslContextFactory createSslContextFactory(Ssl properties) { + // Calling SslContextFactory.Server() calls setEndpointIdentificationAlgorithm(null). + // This ensures that Jetty server does not attempt to validate a hostname in the client certificate's SAN. + final SslContextFactory.Server contextFactory = new SslContextFactory.Server(); + + // if needClientAuth is false then set want to true so we can optionally use certs + if(properties.getClientAuth() == Ssl.ClientAuth.NEED) { + LOGGER.info("Setting Jetty's SSLContextFactory needClientAuth to true"); + contextFactory.setNeedClientAuth(true); + } else { + LOGGER.info("Setting Jetty's SSLContextFactory wantClientAuth to true"); + contextFactory.setWantClientAuth(true); + } + + /* below code sets JSSE system properties when values are provided */ + // keystore properties + if (StringUtils.isNotBlank(properties.getKeyStore())) { + contextFactory.setKeyStorePath(properties.getKeyStore()); + } + if (StringUtils.isNotBlank(properties.getKeyStoreType())) { + contextFactory.setKeyStoreType(properties.getKeyStoreType()); + } + final String keystorePassword = properties.getKeyStorePassword(); + final String keyPassword = properties.getKeyPassword(); + + if (StringUtils.isEmpty(keystorePassword)) { + throw new IllegalArgumentException("The keystore password cannot be null or empty"); + } else { + // if no key password was provided, then assume the key password is the same as the keystore password. + final String defaultKeyPassword = (StringUtils.isBlank(keyPassword)) ? keystorePassword : keyPassword; + contextFactory.setKeyStorePassword(keystorePassword); + contextFactory.setKeyManagerPassword(defaultKeyPassword); + } + + // truststore properties + if (StringUtils.isNotBlank(properties.getTrustStore())) { + contextFactory.setTrustStorePath(properties.getTrustStore()); + } + if (StringUtils.isNotBlank(properties.getTrustStoreType())) { + contextFactory.setTrustStoreType(properties.getTrustStoreType()); + } + if (StringUtils.isNotBlank(properties.getTrustStorePassword())) { + contextFactory.setTrustStorePassword(properties.getTrustStorePassword()); + } + + return contextFactory; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/TestRestAPI.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/TestRestAPI.java new file mode 100644 index 0000000000..038b299629 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/TestRestAPI.java @@ -0,0 +1,173 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web; + +import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.flow.VersionedFlow; +import org.apache.nifi.registry.flow.VersionedFlowSnapshot; +import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata; +import org.apache.nifi.registry.flow.VersionedProcessGroup; +import org.apache.nifi.registry.revision.entity.RevisionInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.ArrayList; +import java.util.List; + +public class TestRestAPI { + + public static final Logger LOGGER = LoggerFactory.getLogger(TestRestAPI.class); + + public static final String REGISTRY_API_URL = "http://localhost:18080/nifi-registry-api"; + public static final String REGISTRY_API_BUCKETS_URL = REGISTRY_API_URL + "/buckets"; + public static final String REGISTRY_API_FLOWS_URL = REGISTRY_API_URL + "/flows"; + + public static void main(String[] args) { + try { + final Client client = ClientBuilder.newClient(); + + // create some buckets + final int numBuckets = 20; + final List createdBuckets = new ArrayList<>(); + + for (int i=0; i < numBuckets; i++) { + final Bucket createdBucket = createBucket(client, i); + System.out.println("Created bucket # " + i + " with id " + createdBucket.getIdentifier()); + createdBuckets.add(createdBucket); + } + + // create some flows + final int numFlowsPerBucket = 10; + final List allFlows = new ArrayList<>(); + + for (final Bucket bucket : createdBuckets) { + final List createdFlows = createFlows(client, bucket, numFlowsPerBucket); + allFlows.addAll(createdFlows); + } + + // create some snapshots + final int numSnapshotsPerFlow = 10; + for (final VersionedFlow flow : allFlows) { + createSnapshots(client, flow, numSnapshotsPerFlow); + } + + // Retrieve the flow by id +// final Response flowResponse = client.target(REGISTRY_API_FLOWS_URL) +// .path("/{flowId}") +// .resolveTemplate("flowId", createdFlow.getIdentifier()) +// .request() +// .get(); +// +// final String flowJson = flowResponse.readEntity(String.class); +// System.out.println("Flow: " + flowJson); + + } catch (WebApplicationException e) { + LOGGER.error(e.getMessage(), e); + + final Response response = e.getResponse(); + LOGGER.error(response.readEntity(String.class)); + } + } + + private static Bucket createBucket(Client client, int num) { + final Bucket bucket = new Bucket(); + bucket.setName("Bucket #" + num); + bucket.setDescription("This is bucket #" + num); + bucket.setRevision(new RevisionInfo("clientId", 0L)); + + final Bucket createdBucket = client.target(REGISTRY_API_BUCKETS_URL) + .request() + .post( + Entity.entity(bucket, MediaType.APPLICATION_JSON), + Bucket.class + ); + + return createdBucket; + } + + private static VersionedFlow createFlow(Client client, Bucket bucket, int num) { + final VersionedFlow versionedFlow = new VersionedFlow(); + versionedFlow.setName(bucket.getName() + " Flow #" + num); + versionedFlow.setDescription("This is " + bucket.getName() + " flow #" + num); + versionedFlow.setRevision(new RevisionInfo("clientId", 0L)); + + final VersionedFlow createdFlow = client.target(REGISTRY_API_BUCKETS_URL) + .path("/{bucketId}/flows") + .resolveTemplate("bucketId", bucket.getIdentifier()) + .request() + .post( + Entity.entity(versionedFlow, MediaType.APPLICATION_JSON), + VersionedFlow.class + ); + + return createdFlow; + } + + private static List createFlows(Client client, Bucket bucket, int numFlows) { + final List createdFlows = new ArrayList<>(); + + for (int i=0; i < numFlows; i++) { + final VersionedFlow createdFlow = createFlow(client, bucket, i); + System.out.println("Created flow # " + i + " with id " + createdFlow.getIdentifier()); + createdFlows.add(createdFlow); + } + + return createdFlows; + } + + private static VersionedFlowSnapshot createSnapshot(Client client, VersionedFlow flow, int num) { + final VersionedFlowSnapshotMetadata snapshotMetadata1 = new VersionedFlowSnapshotMetadata(); + snapshotMetadata1.setBucketIdentifier(flow.getBucketIdentifier()); + snapshotMetadata1.setFlowIdentifier(flow.getIdentifier()); + snapshotMetadata1.setVersion(num); + snapshotMetadata1.setComments("This is snapshot #" + num); + + final VersionedProcessGroup snapshotContents1 = new VersionedProcessGroup(); + snapshotContents1.setIdentifier("pg1"); + snapshotContents1.setName("Process Group 1"); + + final VersionedFlowSnapshot snapshot1 = new VersionedFlowSnapshot(); + snapshot1.setSnapshotMetadata(snapshotMetadata1); + snapshot1.setFlowContents(snapshotContents1); + + final VersionedFlowSnapshot createdSnapshot = client.target(REGISTRY_API_BUCKETS_URL) + .path("{bucketId}/flows/{flowId}/versions") + .resolveTemplate("bucketId", flow.getBucketIdentifier()) + .resolveTemplate("flowId", flow.getIdentifier()) + .request() + .post( + Entity.entity(snapshot1, MediaType.APPLICATION_JSON_TYPE), + VersionedFlowSnapshot.class + ); + + return createdSnapshot; + } + + private static void createSnapshots(Client client, VersionedFlow flow, int numSnapshots) { + for (int i=1; i <= numSnapshots; i++) { + createSnapshot(client, flow, i); + System.out.println("Created snapshot # " + i + " for flow with id " + flow.getIdentifier()); + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/BucketsIT.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/BucketsIT.java new file mode 100644 index 0000000000..2892d030a3 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/BucketsIT.java @@ -0,0 +1,327 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.api; + +import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.revision.entity.RevisionInfo; +import org.junit.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.springframework.test.annotation.IfProfileValue; +import org.springframework.test.context.jdbc.Sql; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import java.util.UUID; + +import static org.apache.nifi.registry.web.api.IntegrationTestUtils.assertBucketsEqual; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class BucketsIT extends UnsecuredITBase { + + @Test + public void testGetBucketsEmpty() throws Exception { + + // Given: a fresh context server with an empty DB + // When: the /buckets endpoint is queried + + final Bucket[] buckets = client + .target(createURL("buckets")) + .request() + .get(Bucket[].class); + + // Then: an empty array is returned + + assertNotNull(buckets); + assertEquals(0, buckets.length); + } + + // NOTE: The tests that seed the DB directly from SQL end up with different results for the timestamp depending on + // which DB is used, so for now these types of tests only run against H2. + @Test + @IfProfileValue(name="current.database.is.h2", value="true") + @Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = {"classpath:db/clearDB.sql", "classpath:db/BucketsIT.sql"}) + public void testGetBuckets() throws Exception { + + // Given: these buckets have been populated in the DB (see BucketsIT.sql) + + String expected = "[" + + "{\"identifier\":\"1\"," + + "\"name\":\"Bucket 1\"," + + "\"createdTimestamp\":1505134260000," + + "\"description\":\"This is test bucket 1\"," + + "\"permissions\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + + "\"link\":{\"params\":{\"rel\":\"self\"},\"href\":\"buckets/1\"}}," + + "{\"identifier\":\"2\"," + + "\"name\":\"Bucket 2\"," + + "\"createdTimestamp\":1505134320000," + + "\"description\":\"This is test bucket 2\"," + + "\"permissions\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + + "\"link\":{\"params\":{\"rel\":\"self\"},\"href\":\"buckets/2\"}}," + + "{\"identifier\":\"3\"," + + "\"name\":\"Bucket 3\"," + + "\"createdTimestamp\":1505134380000," + + "\"description\":\"This is test bucket 3\"," + + "\"permissions\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + + "\"link\":{\"params\":{\"rel\":\"self\"},\"href\":\"buckets/3\"}}" + + "]"; + + // When: the /buckets endpoint is queried + + String bucketsJson = client + .target(createURL("buckets")) + .request() + .get(String.class); + + // Then: the pre-populated list of buckets is returned + + JSONAssert.assertEquals(expected, bucketsJson, false); + assertTrue(!bucketsJson.contains("null")); // JSON serialization from the server should not include null fields, such as "versionedFlows": null + } + + @Test + public void testGetNonexistentBucket() throws Exception { + // Given: a fresh context server with an empty DB + // When: any /buckets/{id} endpoint is queried + Response response = client.target(createURL("buckets/a-nonexistent-identifier")).request().get(); + + // Then: a 404 response status is returned + assertEquals(404, response.getStatus()); + } + + @Test + public void testCreateBucketGetBucket() throws Exception { + final String clientId = UUID.randomUUID().toString(); + final RevisionInfo initialRevision = new RevisionInfo(clientId, 0L); + + // Given: + + long testStartTime = System.currentTimeMillis(); + final Bucket bucket = new Bucket(); + bucket.setName("Integration Test Bucket"); + bucket.setDescription("A bucket created by an integration test."); + bucket.setRevision(initialRevision); + + // When: a bucket is created on the server + + Bucket createdBucket = client + .target(createURL("buckets")) + .request() + .post(Entity.entity(bucket, MediaType.APPLICATION_JSON), Bucket.class); + + // Then: the server returns the created bucket, with server-set fields populated correctly + + assertBucketsEqual(bucket, createdBucket, false); + assertNotNull(createdBucket.getIdentifier()); + assertTrue(createdBucket.getCreatedTimestamp() - testStartTime > 0L); // both server and client in same JVM, so there shouldn't be skew + assertNotNull(createdBucket.getLink()); + assertNotNull(createdBucket.getLink().getUri()); + assertNotNull(createdBucket.getRevision()); + assertEquals(initialRevision.getVersion() + 1, createdBucket.getRevision().getVersion().longValue()); + assertEquals(initialRevision.getClientId(), createdBucket.getRevision().getClientId()); + + // And when /buckets is queried, then the newly created bucket is returned in the list + + final Bucket[] buckets = client + .target(createURL("buckets")) + .request() + .get(Bucket[].class); + assertNotNull(buckets); + assertEquals(1, buckets.length); + assertBucketsEqual(createdBucket, buckets[0], true); + + // And when the link URI is queried, then the newly created bucket is returned + + final Bucket bucketByLink = client + .target(createURL(buckets[0].getLink().getUri().toString())) + .request() + .get(Bucket.class); + assertBucketsEqual(createdBucket, bucketByLink, true); + + // And when the bucket is queried by /buckets/ID, then the newly created bucket is returned + + final Bucket bucketById = client + .target(createURL("buckets/" + createdBucket.getIdentifier())) + .request() + .get(Bucket.class); + assertBucketsEqual(createdBucket, bucketById, true); + assertNotNull(bucketById.getRevision()); + assertEquals(initialRevision.getVersion() + 1, bucketById.getRevision().getVersion().longValue()); + assertEquals(initialRevision.getClientId(), bucketById.getRevision().getClientId()); + } + + @Test + public void testUpdateBucket() throws Exception { + final String clientId = UUID.randomUUID().toString(); + final RevisionInfo initialRevision = new RevisionInfo(clientId, 0L); + + // Given: a bucket exists on the server + + final Bucket bucket = new Bucket(); + bucket.setName("Integration Test Bucket"); + bucket.setDescription("A bucket created by an integration test."); + bucket.setRevision(initialRevision); + + Bucket createdBucket = client + .target(createURL("buckets")) + .request() + .post(Entity.entity(bucket, MediaType.APPLICATION_JSON), Bucket.class); + + // When: the bucket is modified by the client and updated on the server + + createdBucket.setName("Renamed Bucket"); + createdBucket.setDescription("This bucket has been updated by an integration test."); + + final Bucket updatedBucket = client + .target(createURL("buckets/" + createdBucket.getIdentifier())) + .request() + .put(Entity.entity(createdBucket, MediaType.APPLICATION_JSON), Bucket.class); + + // Then: the server returns the updated bucket + + assertBucketsEqual(createdBucket, updatedBucket, true); + } + + @Test + public void testUpdateBucketWithIncorrectRevision() throws Exception { + final String clientId = UUID.randomUUID().toString(); + final RevisionInfo initialRevision = new RevisionInfo(clientId, 0L); + + // Given: a bucket exists on the server + + final Bucket bucket = new Bucket(); + bucket.setName("Integration Test Bucket"); + bucket.setDescription("A bucket created by an integration test."); + bucket.setRevision(initialRevision); + + Bucket createdBucket = client + .target(createURL("buckets")) + .request() + .post(Entity.entity(bucket, MediaType.APPLICATION_JSON), Bucket.class); + + // When: the bucket is modified by the client and updated on the server + + createdBucket.setName("Renamed Bucket"); + createdBucket.setDescription("This bucket has been updated by an integration test."); + + // Change version to incorrect number and don't send a client id + createdBucket.getRevision().setClientId(null); + createdBucket.getRevision().setVersion(99L); + + final Response response = client + .target(createURL("buckets/" + createdBucket.getIdentifier())) + .request() + .put(Entity.entity(createdBucket, MediaType.APPLICATION_JSON)); + + // Then: we get a bad request for sending a wrong revision + + assertEquals(400, response.getStatus()); + } + + @Test + public void testDeleteBucket() throws Exception { + final String clientId = UUID.randomUUID().toString(); + final RevisionInfo initialRevision = new RevisionInfo(clientId, 0L); + + // Given: a bucket has been created + + final Bucket bucket = new Bucket(); + bucket.setName("Integration Test Bucket"); + bucket.setDescription("A bucket created by an integration test."); + bucket.setRevision(initialRevision); + + Bucket createdBucket = client + .target(createURL("buckets")) + .request() + .post(Entity.entity(bucket, MediaType.APPLICATION_JSON), Bucket.class); + + // When: that bucket deleted + + final Bucket deletedBucket = client + .target(createURL("buckets/" + createdBucket.getIdentifier())) + .queryParam("version", createdBucket.getRevision().getVersion().longValue()) + .request() + .delete(Bucket.class); + + // Then: the body of the server response matches the bucket that was deleted + // and: the bucket is no longer accessible (resource not found) + + createdBucket.setPermissions(null); // authorizedActions will not be present in deletedBucket + createdBucket.setLink(null); // links will not be present in deletedBucket + assertBucketsEqual(createdBucket, deletedBucket, true); + + final Response response = client + .target(createURL("buckets/" + createdBucket.getIdentifier())) + .request() + .get(); + assertEquals(404, response.getStatus()); + } + + @Test + public void testDeleteBucketWithIncorrectRevision() throws Exception { + final String clientId = UUID.randomUUID().toString(); + final RevisionInfo initialRevision = new RevisionInfo(clientId, 0L); + + // Given: a bucket has been created + + final Bucket bucket = new Bucket(); + bucket.setName("Integration Test Bucket"); + bucket.setDescription("A bucket created by an integration test."); + bucket.setRevision(initialRevision); + + Bucket createdBucket = client + .target(createURL("buckets")) + .request() + .post(Entity.entity(bucket, MediaType.APPLICATION_JSON), Bucket.class); + + // When: that bucket deleted + + final Response response = client + .target(createURL("buckets/" + createdBucket.getIdentifier())) + .queryParam("version", 99L) + .request() + .delete(); + + // Then: we get a bad request for sending the wrong revision version + + assertEquals(400, response.getStatus()); + } + + @Test + public void getBucketFields() throws Exception { + + // Given: the server is configured to return this fixed response + + String expected = "{\"fields\":[\"ID\",\"NAME\",\"DESCRIPTION\",\"CREATED\"]}"; + + // When: the server is queried + + String bucketFieldsJson = client + .target(createURL("buckets/fields")) + .request() + .get(String.class); + + // Then: the fixed response is returned to the client + + JSONAssert.assertEquals(expected, bucketFieldsJson, false); + + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/DBFlowStorageIT.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/DBFlowStorageIT.java new file mode 100644 index 0000000000..5afccc3553 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/DBFlowStorageIT.java @@ -0,0 +1,185 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.api; + +import org.apache.nifi.registry.NiFiRegistryTestApiApplication; +import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.client.NiFiRegistryClient; +import org.apache.nifi.registry.client.NiFiRegistryClientConfig; +import org.apache.nifi.registry.client.NiFiRegistryException; +import org.apache.nifi.registry.client.impl.JerseyNiFiRegistryClient; +import org.apache.nifi.registry.flow.VersionedFlow; +import org.apache.nifi.registry.flow.VersionedFlowSnapshot; +import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata; +import org.apache.nifi.registry.flow.VersionedProcessGroup; +import org.apache.nifi.registry.revision.entity.RevisionInfo; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.IOException; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +@RunWith(SpringRunner.class) +@SpringBootTest( + classes = NiFiRegistryTestApiApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = "spring.profiles.include=ITDBFlowStorage") +@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:db/clearDB.sql") +public class DBFlowStorageIT extends IntegrationTestBase { + + static final Logger LOGGER = LoggerFactory.getLogger(UnsecuredNiFiRegistryClientIT.class); + + private NiFiRegistryClient client; + + @Before + public void setup() throws IOException { + final String baseUrl = createBaseURL(); + LOGGER.info("Using base url = " + baseUrl); + + final NiFiRegistryClientConfig clientConfig = new NiFiRegistryClientConfig.Builder() + .baseUrl(baseUrl) + .build(); + assertNotNull(clientConfig); + + final NiFiRegistryClient client = new JerseyNiFiRegistryClient.Builder() + .config(clientConfig) + .build(); + assertNotNull(client); + this.client = client; + } + + @After + public void teardown() { + try { + client.close(); + } catch (Exception e) { + LOGGER.warn(e.getMessage(), e); + } + } + + @Test + public void testAll() throws IOException, NiFiRegistryException { + final RevisionInfo initialRevision = new RevisionInfo("DBFlowStorageIT", 0L); + + // Create two buckets... + + final Bucket b1 = new Bucket(); + b1.setName("b1"); + b1.setRevision(initialRevision); + + final Bucket createdB1 = client.getBucketClient().create(b1); + assertNotNull(createdB1); + + final Bucket b2 = new Bucket(); + b2.setName("b2"); + b2.setRevision(initialRevision); + + final Bucket createdB2 = client.getBucketClient().create(b2); + assertNotNull(createdB2); + + // Create two flows... + + final VersionedFlow f1 = new VersionedFlow(); + f1.setName("f1"); + f1.setBucketIdentifier(createdB1.getIdentifier()); + f1.setRevision(initialRevision); + + final VersionedFlow createdF1 = client.getFlowClient().create(f1); + assertNotNull(createdF1); + + final VersionedFlow f2 = new VersionedFlow(); + f2.setName("f2"); + f2.setBucketIdentifier(createdB2.getIdentifier()); + f2.setRevision(initialRevision); + + final VersionedFlow createdF2 = client.getFlowClient().create(f2); + assertNotNull(createdF2); + + // Create some versions for each flow... + + final VersionedFlowSnapshot snapshotF1V1 = createSnapshot(createdB1, createdF1, 1, "f1v1"); + final VersionedFlowSnapshot createdSnapshotF1V1 = client.getFlowSnapshotClient().create(snapshotF1V1); + assertNotNull(createdSnapshotF1V1); + + final VersionedFlowSnapshot snapshotF1V2 = createSnapshot(createdB1, createdF1, 2, "f1v2"); + final VersionedFlowSnapshot createdSnapshotF1V2 = client.getFlowSnapshotClient().create(snapshotF1V2); + assertNotNull(createdSnapshotF1V2); + + final VersionedFlowSnapshot snapshotF2V1 = createSnapshot(createdB2, createdF2, 1, "f2v1"); + final VersionedFlowSnapshot createdSnapshotF2V1 = client.getFlowSnapshotClient().create(snapshotF2V1); + assertNotNull(createdSnapshotF2V1); + + final VersionedFlowSnapshot snapshotF2V2 = createSnapshot(createdB2, createdF2, 2, "f2v2"); + final VersionedFlowSnapshot createdSnapshotF2V2 = client.getFlowSnapshotClient().create(snapshotF2V2); + assertNotNull(createdSnapshotF2V2); + + // Verify retrieving flow versions... + + final VersionedFlowSnapshot retrievedSnapshotF1V1 = client.getFlowSnapshotClient().get(createdF1.getIdentifier(), 1); + assertNotNull(retrievedSnapshotF1V1); + assertNotNull(retrievedSnapshotF1V1.getFlowContents()); + assertEquals("f1v1", retrievedSnapshotF1V1.getFlowContents().getName()); + + final VersionedFlowSnapshot retrievedSnapshotF1V2 = client.getFlowSnapshotClient().get(createdF1.getIdentifier(), 2); + assertNotNull(retrievedSnapshotF1V2); + assertNotNull(retrievedSnapshotF1V2.getFlowContents()); + assertEquals("f1v2", retrievedSnapshotF1V2.getFlowContents().getName()); + + // Verify deleting a flow... + + client.getFlowClient().delete(createdB1.getIdentifier(), createdF1.getIdentifier(), createdF1.getRevision()); + + // All versions of f1 should be deleted + try { + client.getFlowSnapshotClient().get(createdF1.getIdentifier(), 1); + fail("Should have thrown exception"); + } catch (NiFiRegistryException nre) { + } + + // Versions of f2 should still exist... + final VersionedFlowSnapshot retrievedSnapshotF2V1 = client.getFlowSnapshotClient().get(createdF2.getIdentifier(), 1); + assertNotNull(retrievedSnapshotF2V1); + assertNotNull(retrievedSnapshotF2V1.getFlowContents()); + assertEquals("f2v1", retrievedSnapshotF2V1.getFlowContents().getName()); + } + + private VersionedFlowSnapshot createSnapshot(final Bucket bucket, final VersionedFlow flow, final int version, final String rootPgName) { + final VersionedProcessGroup rootPg = new VersionedProcessGroup(); + rootPg.setName(rootPgName); + + final VersionedFlowSnapshotMetadata snapshotMetadata = new VersionedFlowSnapshotMetadata(); + snapshotMetadata.setBucketIdentifier(bucket.getIdentifier()); + snapshotMetadata.setFlowIdentifier(flow.getIdentifier()); + snapshotMetadata.setVersion(version); + snapshotMetadata.setComments("comments"); + + final VersionedFlowSnapshot snapshot = new VersionedFlowSnapshot(); + snapshot.setFlowContents(rootPg); + snapshot.setSnapshotMetadata(snapshotMetadata); + return snapshot; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/FlowsIT.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/FlowsIT.java new file mode 100644 index 0000000000..4471494b9e --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/FlowsIT.java @@ -0,0 +1,544 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.api; + +import org.apache.nifi.registry.bucket.BucketItemType; +import org.apache.nifi.registry.flow.VersionedFlow; +import org.apache.nifi.registry.flow.VersionedFlowSnapshot; +import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata; +import org.apache.nifi.registry.flow.VersionedProcessGroup; +import org.apache.nifi.registry.revision.entity.RevisionInfo; +import org.junit.Assert; +import org.junit.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.springframework.test.annotation.IfProfileValue; +import org.springframework.test.context.jdbc.Sql; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import static org.apache.nifi.registry.web.api.IntegrationTestUtils.assertFlowSnapshotMetadataEqual; +import static org.apache.nifi.registry.web.api.IntegrationTestUtils.assertFlowSnapshotsEqual; +import static org.apache.nifi.registry.web.api.IntegrationTestUtils.assertFlowsEqual; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = {"classpath:db/clearDB.sql", "classpath:db/FlowsIT.sql"}) +public class FlowsIT extends UnsecuredITBase { + + @Test + public void testGetFlowsEmpty() throws Exception { + + // Given: an empty bucket with id "3" (see FlowsIT.sql) + final String emptyBucketId = "3"; + + // When: the /buckets/{id}/flows endpoint is queried + + final VersionedFlow[] flows = client + .target(createURL("buckets/{bucketId}/flows")) + .resolveTemplate("bucketId", emptyBucketId) + .request() + .get(VersionedFlow[].class); + + // Then: an empty array is returned + + assertNotNull(flows); + assertEquals(0, flows.length); + } + + // NOTE: The tests that seed the DB directly from SQL end up with different results for the timestamp depending on + // which DB is used, so for now these types of tests only run against H2. + @Test + @IfProfileValue(name="current.database.is.h2", value="true") + public void testGetFlows() throws Exception { + + // Given: a few buckets and flows have been populated in the DB (see FlowsIT.sql) + + final String prePopulatedBucketId = "1"; + final String expected = "[" + + "{\"identifier\":\"1\"," + + "\"name\":\"Flow 1\"," + + "\"description\":\"This is flow 1\"," + + "\"bucketIdentifier\":\"1\"," + + "\"createdTimestamp\":1505088000000," + + "\"modifiedTimestamp\":1505088000000," + + "\"type\":\"Flow\"," + + "\"permissions\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + + "\"link\":{\"params\":{\"rel\":\"self\"},\"href\":\"buckets/1/flows/1\"}}," + + "{\"identifier\":\"2\",\"name\":\"Flow 2\"," + + "\"description\":\"This is flow 2\"," + + "\"bucketIdentifier\":\"1\"," + + "\"createdTimestamp\":1505088000000," + + "\"modifiedTimestamp\":1505088000000," + + "\"type\":\"Flow\"," + + "\"permissions\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + + "\"versionCount\":0," + + "\"link\":{\"params\":{\"rel\":\"self\"},\"href\":\"buckets/1/flows/2\"}}" + + "]"; + + // When: the /buckets/{id}/flows endpoint is queried + + final String flowsJson = client + .target(createURL("buckets/{bucketId}/flows")) + .resolveTemplate("bucketId", prePopulatedBucketId) + .request() + .get(String.class); + + // Then: the pre-populated list of flows is returned + + JSONAssert.assertEquals(expected, flowsJson, false); + } + + @Test + public void testCreateFlowGetFlow() throws Exception { + final RevisionInfo initialRevision = new RevisionInfo("FlowsIT", 0L); + + // Given: an empty bucket with id "3" (see FlowsIT.sql) + + long testStartTime = System.currentTimeMillis(); + final String bucketId = "3"; + + // When: a flow is created + + final VersionedFlow flow = new VersionedFlow(); + flow.setBucketIdentifier(bucketId); + flow.setName("Test Flow"); + flow.setDescription("This is a flow created by an integration test."); + flow.setRevision(initialRevision); + + final VersionedFlow createdFlow = client + .target(createURL("buckets/{bucketId}/flows")) + .resolveTemplate("bucketId", bucketId) + .request() + .post(Entity.entity(flow, MediaType.APPLICATION_JSON), VersionedFlow.class); + + // Then: the server returns the created flow, with server-set fields populated correctly + + assertFlowsEqual(flow, createdFlow, false); + assertNotNull(createdFlow.getIdentifier()); + assertNotNull(createdFlow.getBucketName()); + assertEquals(0, createdFlow.getVersionCount()); + assertEquals(createdFlow.getType(), BucketItemType.Flow); + assertTrue(createdFlow.getCreatedTimestamp() - testStartTime > 0L); // both server and client in same JVM, so there shouldn't be skew + assertEquals(createdFlow.getCreatedTimestamp(), createdFlow.getModifiedTimestamp()); + assertNotNull(createdFlow.getLink()); + assertNotNull(createdFlow.getLink().getUri()); + assertNotNull(createdFlow.getRevision()); + assertEquals(initialRevision.getClientId(), createdFlow.getRevision().getClientId()); + assertEquals(initialRevision.getVersion() + 1, createdFlow.getRevision().getVersion().longValue()); + + // And when .../flows is queried, then the newly created flow is returned in the list + + final VersionedFlow[] flows = client + .target(createURL("buckets/{bucketId}/flows")) + .resolveTemplate("bucketId", bucketId) + .request() + .get(VersionedFlow[].class); + assertNotNull(flows); + assertEquals(1, flows.length); + assertFlowsEqual(createdFlow, flows[0], true); + + // And when the link URI is queried, then the newly created flow is returned + + final VersionedFlow flowByLink = client + .target(createURL(flows[0].getLink().getUri().toString())) + .request() + .get(VersionedFlow.class); + assertFlowsEqual(createdFlow, flowByLink, true); + + // And when the bucket is queried by .../flows/ID, then the newly created flow is returned + + final VersionedFlow flowById = client + .target(createURL("buckets/{bucketId}/flows/{flowId}")) + .resolveTemplate("bucketId", bucketId) + .resolveTemplate("flowId", createdFlow.getIdentifier()) + .request() + .get(VersionedFlow.class); + assertFlowsEqual(createdFlow, flowById, true); + assertNotNull(flowById.getRevision()); + assertEquals(initialRevision.getClientId(), flowById.getRevision().getClientId()); + assertEquals(initialRevision.getVersion() + 1, flowById.getRevision().getVersion().longValue()); + + } + + @Test + public void testUpdateFlow() throws Exception { + final RevisionInfo initialRevision = new RevisionInfo("FlowsIT", 0L); + + // Given: a flow exists on the server + + final String bucketId = "3"; + final VersionedFlow flow = new VersionedFlow(); + flow.setBucketIdentifier(bucketId); + flow.setName("Test Flow"); + flow.setDescription("This is a flow created by an integration test."); + flow.setRevision(initialRevision); + + final VersionedFlow createdFlow = client + .target(createURL("buckets/{bucketId}/flows")) + .resolveTemplate("bucketId", bucketId) + .request() + .post(Entity.entity(flow, MediaType.APPLICATION_JSON), VersionedFlow.class); + + // When: the flow is modified by the client and updated on the server + + createdFlow.setName("Renamed Flow"); + createdFlow.setDescription("This flow has been updated by an integration test."); + + final VersionedFlow updatedFlow = client + .target(createURL("buckets/{bucketId}/flows/{flowId}")) + .resolveTemplate("bucketId", bucketId) + .resolveTemplate("flowId", createdFlow.getIdentifier()) + .request() + .put(Entity.entity(createdFlow, MediaType.APPLICATION_JSON), VersionedFlow.class); + + // Then: the server returns the updated flow, with a new modified timestamp + + assertTrue(updatedFlow.getModifiedTimestamp() > createdFlow.getModifiedTimestamp()); + createdFlow.setModifiedTimestamp(updatedFlow.getModifiedTimestamp()); + assertFlowsEqual(createdFlow, updatedFlow, true); + } + + @Test + public void testUpdateFlowWithIncorrectRevision() throws Exception { + final RevisionInfo initialRevision = new RevisionInfo("FlowsIT", 0L); + + // Given: a flow exists on the server + + final String bucketId = "3"; + final VersionedFlow flow = new VersionedFlow(); + flow.setBucketIdentifier(bucketId); + flow.setName("Test Flow"); + flow.setDescription("This is a flow created by an integration test."); + flow.setRevision(initialRevision); + + final VersionedFlow createdFlow = client + .target(createURL("buckets/{bucketId}/flows")) + .resolveTemplate("bucketId", bucketId) + .request() + .post(Entity.entity(flow, MediaType.APPLICATION_JSON), VersionedFlow.class); + + // When: the flow revision has no clientId and the incorrect version + + createdFlow.setName("Renamed Flow"); + createdFlow.setDescription("This flow has been updated by an integration test."); + createdFlow.getRevision().setClientId(null); + createdFlow.getRevision().setVersion(99L); + + final Response response = client + .target(createURL("buckets/{bucketId}/flows/{flowId}")) + .resolveTemplate("bucketId", bucketId) + .resolveTemplate("flowId", createdFlow.getIdentifier()) + .request() + .put(Entity.entity(createdFlow, MediaType.APPLICATION_JSON)); + + // Then: 400 bad request because of the incorrect version sent + + assertEquals(400, response.getStatus()); + + } + + @Test + public void testDeleteFlow() throws Exception { + final RevisionInfo initialRevision = new RevisionInfo("FlowsIT", 0L); + + // Given: a flow exists on the server + + final String bucketId = "3"; + final VersionedFlow flow = new VersionedFlow(); + flow.setBucketIdentifier(bucketId); + flow.setName("Test Flow"); + flow.setDescription("This is a flow created by an integration test."); + flow.setRevision(initialRevision); + + final VersionedFlow createdFlow = client + .target(createURL("buckets/{bucketId}/flows")) + .resolveTemplate("bucketId", bucketId) + .request() + .post(Entity.entity(flow, MediaType.APPLICATION_JSON), VersionedFlow.class); + + // When: the flow is deleted + + final VersionedFlow deletedFlow = client + .target(createURL("buckets/{bucketId}/flows/{flowId}")) + .resolveTemplate("bucketId", bucketId) + .resolveTemplate("flowId", createdFlow.getIdentifier()) + .queryParam("version", createdFlow.getRevision().getVersion().longValue()) + .request() + .delete(VersionedFlow.class); + + // Then: the body of the server response matches the flow that was deleted + // and: the flow is no longer accessible (resource not found) + + createdFlow.setLink(null); // self URI will not be present in deletedBucket + assertFlowsEqual(createdFlow, deletedFlow, true); + + final Response response = client + .target(createURL("buckets/{bucketId}/flows/{flowId}")) + .resolveTemplate("bucketId", bucketId) + .resolveTemplate("flowId", createdFlow.getIdentifier()) + .request() + .get(); + assertEquals(404, response.getStatus()); + + } + + @Test + public void testDeleteFlowWithIncorrectRevision() throws Exception { + final RevisionInfo initialRevision = new RevisionInfo("FlowsIT", 0L); + + // Given: a flow exists on the server + + final String bucketId = "3"; + final VersionedFlow flow = new VersionedFlow(); + flow.setBucketIdentifier(bucketId); + flow.setName("Test Flow"); + flow.setDescription("This is a flow created by an integration test."); + flow.setRevision(initialRevision); + + final VersionedFlow createdFlow = client + .target(createURL("buckets/{bucketId}/flows")) + .resolveTemplate("bucketId", bucketId) + .request() + .post(Entity.entity(flow, MediaType.APPLICATION_JSON), VersionedFlow.class); + + // When: the flow is deleted + + final Response response = client + .target(createURL("buckets/{bucketId}/flows/{flowId}")) + .resolveTemplate("bucketId", bucketId) + .resolveTemplate("flowId", createdFlow.getIdentifier()) + .queryParam("version", 99L) + .request() + .delete(); + + // Then: 400 bad request because of the incorrect version sent + + assertEquals(400, response.getStatus()); + } + + @Test + public void testGetFlowVersionsEmpty() throws Exception { + + // Given: a Bucket "2" containing a flow "3" with no snapshots (see FlowsIT.sql) + final String bucketId = "2"; + final String flowId = "3"; + + // When: the /buckets/{id}/flows/{id}/versions endpoint is queried + + final VersionedFlowSnapshot[] flowSnapshots = client + .target(createURL("buckets/{bucketId}/flows/{flowId}/versions")) + .resolveTemplate("bucketId", bucketId) + .resolveTemplate("flowId", flowId) + .request() + .get(VersionedFlowSnapshot[].class); + + // Then: an empty array is returned + + assertNotNull(flowSnapshots); + assertEquals(0, flowSnapshots.length); + } + + // NOTE: The tests that seed the DB directly from SQL end up with different results for the timestamp depending on + // which DB is used, so for now these types of tests only run against H2. + @Test + @IfProfileValue(name="current.database.is.h2", value="true") + public void testGetFlowVersions() throws Exception { + + // Given: a bucket "1" with flow "1" with existing snapshots has been populated in the DB (see FlowsIT.sql) + + final String prePopulatedBucketId = "1"; + final String prePopulatedFlowId = "1"; + // For this test case, the order of the expected list matters as we are asserting a strict equality check + final String expected = "[" + + "{\"bucketIdentifier\":\"1\"," + + "\"flowIdentifier\":\"1\"," + + "\"version\":2," + + "\"timestamp\":1505174400000," + + "\"author\" : \"user2\"," + + "\"comments\":\"This is flow 1 snapshot 2\"," + + "\"link\":{\"params\":{\"rel\":\"content\"},\"href\":\"buckets/1/flows/1/versions/2\"}}," + + "{\"bucketIdentifier\":\"1\"," + + "\"flowIdentifier\":\"1\"," + + "\"version\":1," + + "\"timestamp\":1505088000000," + + "\"author\" : \"user1\"," + + "\"comments\":\"This is flow 1 snapshot 1\"," + + "\"link\":{\"params\":{\"rel\":\"content\"},\"href\":\"buckets/1/flows/1/versions/1\"}}" + + "]"; + + // When: the /buckets/{id}/flows/{id}/versions endpoint is queried + final String flowSnapshotsJson = client + .target(createURL("buckets/{bucketId}/flows/{flowId}/versions")) + .resolveTemplate("bucketId", prePopulatedBucketId) + .resolveTemplate("flowId", prePopulatedFlowId) + .request() + .get(String.class); + + // Then: the pre-populated list of flow versions is returned, in descending order + JSONAssert.assertEquals(expected, flowSnapshotsJson, true); + + } + + @Test + public void testCreateFlowVersionGetFlowVersion() throws Exception { + final RevisionInfo initialRevision = new RevisionInfo("FlowsIT", 0L); + + // Given: an empty Bucket "3" (see FlowsIT.sql) with a newly created flow + + long testStartTime = System.currentTimeMillis(); + final String bucketId = "2"; + final VersionedFlow flow = new VersionedFlow(); + flow.setBucketIdentifier(bucketId); + flow.setName("Test Flow for creating snapshots"); + flow.setDescription("This is a randomly named flow created by an integration test for the purpose of holding snapshots."); + flow.setRevision(initialRevision); + + final VersionedFlow createdFlow = client + .target(createURL("buckets/{bucketId}/flows")) + .resolveTemplate("bucketId", bucketId) + .request() + .post(Entity.entity(flow, MediaType.APPLICATION_JSON), VersionedFlow.class); + final String flowId = createdFlow.getIdentifier(); + + // When: an initial flow snapshot is created *without* a version + + final VersionedFlowSnapshotMetadata flowSnapshotMetadata = new VersionedFlowSnapshotMetadata(); + flowSnapshotMetadata.setBucketIdentifier("2"); + flowSnapshotMetadata.setFlowIdentifier(flowId); + flowSnapshotMetadata.setComments("This is snapshot 1, created by an integration test."); + final VersionedFlowSnapshot flowSnapshot = new VersionedFlowSnapshot(); + flowSnapshot.setSnapshotMetadata(flowSnapshotMetadata); + flowSnapshot.setFlowContents(new VersionedProcessGroup()); // an empty root process group + + WebTarget clientRequestTarget = client + .target(createURL("buckets/{bucketId}/flows/{flowId}/versions")) + .resolveTemplate("bucketId", bucketId) + .resolveTemplate("flowId", flowId); + final Response response = + clientRequestTarget.request().post(Entity.entity(flowSnapshot, MediaType.APPLICATION_JSON), Response.class); + + // Then: an error is returned because version != 1 + + assertEquals(400, response.getStatus()); + + // But When: an initial flow snapshot is created with version == 1 + + flowSnapshot.getSnapshotMetadata().setVersion(1); + final VersionedFlowSnapshot createdFlowSnapshot = + clientRequestTarget.request().post(Entity.entity(flowSnapshot, MediaType.APPLICATION_JSON), VersionedFlowSnapshot.class); + + // Then: the server returns the created flow snapshot, with server-set fields populated correctly :) + + assertFlowSnapshotsEqual(flowSnapshot, createdFlowSnapshot, false); + assertTrue(createdFlowSnapshot.getSnapshotMetadata().getTimestamp() - testStartTime > 0L); // both server and client in same JVM, so there shouldn't be skew + assertEquals("anonymous", createdFlowSnapshot.getSnapshotMetadata().getAuthor()); + assertNotNull(createdFlowSnapshot.getSnapshotMetadata().getLink()); + assertNotNull(createdFlowSnapshot.getSnapshotMetadata().getLink().getUri()); + assertNotNull(createdFlowSnapshot.getFlow()); + assertEquals(1, createdFlowSnapshot.getFlow().getVersionCount()); + assertNotNull(createdFlowSnapshot.getBucket()); + + // And when .../flows/{id}/versions is queried, then the newly created flow snapshot is returned in the list + + final VersionedFlowSnapshotMetadata[] versionedFlowSnapshots = + clientRequestTarget.request().get(VersionedFlowSnapshotMetadata[].class); + assertNotNull(versionedFlowSnapshots); + assertEquals(1, versionedFlowSnapshots.length); + assertFlowSnapshotMetadataEqual(createdFlowSnapshot.getSnapshotMetadata(), versionedFlowSnapshots[0], true); + + // And when the link URI is queried, then the newly created flow snapshot is returned + + final VersionedFlowSnapshot flowSnapshotByLink = client + .target(createURL(versionedFlowSnapshots[0].getLink().getUri().toString())) + .request() + .get(VersionedFlowSnapshot.class); + assertFlowSnapshotsEqual(createdFlowSnapshot, flowSnapshotByLink, true); + assertNotNull(flowSnapshotByLink.getFlow()); + assertNotNull(flowSnapshotByLink.getBucket()); + + // And when the bucket is queried by .../versions/{v}, then the newly created flow snapshot is returned + + final VersionedFlowSnapshot flowSnapshotByVersionNumber = clientRequestTarget.path("/1").request().get(VersionedFlowSnapshot.class); + assertFlowSnapshotsEqual(createdFlowSnapshot, flowSnapshotByVersionNumber, true); + assertNotNull(flowSnapshotByVersionNumber.getFlow()); + assertNotNull(flowSnapshotByVersionNumber.getBucket()); + + // And when the latest URI is queried, then the newly created flow snapshot is returned + + final VersionedFlowSnapshot flowSnapshotByLatest = clientRequestTarget.path("/latest").request().get(VersionedFlowSnapshot.class); + assertFlowSnapshotsEqual(createdFlowSnapshot, flowSnapshotByLatest, true); + assertNotNull(flowSnapshotByLatest.getFlow()); + assertNotNull(flowSnapshotByLatest.getBucket()); + + } + + @Test + public void testFlowNameUniquePerBucket() throws Exception { + + final String flowName = "Flow 1"; + + // verify we have an existing flow with the name "Flow 1" in bucket 1 + final VersionedFlow existingFlow = client + .target(createURL("buckets/1/flows/1")) + .request() + .get(VersionedFlow.class); + + assertNotNull(existingFlow); + assertEquals(flowName, existingFlow.getName()); + + // create a new flow with the same name + + final String bucketId = "3"; + + final VersionedFlow flow = new VersionedFlow(); + flow.setBucketIdentifier(bucketId); + flow.setName(flowName); + flow.setDescription("This is a flow created by an integration test."); + flow.setRevision(new RevisionInfo("FlowsIT", 0L)); + + // saving this flow to bucket 3 should work because bucket 3 is empty + + final VersionedFlow createdFlow = client + .target(createURL("buckets/3/flows")) + .resolveTemplate("bucketId", bucketId) + .request() + .post(Entity.entity(flow, MediaType.APPLICATION_JSON), VersionedFlow.class); + + assertNotNull(createdFlow); + + // saving the flow to bucket 1 should not work because there is a flow with the same name + flow.setBucketIdentifier("1"); + try { + client.target(createURL("buckets/1/flows")) + .resolveTemplate("bucketId", bucketId) + .request() + .post(Entity.entity(flow, MediaType.APPLICATION_JSON), VersionedFlow.class); + + Assert.fail("Should have thrown exception"); + } catch (WebApplicationException e) { + final String errorMessage = e.getResponse().readEntity(String.class); + Assert.assertEquals("A versioned flow with the same name already exists in the selected bucket", errorMessage); + } + + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/IntegrationTestBase.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/IntegrationTestBase.java new file mode 100644 index 0000000000..0fe05380a5 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/IntegrationTestBase.java @@ -0,0 +1,222 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.api; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector; +import org.apache.nifi.registry.client.NiFiRegistryClientConfig; +import org.apache.nifi.registry.db.DatabaseProfileValueSource; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJaxbJsonProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.context.annotation.Bean; +import org.springframework.test.annotation.ProfileValueSourceConfiguration; + +import javax.annotation.PostConstruct; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import java.io.FileReader; +import java.io.IOException; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * A base class to simplify creating integration tests against an API application running with an embedded server and volatile DB. + */ +@ProfileValueSourceConfiguration(DatabaseProfileValueSource.class) +public abstract class IntegrationTestBase { + + private static final String CONTEXT_PATH = "/nifi-registry-api"; + + @TestConfiguration + public static class TestConfigurationClass { + + /* REQUIRED: Any subclass extending IntegrationTestBase must add a Spring profile that defines a + * property value for this key containing the path to the nifi-registy.properties file to use to + * create a NiFiRegistryProperties Bean in the ApplicationContext. */ + @Value("${nifi.registry.properties.file}") + private String propertiesFileLocation; + + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + private final Lock readLock = lock.readLock(); + private NiFiRegistryProperties testProperties; + + @Bean + public JettyServletWebServerFactory jettyEmbeddedServletContainerFactory() { + JettyServletWebServerFactory jettyContainerFactory = new JettyServletWebServerFactory(); + jettyContainerFactory.setContextPath(CONTEXT_PATH); + return jettyContainerFactory; + } + + @Bean + public NiFiRegistryProperties getNiFiRegistryProperties() { + readLock.lock(); + try { + if (testProperties == null) { + testProperties = loadNiFiRegistryProperties(propertiesFileLocation); + } + } finally { + readLock.unlock(); + } + return testProperties; + } + + } + + @Autowired + private NiFiRegistryProperties properties; + + /* OPTIONAL: Any subclass that extends this base class MAY provide or specify a @TestConfiguration that provides a + * NiFiRegistryClientConfig @Bean. The properties specified should correspond with the integration test cases in + * the concrete subclass. See SecureFileIT for an example. */ + @Autowired(required = false) + private NiFiRegistryClientConfig clientConfig; + + /* This will be injected with the random port assigned to the embedded Jetty container. */ + @LocalServerPort + private int port; + + /** + * Subclasses can access this auto-configured JAX-RS client to communicate to the NiFi Registry Server + */ + protected Client client; + + @PostConstruct + void initialize() { + if (this.clientConfig != null) { + this.client = createClientFromConfig(this.clientConfig); + } else { + this.client = ClientBuilder.newClient(); + } + + } + + /** + * Subclasses can utilize this method to build a URL that has the correct protocol, hostname, and port + * for a given path. + * + * @param relativeResourcePath the path component of the resource you wish to access, relative to the + * base API URL, where the base includes the servlet context path. + * + * @return a String containing the absolute URL of the resource. + */ + String createURL(String relativeResourcePath) { + if (relativeResourcePath == null) { + throw new IllegalArgumentException("Resource path cannot be null"); + } + + final StringBuilder baseUriBuilder = new StringBuilder(createBaseURL()).append(CONTEXT_PATH); + + if (!relativeResourcePath.startsWith("/")) { + baseUriBuilder.append('/'); + } + baseUriBuilder.append(relativeResourcePath); + + return baseUriBuilder.toString(); + } + + /** + * Sub-classes can utilize this method to obtain the base-url for a client. + * + * @return a string containing the base url which includes the scheme, host, and port + */ + String createBaseURL() { + final boolean isSecure = this.properties.getSslPort() != null; + final String protocolSchema = isSecure ? "https" : "http"; + + final StringBuilder baseUriBuilder = new StringBuilder() + .append(protocolSchema).append("://localhost:").append(port); + + return baseUriBuilder.toString(); + } + + NiFiRegistryClientConfig createClientConfig(String baseUrl) { + final NiFiRegistryClientConfig.Builder builder = new NiFiRegistryClientConfig.Builder(); + builder.baseUrl(baseUrl); + + if (this.clientConfig != null) { + if (this.clientConfig.getSslContext() != null) { + builder.sslContext(this.clientConfig.getSslContext()); + } + + if (this.clientConfig.getHostnameVerifier() != null) { + builder.hostnameVerifier(this.clientConfig.getHostnameVerifier()); + } + } + + return builder.build(); + } + + /** + * A helper method for loading NiFiRegistryProperties by reading *.properties files from disk. + * + * @param propertiesFilePath The location of the properties file + * @return A NiFIRegistryProperties instance based on the properties file contents + */ + static NiFiRegistryProperties loadNiFiRegistryProperties(String propertiesFilePath) { + NiFiRegistryProperties properties = new NiFiRegistryProperties(); + try (final FileReader reader = new FileReader(propertiesFilePath)) { + properties.load(reader); + } catch (final IOException ioe) { + throw new RuntimeException("Unable to load properties: " + ioe, ioe); + } + return properties; + } + + private static Client createClientFromConfig(NiFiRegistryClientConfig registryClientConfig) { + + final ClientConfig clientConfig = new ClientConfig(); + clientConfig.register(jacksonJaxbJsonProvider()); + + final ClientBuilder clientBuilder = ClientBuilder.newBuilder().withConfig(clientConfig); + + final SSLContext sslContext = registryClientConfig.getSslContext(); + if (sslContext != null) { + clientBuilder.sslContext(sslContext); + } + + final HostnameVerifier hostnameVerifier = registryClientConfig.getHostnameVerifier(); + if (hostnameVerifier != null) { + clientBuilder.hostnameVerifier(hostnameVerifier); + } + + return clientBuilder.build(); + } + + private static JacksonJaxbJsonProvider jacksonJaxbJsonProvider() { + JacksonJaxbJsonProvider jacksonJaxbJsonProvider = new JacksonJaxbJsonProvider(); + + ObjectMapper mapper = new ObjectMapper(); + mapper.setPropertyInclusion(JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, JsonInclude.Include.NON_NULL)); + mapper.setAnnotationIntrospector(new JaxbAnnotationIntrospector(mapper.getTypeFactory())); + // Ignore unknown properties so that deployed client remain compatible with future versions of NiFi Registry that add new fields + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + jacksonJaxbJsonProvider.setMapper(mapper); + return jacksonJaxbJsonProvider; + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/IntegrationTestUtils.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/IntegrationTestUtils.java new file mode 100644 index 0000000000..8cfcb38bde --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/IntegrationTestUtils.java @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.api; + +import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.flow.VersionedComponent; +import org.apache.nifi.registry.flow.VersionedFlow; +import org.apache.nifi.registry.flow.VersionedFlowSnapshot; +import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata; +import org.apache.nifi.registry.flow.VersionedProcessGroup; + +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +class IntegrationTestUtils { + + public static void assertBucketsEqual(Bucket expected, Bucket actual, boolean checkServerSetFields) { + assertNotNull(actual); + assertEquals(expected.getName(), actual.getName()); + assertEquals(expected.getDescription(), actual.getDescription()); + if (checkServerSetFields) { + assertEquals(expected.getIdentifier(), actual.getIdentifier()); + assertEquals(expected.getCreatedTimestamp(), actual.getCreatedTimestamp()); + assertEquals(expected.getPermissions(), actual.getPermissions()); + assertEquals(expected.getLink(), actual.getLink()); + } + } + + public static void assertFlowsEqual(VersionedFlow expected, VersionedFlow actual, boolean checkServerSetFields) { + assertNotNull(actual); + assertEquals(expected.getName(), actual.getName()); + assertEquals(expected.getDescription(), actual.getDescription()); + assertEquals(expected.getBucketIdentifier(), actual.getBucketIdentifier()); + if (checkServerSetFields) { + assertEquals(expected.getIdentifier(), actual.getIdentifier()); + assertEquals(expected.getVersionCount(), actual.getVersionCount()); + assertEquals(expected.getCreatedTimestamp(), actual.getCreatedTimestamp()); + assertEquals(expected.getModifiedTimestamp(), actual.getModifiedTimestamp()); + assertEquals(expected.getType(), actual.getType()); + assertEquals(expected.getLink(), actual.getLink()); + } + } + + public static void assertFlowSnapshotsEqual(VersionedFlowSnapshot expected, VersionedFlowSnapshot actual, boolean checkServerSetFields) { + + assertNotNull(actual); + + if (expected.getSnapshotMetadata() != null) { + assertFlowSnapshotMetadataEqual(expected.getSnapshotMetadata(), actual.getSnapshotMetadata(), checkServerSetFields); + } + + if (expected.getFlowContents() != null) { + assertVersionedProcessGroupsEqual(expected.getFlowContents(), actual.getFlowContents()); + } + + if (checkServerSetFields) { + assertFlowsEqual(expected.getFlow(), actual.getFlow(), false); // false because if we are checking a newly created snapshot, the versionsCount won't match + assertBucketsEqual(expected.getBucket(), actual.getBucket(), true); + } + + } + + public static void assertFlowSnapshotMetadataEqual( + VersionedFlowSnapshotMetadata expected, VersionedFlowSnapshotMetadata actual, boolean checkServerSetFields) { + + assertNotNull(actual); + assertEquals(expected.getBucketIdentifier(), actual.getBucketIdentifier()); + assertEquals(expected.getFlowIdentifier(), actual.getFlowIdentifier()); + assertEquals(expected.getVersion(), actual.getVersion()); + assertEquals(expected.getComments(), actual.getComments()); + if (checkServerSetFields) { + assertEquals(expected.getTimestamp(), actual.getTimestamp()); + } + } + + private static void assertVersionedProcessGroupsEqual(VersionedProcessGroup expected, VersionedProcessGroup actual) { + assertNotNull(actual); + + assertEquals(((VersionedComponent)expected), ((VersionedComponent)actual)); + + // Poor man's set equality assertion as we are only checking the base type and not doing a recursive check + // TODO, this would be a stronger assertion by replacing this with a true VersionedProcessGroup.equals() method that does a deep equality check + assertSetsEqual(expected.getProcessGroups(), actual.getProcessGroups()); + assertSetsEqual(expected.getRemoteProcessGroups(), actual.getRemoteProcessGroups()); + assertSetsEqual(expected.getProcessors(), actual.getProcessors()); + assertSetsEqual(expected.getInputPorts(), actual.getInputPorts()); + assertSetsEqual(expected.getOutputPorts(), actual.getOutputPorts()); + assertSetsEqual(expected.getConnections(), actual.getConnections()); + assertSetsEqual(expected.getLabels(), actual.getLabels()); + assertSetsEqual(expected.getFunnels(), actual.getFunnels()); + assertSetsEqual(expected.getControllerServices(), actual.getControllerServices()); + } + + + private static void assertSetsEqual(Set expected, Set actual) { + if (expected != null) { + assertNotNull(actual); + assertEquals(expected.size(), actual.size()); + assertTrue(actual.containsAll(expected)); + } + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/NoRevisionsIT.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/NoRevisionsIT.java new file mode 100644 index 0000000000..e93b522adf --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/NoRevisionsIT.java @@ -0,0 +1,139 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.api; + +import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.flow.VersionedFlow; +import org.apache.nifi.registry.flow.VersionedFlowSnapshot; +import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata; +import org.apache.nifi.registry.flow.VersionedProcessGroup; +import org.junit.Test; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; + +import java.util.UUID; + +import static org.apache.nifi.registry.web.api.IntegrationTestUtils.assertBucketsEqual; +import static org.apache.nifi.registry.web.api.IntegrationTestUtils.assertFlowSnapshotsEqual; +import static org.apache.nifi.registry.web.api.IntegrationTestUtils.assertFlowsEqual; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class NoRevisionsIT extends UnsecuredNoRevisionsITBase { + + @Test + public void testNoRevisions() { + // Create a bucket... + + final Bucket bucket = new Bucket(); + bucket.setName("Integration Test Bucket"); + bucket.setDescription("A bucket created by an integration test."); + + final Bucket createdBucket = client + .target(createURL("buckets")) + .request() + .post(Entity.entity(bucket, MediaType.APPLICATION_JSON), Bucket.class); + + assertBucketsEqual(bucket, createdBucket, false); + assertNotNull(createdBucket.getIdentifier()); + + // Update bucket... + + createdBucket.setName("Renamed Bucket"); + createdBucket.setDescription("This bucket has been updated by an integration test."); + + final Bucket updatedBucket = client + .target(createURL("buckets/" + createdBucket.getIdentifier())) + .request() + .put(Entity.entity(createdBucket, MediaType.APPLICATION_JSON), Bucket.class); + + assertBucketsEqual(updatedBucket, createdBucket, true); + + // Create a flow... + + final VersionedFlow flow = new VersionedFlow(); + flow.setIdentifier(UUID.randomUUID().toString()); // Simulate NiFi sending an identifier + flow.setBucketIdentifier(createdBucket.getIdentifier()); + flow.setName("Test Flow"); + flow.setDescription("This is a flow created by an integration test."); + + final VersionedFlow createdFlow = client + .target(createURL("buckets/{bucketId}/flows")) + .resolveTemplate("bucketId", flow.getBucketIdentifier()) + .request() + .post(Entity.entity(flow, MediaType.APPLICATION_JSON), VersionedFlow.class); + + assertFlowsEqual(flow, createdFlow, false); + assertNotNull(createdFlow.getIdentifier()); + + // Update flow... + + createdFlow.setName("Renamed Flow"); + createdFlow.setDescription("This flow has been updated by an integration test."); + + final VersionedFlow updatedFlow = client + .target(createURL("buckets/{bucketId}/flows/{flowId}")) + .resolveTemplate("bucketId",flow.getBucketIdentifier()) + .resolveTemplate("flowId", createdFlow.getIdentifier()) + .request() + .put(Entity.entity(createdFlow, MediaType.APPLICATION_JSON), VersionedFlow.class); + + assertTrue(updatedFlow.getModifiedTimestamp() > createdFlow.getModifiedTimestamp()); + + // Create a version of a flow... + + final VersionedFlowSnapshotMetadata flowSnapshotMetadata = new VersionedFlowSnapshotMetadata(); + flowSnapshotMetadata.setVersion(1); + flowSnapshotMetadata.setBucketIdentifier(createdFlow.getBucketIdentifier()); + flowSnapshotMetadata.setFlowIdentifier(createdFlow.getIdentifier()); + flowSnapshotMetadata.setComments("This is snapshot 1, created by an integration test."); + + final VersionedFlowSnapshot flowSnapshot = new VersionedFlowSnapshot(); + flowSnapshot.setSnapshotMetadata(flowSnapshotMetadata); + flowSnapshot.setFlowContents(new VersionedProcessGroup()); // an empty root process group + + final VersionedFlowSnapshot createdFlowSnapshot = client + .target(createURL("buckets/{bucketId}/flows/{flowId}/versions")) + .resolveTemplate("bucketId", flowSnapshotMetadata.getBucketIdentifier()) + .resolveTemplate("flowId", flowSnapshotMetadata.getFlowIdentifier()) + .request() + .post(Entity.entity(flowSnapshot, MediaType.APPLICATION_JSON), VersionedFlowSnapshot.class); + + assertFlowSnapshotsEqual(flowSnapshot, createdFlowSnapshot, false); + + // Delete flow... + + final VersionedFlow deletedFlow = client + .target(createURL("buckets/{bucketId}/flows/{flowId}")) + .resolveTemplate("bucketId", createdFlow.getBucketIdentifier()) + .resolveTemplate("flowId", createdFlow.getIdentifier()) + .request() + .delete(VersionedFlow.class); + + assertNotNull(deletedFlow); + + // Delete bucket... + + final Bucket deletedBucket = client + .target(createURL("buckets/" + createdBucket.getIdentifier())) + .request() + .delete(Bucket.class); + + assertNotNull(deletedBucket); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureDatabaseIT.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureDatabaseIT.java new file mode 100644 index 0000000000..3b7ce608d5 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureDatabaseIT.java @@ -0,0 +1,238 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.api; + +import org.apache.nifi.registry.NiFiRegistryTestApiApplication; +import org.apache.nifi.registry.authorization.AccessPolicy; +import org.apache.nifi.registry.authorization.User; +import org.apache.nifi.registry.authorization.UserGroup; +import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.client.BucketClient; +import org.apache.nifi.registry.client.NiFiRegistryClient; +import org.apache.nifi.registry.client.NiFiRegistryClientConfig; +import org.apache.nifi.registry.client.PoliciesClient; +import org.apache.nifi.registry.client.TenantsClient; +import org.apache.nifi.registry.client.impl.JerseyNiFiRegistryClient; +import org.apache.nifi.registry.revision.entity.RevisionInfo; +import org.apache.nifi.registry.security.authorization.RequestAction; +import org.apache.nifi.registry.security.authorization.resource.ResourceFactory; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@RunWith(SpringRunner.class) +@SpringBootTest( + classes = NiFiRegistryTestApiApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = "spring.profiles.include=ITSecureDatabase") +@Import(SecureITClientConfiguration.class) +@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = {"classpath:db/clearDB.sql"}) +public class SecureDatabaseIT extends IntegrationTestBase { + + private static final Logger LOGGER = LoggerFactory.getLogger(SecureDatabaseIT.class); + + private static final String INITIAL_ADMIN_IDENTITY = "CN=user1, OU=nifi"; + private static final String OTHER_USER_IDENTITY = "CN=user2, OU=nifi"; + + private NiFiRegistryClient client; + + @Before + public void setup() { + final String baseUrl = createBaseURL(); + LOGGER.info("Using base url = " + baseUrl); + + final NiFiRegistryClientConfig clientConfig = createClientConfig(baseUrl); + Assert.assertNotNull(clientConfig); + + final NiFiRegistryClient client = new JerseyNiFiRegistryClient.Builder() + .config(clientConfig) + .build(); + Assert.assertNotNull(client); + this.client = client; + } + + @After + public void teardown() { + try { + client.close(); + } catch (Exception e) { + + } + } + + @Test + public void testTenantsClientUsers() throws Exception { + final TenantsClient tenantsClient = client.getTenantsClient(); + + // get all users + final List users = tenantsClient.getUsers(); + assertEquals(2, users.size()); + + final User initialAdminUser = users.stream() + .filter(u -> u.getIdentity().equals(INITIAL_ADMIN_IDENTITY)) + .findFirst() + .orElse(null); + assertNotNull(initialAdminUser); + + // get user by id + final User retrievedInitialAdminUser = tenantsClient.getUser(initialAdminUser.getIdentifier()); + assertNotNull(retrievedInitialAdminUser); + assertEquals(initialAdminUser.getIdentity(), retrievedInitialAdminUser.getIdentity()); + + // add user + final User userToAdd = new User(); + userToAdd.setIdentity("some-new-user"); + userToAdd.setRevision(new RevisionInfo(null, 0L)); + + final User createdUser = tenantsClient.createUser(userToAdd); + assertNotNull(createdUser); + assertEquals(3, tenantsClient.getUsers().size()); + + // update user + createdUser.setIdentity(createdUser.getIdentity() + "-updated"); + final User updatedUser = tenantsClient.updateUser(createdUser); + assertNotNull(updatedUser); + assertEquals(createdUser.getIdentity(), updatedUser.getIdentity()); + + // delete user + final User deletedUser = tenantsClient.deleteUser(updatedUser.getIdentifier(), updatedUser.getRevision()); + assertNotNull(deletedUser); + assertEquals(updatedUser.getIdentifier(), deletedUser.getIdentifier()); + } + + @Test + public void testTenantsClientGroups() throws Exception { + final TenantsClient tenantsClient = client.getTenantsClient(); + + // get all groups + final List groups = tenantsClient.getUserGroups(); + assertEquals(0, groups.size()); + + // create group + final UserGroup userGroup = new UserGroup(); + userGroup.setIdentity("some-new group"); + userGroup.setRevision(new RevisionInfo(null, 0L)); + + final UserGroup createdGroup = tenantsClient.createUserGroup(userGroup); + assertNotNull(createdGroup); + assertEquals(userGroup.getIdentity(), createdGroup.getIdentity()); + + // get group by id + final UserGroup retrievedGroup = tenantsClient.getUserGroup(createdGroup.getIdentifier()); + assertNotNull(retrievedGroup); + assertEquals(createdGroup.getIdentifier(), retrievedGroup.getIdentifier()); + + // update group + retrievedGroup.setIdentity(retrievedGroup.getIdentity() + "-updated"); + final UserGroup updatedGroup = tenantsClient.updateUserGroup(retrievedGroup); + assertEquals(retrievedGroup.getIdentity(), updatedGroup.getIdentity()); + + // delete group + final UserGroup deletedGroup = tenantsClient.deleteUserGroup(updatedGroup.getIdentifier(), updatedGroup.getRevision()); + assertNotNull(deletedGroup); + assertEquals(retrievedGroup.getIdentifier(), deletedGroup.getIdentifier()); + } + + @Test + public void testPoliciesClient() throws Exception { + // Create a bucket... + final Bucket bucket = new Bucket(); + bucket.setName("Bucket 1 " + System.currentTimeMillis()); + bucket.setDescription("This is bucket 1"); + bucket.setRevision(new RevisionInfo(null, 0L)); + + final BucketClient bucketClient = client.getBucketClient(); + final Bucket createdBucket = bucketClient.create(bucket); + assertNotNull(createdBucket); + assertNotNull(createdBucket.getIdentifier()); + assertNotNull(createdBucket.getRevision()); + + // Get initial users... + final TenantsClient tenantsClient = client.getTenantsClient(); + + final List users = tenantsClient.getUsers(); + assertEquals(2, users.size()); + + final User initialAdminUser = users.stream() + .filter(u -> u.getIdentity().equals(INITIAL_ADMIN_IDENTITY)) + .findFirst() + .orElse(null); + assertNotNull(initialAdminUser); + + final User otherUser = users.stream() + .filter(u -> u.getIdentity().equals(OTHER_USER_IDENTITY)) + .findFirst() + .orElse(null); + assertNotNull(otherUser); + + // Create a policy on the bucket... + final PoliciesClient policiesClient = client.getPoliciesClient(); + + final AccessPolicy readBucketAccessPolicy = new AccessPolicy(); + readBucketAccessPolicy.setResource(ResourceFactory.getBucketResource( + createdBucket.getIdentifier(), createdBucket.getName()) + .getIdentifier()); + readBucketAccessPolicy.setAction(RequestAction.READ.toString()); + readBucketAccessPolicy.setUsers(Collections.singleton(initialAdminUser)); + readBucketAccessPolicy.setRevision(new RevisionInfo(null, 0L)); + + final AccessPolicy createdAccessPolicy = policiesClient.createAccessPolicy(readBucketAccessPolicy); + assertNotNull(createdAccessPolicy); + assertEquals(readBucketAccessPolicy.getAction(), createdAccessPolicy.getAction()); + assertEquals(readBucketAccessPolicy.getResource(), createdAccessPolicy.getResource()); + assertEquals(1, createdAccessPolicy.getUsers().size()); + assertEquals(INITIAL_ADMIN_IDENTITY, createdAccessPolicy.getUsers().iterator().next().getIdentity()); + assertEquals(1, createdAccessPolicy.getRevision().getVersion().longValue()); + + // Retrieve the policy by action + resource + final AccessPolicy retrievedAccessPolicy = policiesClient.getAccessPolicy( + createdAccessPolicy.getAction(), createdAccessPolicy.getResource()); + assertNotNull(retrievedAccessPolicy); + assertEquals(createdAccessPolicy.getAction(), retrievedAccessPolicy.getAction()); + assertEquals(createdAccessPolicy.getResource(), retrievedAccessPolicy.getResource()); + assertEquals(1, retrievedAccessPolicy.getUsers().size()); + assertEquals(INITIAL_ADMIN_IDENTITY, retrievedAccessPolicy.getUsers().iterator().next().getIdentity()); + assertEquals(1, retrievedAccessPolicy.getRevision().getVersion().longValue()); + + // Update the policy + retrievedAccessPolicy.setUsers(new HashSet<>(Arrays.asList(initialAdminUser, otherUser))); + + final AccessPolicy updatedAccessPolicy = policiesClient.updateAccessPolicy(retrievedAccessPolicy); + assertNotNull(updatedAccessPolicy); + assertEquals(retrievedAccessPolicy.getAction(), updatedAccessPolicy.getAction()); + assertEquals(retrievedAccessPolicy.getResource(), updatedAccessPolicy.getResource()); + assertEquals(2, updatedAccessPolicy.getUsers().size()); + assertEquals(2, updatedAccessPolicy.getRevision().getVersion().longValue()); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureFileIT.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureFileIT.java new file mode 100644 index 0000000000..7f812156fa --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureFileIT.java @@ -0,0 +1,193 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.api; + +import org.apache.nifi.registry.NiFiRegistryTestApiApplication; +import org.apache.nifi.registry.authorization.ResourcePermissions; +import org.apache.nifi.registry.authorization.Tenant; +import org.apache.nifi.registry.authorization.User; +import org.apache.nifi.registry.authorization.UserGroup; +import org.apache.nifi.registry.revision.entity.RevisionInfo; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.skyscreamer.jsonassert.JSONAssert; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.junit4.SpringRunner; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * Deploy the Web API Application using an embedded Jetty Server for local integration testing, with the follow characteristics: + * + * - A NiFiRegistryProperties has to be explicitly provided to the ApplicationContext using a profile unique to this test suite. + * - A NiFiRegistryClientConfig has been configured to create a client capable of completing two-way TLS + * - The database is embed H2 using volatile (in-memory) persistence + * - Custom SQL is clearing the DB before each test method by default, unless method overrides this behavior + */ +@RunWith(SpringRunner.class) +@SpringBootTest( + classes = NiFiRegistryTestApiApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = "spring.profiles.include=ITSecureFile") +@Import(SecureITClientConfiguration.class) +@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:db/clearDB.sql") +public class SecureFileIT extends IntegrationTestBase { + + @Test + public void testAccessStatus() throws Exception { + + // Given: the client and server have been configured correctly for two-way TLS + String expectedJson = "{" + + "\"identity\":\"CN=user1, OU=nifi\"," + + "\"anonymous\":false," + + "\"resourcePermissions\":{" + + "\"anyTopLevelResource\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + + "\"buckets\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + + "\"tenants\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + + "\"policies\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + + "\"proxy\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}}" + + "}"; + + // When: the /access endpoint is queried + final Response response = client + .target(createURL("access")) + .request() + .get(Response.class); + + // Then: the server returns 200 OK with the expected client identity + assertEquals(200, response.getStatus()); + String actualJson = response.readEntity(String.class); + JSONAssert.assertEquals(expectedJson, actualJson, false); + } + + @Test + public void testRetrieveResources() throws Exception { + + // Given: an empty registry returns these resources + String expected = "[" + + "{\"identifier\":\"/actuator\",\"name\":\"Actuator\"}," + + "{\"identifier\":\"/swagger\",\"name\":\"Swagger\"}," + + "{\"identifier\":\"/policies\",\"name\":\"Access Policies\"}," + + "{\"identifier\":\"/tenants\",\"name\":\"Tenants\"}," + + "{\"identifier\":\"/proxy\",\"name\":\"Proxy User Requests\"}," + + "{\"identifier\":\"/buckets\",\"name\":\"Buckets\"}" + + "]"; + + // When: the /resources endpoint is queried + final String resourcesJson = client + .target(createURL("/policies/resources")) + .request() + .get(String.class); + + // Then: the expected array of resources is returned + JSONAssert.assertEquals(expected, resourcesJson, false); + } + + @Test + public void testCreateUser() throws Exception { + + // Given: the server has been configured with FileUserGroupProvider, which is configurable, + // and: the initial admin client wants to create a tenant + Long initialVersion = new Long(0); + String clientId = UUID.randomUUID().toString(); + RevisionInfo revisionInfo = new RevisionInfo(clientId, initialVersion); + + Tenant tenant = new Tenant(); + tenant.setIdentity("New User"); + tenant.setRevision(revisionInfo); + + // When: the POST /tenants/users endpoint is accessed + final Response createUserResponse = client + .target(createURL("tenants/users")) + .request() + .post(Entity.entity(tenant, MediaType.APPLICATION_JSON_TYPE), Response.class); + + // Then: "201 created" is returned with the expected user + assertEquals(201, createUserResponse.getStatus()); + User actualUser = createUserResponse.readEntity(User.class); + assertNotNull(actualUser.getIdentifier()); + assertNotNull(actualUser.getRevision()); + assertNotNull(actualUser.getRevision().getVersion()); + + try { + assertEquals(tenant.getIdentity(), actualUser.getIdentity()); + assertEquals(true, actualUser.getConfigurable()); + assertEquals(0, actualUser.getUserGroups().size()); + assertEquals(0, actualUser.getAccessPolicies().size()); + assertEquals(new ResourcePermissions(), actualUser.getResourcePermissions()); + } finally { + // cleanup user for other tests + final long version = actualUser.getRevision().getVersion(); + client.target(createURL("tenants/users/" + actualUser.getIdentifier() + "?version=" + version)) + .request() + .delete(); + } + + } + + @Test + public void testCreateUserGroup() throws Exception { + + // Given: the server has been configured with FileUserGroupProvider, which is configurable, + // and: the initial admin client wants to create a tenant + Long initialVersion = new Long(0); + String clientId = UUID.randomUUID().toString(); + RevisionInfo revisionInfo = new RevisionInfo(clientId, initialVersion); + + Tenant tenant = new Tenant(); + tenant.setIdentity("New Group"); + tenant.setRevision(revisionInfo); + + // When: the POST /tenants/user-groups endpoint is used + final Response createUserGroupResponse = client + .target(createURL("tenants/user-groups")) + .request() + .post(Entity.entity(tenant, MediaType.APPLICATION_JSON_TYPE), Response.class); + + // Then: 201 created is returned with the expected group + assertEquals(201, createUserGroupResponse.getStatus()); + UserGroup actualUserGroup = createUserGroupResponse.readEntity(UserGroup.class); + assertNotNull(actualUserGroup.getIdentifier()); + assertNotNull(actualUserGroup.getRevision()); + assertNotNull(actualUserGroup.getRevision().getVersion()); + + try { + assertEquals(tenant.getIdentity(), actualUserGroup.getIdentity()); + assertEquals(true, actualUserGroup.getConfigurable()); + assertEquals(0, actualUserGroup.getUsers().size()); + assertEquals(0, actualUserGroup.getAccessPolicies().size()); + assertEquals(new ResourcePermissions(), actualUserGroup.getResourcePermissions()); + } finally { + // cleanup user for other tests + final long version = actualUserGroup.getRevision().getVersion(); + client.target(createURL("tenants/user-groups/" + actualUserGroup.getIdentifier() + "?version=" + version)) + .request() + .delete(); + } + + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureITClientConfiguration.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureITClientConfiguration.java new file mode 100644 index 0000000000..ab07a0875f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureITClientConfiguration.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.api; + +import org.apache.nifi.registry.client.NiFiRegistryClientConfig; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.apache.nifi.registry.security.util.KeystoreType; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import static org.apache.nifi.registry.web.api.IntegrationTestBase.loadNiFiRegistryProperties; + +// Do not add Spring annotations that would cause this class to be picked up by a ComponentScan. It must be imported manually. +public class SecureITClientConfiguration { + + @Value("${nifi.registry.client.properties.file}") + String clientPropertiesFileLocation; + + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + private final Lock readLock = lock.readLock(); + private NiFiRegistryClientConfig clientConfig; + + @Bean + public NiFiRegistryClientConfig getNiFiRegistryClientConfig() { + readLock.lock(); + try { + if (clientConfig == null) { + final NiFiRegistryProperties clientProperties = loadNiFiRegistryProperties(clientPropertiesFileLocation); + clientConfig = createNiFiRegistryClientConfig(clientProperties); + } + } finally { + readLock.unlock(); + } + return clientConfig; + } + + /** + * A helper method for loading a NiFiRegistryClientConfig corresponding to a NiFiRegistryProperties object + * holding the values needed to create a client configuration context. + * + * @param clientProperties A NiFiRegistryProperties object holding the config for client keystore, truststore, etc. + * @return A NiFiRegistryClientConfig instance based on the properties file contents + */ + private static NiFiRegistryClientConfig createNiFiRegistryClientConfig(NiFiRegistryProperties clientProperties) { + + NiFiRegistryClientConfig.Builder configBuilder = new NiFiRegistryClientConfig.Builder(); + + // load keystore/truststore if applicable + if (clientProperties.getKeyStorePath() != null) { + configBuilder.keystoreFilename(clientProperties.getKeyStorePath()); + } + if (clientProperties.getKeyStoreType() != null) { + configBuilder.keystoreType(KeystoreType.valueOf(clientProperties.getKeyStoreType())); + } + if (clientProperties.getKeyStorePassword() != null) { + configBuilder.keystorePassword(clientProperties.getKeyStorePassword()); + } + if (clientProperties.getKeyPassword() != null) { + configBuilder.keyPassword(clientProperties.getKeyPassword()); + } + if (clientProperties.getTrustStorePath() != null) { + configBuilder.truststoreFilename(clientProperties.getTrustStorePath()); + } + if (clientProperties.getTrustStoreType() != null) { + configBuilder.truststoreType(KeystoreType.valueOf(clientProperties.getTrustStoreType())); + } + if (clientProperties.getTrustStorePassword() != null) { + configBuilder.truststorePassword(clientProperties.getTrustStorePassword()); + } + + return configBuilder.build(); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureKerberosIT.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureKerberosIT.java new file mode 100644 index 0000000000..de87fcf1a9 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureKerberosIT.java @@ -0,0 +1,216 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.api; + + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.NiFiRegistryTestApiApplication; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.skyscreamer.jsonassert.JSONAssert; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Profile; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.kerberos.authentication.KerberosTicketValidation; +import org.springframework.security.kerberos.authentication.KerberosTicketValidator; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.junit4.SpringRunner; + +import javax.ws.rs.core.Response; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Base64; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Deploy the Web API Application using an embedded Jetty Server for local integration testing, with the follow characteristics: + * + * - A NiFiRegistryProperties has to be explicitly provided to the ApplicationContext using a profile unique to this test suite. + * - A NiFiRegistryClientConfig has been configured to create a client capable of completing one-way TLS + * - The database is embed H2 using volatile (in-memory) persistence + * - Custom SQL is clearing the DB before each test method by default, unless method overrides this behavior + */ +@RunWith(SpringRunner.class) +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = "spring.profiles.include=ITSecureKerberos") +@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:db/clearDB.sql") +public class SecureKerberosIT extends IntegrationTestBase { + + private static final String validKerberosTicket = "authenticate_me"; + private static final String invalidKerberosTicket = "do_not_authenticate_me"; + + public static class MockKerberosTicketValidator implements KerberosTicketValidator { + + @Override + public KerberosTicketValidation validateTicket(byte[] token) throws BadCredentialsException { + + boolean validTicket; + try { + validTicket = Arrays.equals(validKerberosTicket.getBytes("UTF-8"), token); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } + + if (!validTicket) { + throw new BadCredentialsException(MockKerberosTicketValidator.class.getSimpleName() + " does not validate token"); + } + + return new KerberosTicketValidation( + "kerberosUser@LOCALHOST", + "HTTP/localhsot@LOCALHOST", + null, + null); + } + } + + @Configuration + @Profile("ITSecureKerberos") + @Import({NiFiRegistryTestApiApplication.class, SecureITClientConfiguration.class}) + public static class KerberosSpnegoTestConfiguration { + + @Primary + @Bean + public static KerberosTicketValidator kerberosTicketValidator() { + return new MockKerberosTicketValidator(); + } + + } + + private String adminAuthToken; + + @Before + public void generateAuthToken() { + String validTicket = new String(Base64.getEncoder().encode(validKerberosTicket.getBytes(Charset.forName("UTF-8")))); + final String token = client + .target(createURL("/access/token/kerberos")) + .request() + .header("Authorization", "Negotiate " + validTicket) + .post(null, String.class); + adminAuthToken = token; + } + + @Test + public void testTokenGenerationAndAccessStatus() throws Exception { + + // Note: this test intentionally does not use the token generated + // for nifiadmin by the @Before method + + // Given: the client and server have been configured correctly for Kerberos SPNEGO authentication + String expectedJwtPayloadJson = "{" + + "\"sub\":\"kerberosUser@LOCALHOST\"," + + "\"preferred_username\":\"kerberosUser@LOCALHOST\"," + + "\"iss\":\"KerberosSpnegoIdentityProvider\"" + + "}"; + String expectedAccessStatusJson = "{" + + "\"identity\":\"kerberosUser@LOCALHOST\"," + + "\"anonymous\":false}"; + + // When: the /access/token/kerberos endpoint is accessed with no credentials + final Response tokenResponse1 = client + .target(createURL("/access/token/kerberos")) + .request() + .post(null, Response.class); + + // Then: the server returns 401 Unauthorized with an authenticate challenge header + assertEquals(401, tokenResponse1.getStatus()); + assertNotNull(tokenResponse1.getHeaders().get("www-authenticate")); + assertEquals(1, tokenResponse1.getHeaders().get("www-authenticate").size()); + assertEquals("Negotiate", tokenResponse1.getHeaders().get("www-authenticate").get(0)); + + // When: the /access/token/kerberos endpoint is accessed again with an invalid ticket + String invalidTicket = new String(java.util.Base64.getEncoder().encode(invalidKerberosTicket.getBytes(Charset.forName("UTF-8")))); + final Response tokenResponse2 = client + .target(createURL("/access/token/kerberos")) + .request() + .header("Authorization", "Negotiate " + invalidTicket) + .post(null, Response.class); + + // Then: the server returns 401 Unauthorized + assertEquals(401, tokenResponse2.getStatus()); + + // When: the /access/token/kerberos endpoint is accessed with a valid ticket + String validTicket = new String(Base64.getEncoder().encode(validKerberosTicket.getBytes(Charset.forName("UTF-8")))); + final Response tokenResponse3 = client + .target(createURL("/access/token/kerberos")) + .request() + .header("Authorization", "Negotiate " + validTicket) + .post(null, Response.class); + + // Then: the server returns 200 OK with a JWT in the body + assertEquals(201, tokenResponse3.getStatus()); + String token = tokenResponse3.readEntity(String.class); + assertTrue(StringUtils.isNotEmpty(token)); + String[] jwtParts = token.split("\\."); + assertEquals(3, jwtParts.length); + String jwtPayload = new String(Base64.getDecoder().decode(jwtParts[1]), "UTF-8"); + JSONAssert.assertEquals(expectedJwtPayloadJson, jwtPayload, false); + + // When: the token is returned in the Authorization header + final Response accessResponse = client + .target(createURL("access")) + .request() + .header("Authorization", "Bearer " + token) + .get(Response.class); + + // Then: the server acknowledges the client has access + assertEquals(200, accessResponse.getStatus()); + String accessStatus = accessResponse.readEntity(String.class); + JSONAssert.assertEquals(expectedAccessStatusJson, accessStatus, false); + + } + + @Test + public void testGetCurrentUser() throws Exception { + + // Given: the client is connected to an unsecured NiFi Registry + String expectedJson = "{" + + "\"identity\":\"kerberosUser@LOCALHOST\"," + + "\"anonymous\":false," + + "\"resourcePermissions\":{" + + "\"anyTopLevelResource\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + + "\"buckets\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + + "\"tenants\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + + "\"policies\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + + "\"proxy\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}}" + + "}"; + + // When: the /access endpoint is queried using a JWT for the kerberos user + final Response response = client + .target(createURL("/access")) + .request() + .header("Authorization", "Bearer " + adminAuthToken) + .get(Response.class); + + // Then: the server returns a 200 OK with the expected current user + assertEquals(200, response.getStatus()); + String actualJson = response.readEntity(String.class); + JSONAssert.assertEquals(expectedJson, actualJson, false); + + } + + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java new file mode 100644 index 0000000000..f6d12483a5 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java @@ -0,0 +1,813 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.api; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.SecureLdapTestApiApplication; +import org.apache.nifi.registry.authorization.AccessPolicy; +import org.apache.nifi.registry.authorization.AccessPolicySummary; +import org.apache.nifi.registry.authorization.CurrentUser; +import org.apache.nifi.registry.authorization.Permissions; +import org.apache.nifi.registry.authorization.Tenant; +import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.client.AccessClient; +import org.apache.nifi.registry.client.NiFiRegistryClient; +import org.apache.nifi.registry.client.NiFiRegistryClientConfig; +import org.apache.nifi.registry.client.NiFiRegistryException; +import org.apache.nifi.registry.client.RequestConfig; +import org.apache.nifi.registry.client.UserClient; +import org.apache.nifi.registry.client.impl.JerseyNiFiRegistryClient; +import org.apache.nifi.registry.client.impl.request.BearerTokenRequestConfig; +import org.apache.nifi.registry.extension.ExtensionManager; +import org.apache.nifi.registry.properties.AESSensitivePropertyProvider; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.apache.nifi.registry.properties.SensitivePropertyProvider; +import org.apache.nifi.registry.revision.entity.RevisionInfo; +import org.apache.nifi.registry.security.authorization.Authorizer; +import org.apache.nifi.registry.security.authorization.AuthorizerFactory; +import org.apache.nifi.registry.security.crypto.BootstrapFileCryptoKeyProvider; +import org.apache.nifi.registry.security.crypto.CryptoKeyProvider; +import org.apache.nifi.registry.security.identity.IdentityMapper; +import org.apache.nifi.registry.service.RegistryService; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.skyscreamer.jsonassert.JSONAssert; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.DependsOn; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Profile; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.junit4.SpringRunner; + +import javax.sql.DataSource; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Deploy the Web API Application using an embedded Jetty Server for local integration testing, with the follow characteristics: + * + * - A NiFiRegistryProperties has to be explicitly provided to the ApplicationContext using a profile unique to this test suite. + * - A NiFiRegistryClientConfig has been configured to create a client capable of completing one-way TLS + * - The database is embed H2 using volatile (in-memory) persistence + * - Custom SQL is clearing the DB before each test method by default, unless method overrides this behavior + */ +@RunWith(SpringRunner.class) +@SpringBootTest( + classes = SecureLdapTestApiApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = "spring.profiles.include=ITSecureLdap") +@Import(SecureITClientConfiguration.class) +@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:db/clearDB.sql") +public class SecureLdapIT extends IntegrationTestBase { + + private static Logger LOGGER = LoggerFactory.getLogger(SecureLdapIT.class); + + private static final String tokenLoginPath = "access/token/login"; + private static final String tokenIdentityProviderPath = "access/token/identity-provider"; + // A JWT signed by a key of 'secret' + private static final String SIGNED_BY_WRONG_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" + + ".eyJzdWIiOiJuaWZpYWRtaW4iLCJpc3MiOiJMZGFwSWRlbnRpdHlQcm92aWRlciIsImF1ZCI6IkxkYXB" + + "JZGVudGl0eVByb3ZpZGVyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoibmlmaWFkbWluIiwia2lkIjoiNDd" + + "lMjA1NzctY2I3Yi00M2MzLWFhOGYtZjI0ZDcyODQ3MDEwIiwiaWF0IjoxNTgxNTI5NTA1LCJleHAiOjE" + + "1ODE1NzI3MDV9.vvMpwLJt1w_6Id_tlS1knxTkJ2gv7_j5ySG6PmNjF0s"; + + @TestConfiguration + @Profile("ITSecureLdap") + public static class LdapTestConfiguration { + + static AuthorizerFactory authorizerFactory; + + @Primary + @Bean + @DependsOn({"directoryServer"}) // Can't load LdapUserGroupProvider until the embedded LDAP server, which creates the "directoryServer" bean, is running + public static Authorizer getAuthorizer( + @Autowired NiFiRegistryProperties properties, + ExtensionManager extensionManager, + RegistryService registryService, + DataSource dataSource, + IdentityMapper identityMapper) throws Exception { + + if (authorizerFactory == null) { + authorizerFactory = new AuthorizerFactory( + properties, + extensionManager, + sensitivePropertyProvider(), + registryService, + dataSource, + identityMapper); + } + return authorizerFactory.getAuthorizer(); + } + + @Primary + @Bean + public static SensitivePropertyProvider sensitivePropertyProvider() throws Exception { + return new AESSensitivePropertyProvider(getNiFiRegistryMasterKeyProvider().getKey()); + } + + private static CryptoKeyProvider getNiFiRegistryMasterKeyProvider() { + return new BootstrapFileCryptoKeyProvider("src/test/resources/conf/secure-ldap/bootstrap.conf"); + } + + } + + private String adminAuthToken; + private List beforeTestAccessPoliciesSnapshot; + + @Before + public void setup() { + final String basicAuthCredentials = encodeCredentialsForBasicAuth("nifiadmin", "password"); + final String token = client + .target(createURL(tokenIdentityProviderPath)) + .request() + .header("Authorization", "Basic " + basicAuthCredentials) + .post(null, String.class); + adminAuthToken = token; + + beforeTestAccessPoliciesSnapshot = createAccessPoliciesSnapshot(); + } + + @After + public void cleanup() { + restoreAccessPoliciesSnapshot(beforeTestAccessPoliciesSnapshot); + } + + @Test + public void testTokenGenerationAndAccessStatus() throws Exception { + + // Note: this test intentionally does not use the token generated + // for nifiadmin by the @Before method + + // Given: the client and server have been configured correctly for LDAP authentication + String expectedJwtPayloadJson = "{" + + "\"sub\":\"nobel\"," + + "\"preferred_username\":\"nobel\"," + + "\"iss\":\"LdapIdentityProvider\"" + + "}"; + String expectedAccessStatusJson = "{" + + "\"identity\":\"nobel\"," + + "\"anonymous\":false" + + "}"; + + // When: the /access/token/login endpoint is queried + final String basicAuthCredentials = encodeCredentialsForBasicAuth("nobel", "password"); + final Response tokenResponse = client + .target(createURL(tokenIdentityProviderPath)) + .request() + .header("Authorization", "Basic " + basicAuthCredentials) + .post(null, Response.class); + + // Then: the server returns 200 OK with an access token + assertEquals(201, tokenResponse.getStatus()); + String token = tokenResponse.readEntity(String.class); + assertTrue(StringUtils.isNotEmpty(token)); + String[] jwtParts = token.split("\\."); + assertEquals(3, jwtParts.length); + String jwtPayload = new String(Base64.getDecoder().decode(jwtParts[1]), "UTF-8"); + JSONAssert.assertEquals(expectedJwtPayloadJson, jwtPayload, false); + + // When: the token is returned in the Authorization header + final Response accessResponse = client + .target(createURL("access")) + .request() + .header("Authorization", "Bearer " + token) + .get(Response.class); + + // Then: the server acknowledges the client has access + assertEquals(200, accessResponse.getStatus()); + String accessStatus = accessResponse.readEntity(String.class); + JSONAssert.assertEquals(expectedAccessStatusJson, accessStatus, false); + + } + + @Test + public void testTokenGenerationWithIdentityProvider() throws Exception { + + // Given: the client and server have been configured correctly for LDAP authentication + String expectedJwtPayloadJson = "{" + + "\"sub\":\"nobel\"," + + "\"preferred_username\":\"nobel\"," + + "\"iss\":\"LdapIdentityProvider\"," + + "\"aud\":\"LdapIdentityProvider\"" + + "}"; + String expectedAccessStatusJson = "{" + + "\"identity\":\"nobel\"," + + "\"anonymous\":false" + + "}"; + + // When: the /access/token/identity-provider endpoint is queried + final String basicAuthCredentials = encodeCredentialsForBasicAuth("nobel", "password"); + final Response tokenResponse = client + .target(createURL(tokenIdentityProviderPath)) + .request() + .header("Authorization", "Basic " + basicAuthCredentials) + .post(null, Response.class); + + // Then: the server returns 200 OK with an access token + assertEquals(201, tokenResponse.getStatus()); + String token = tokenResponse.readEntity(String.class); + assertTrue(StringUtils.isNotEmpty(token)); + String[] jwtParts = token.split("\\."); + assertEquals(3, jwtParts.length); + String jwtPayload = new String(Base64.getDecoder().decode(jwtParts[1]), "UTF-8"); + JSONAssert.assertEquals(expectedJwtPayloadJson, jwtPayload, false); + + // When: the token is returned in the Authorization header + final Response accessResponse = client + .target(createURL("access")) + .request() + .header("Authorization", "Bearer " + token) + .get(Response.class); + + // Then: the server acknowledges the client has access + assertEquals(200, accessResponse.getStatus()); + String accessStatus = accessResponse.readEntity(String.class); + JSONAssert.assertEquals(expectedAccessStatusJson, accessStatus, false); + + } + + @Test + public void testGetCurrentUserFailsForAnonymous() throws Exception { + + // Given: the client is connected to an secured NiFi Registry + final String expectedJson = "{" + + "\"anonymous\":true," + + "\"identity\":\"anonymous\"," + + "\"loginSupported\":true," + + "\"resourcePermissions\":{" + + "\"anyTopLevelResource\":{\"canDelete\":false,\"canRead\":false,\"canWrite\":false}," + + "\"buckets\":{\"canDelete\":false,\"canRead\":false,\"canWrite\":false}," + + "\"policies\":{\"canDelete\":false,\"canRead\":false,\"canWrite\":false}," + + "\"proxy\":{\"canDelete\":false,\"canRead\":false,\"canWrite\":false}," + + "\"tenants\":{\"canDelete\":false,\"canRead\":false,\"canWrite\":false}}" + + "}"; + + // When: the /access endpoint is queried with no credentials + final Response response = client + .target(createURL("/access")) + .request() + .get(Response.class); + + // Then: the server returns a 200 OK with the expected current user + assertEquals(200, response.getStatus()); + + final String actualJson = response.readEntity(String.class); + JSONAssert.assertEquals(expectedJson, actualJson, false); + } + + @Test + public void testGetCurrentUser() throws Exception { + + // Given: the client is connected to an unsecured NiFi Registry + String expectedJson = "{" + + "\"identity\":\"nifiadmin\"," + + "\"anonymous\":false," + + "\"resourcePermissions\":{" + + "\"anyTopLevelResource\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + + "\"buckets\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + + "\"tenants\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + + "\"policies\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + + "\"proxy\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}}" + + "}"; + + // When: the /access endpoint is queried using a JWT for the nifiadmin LDAP user + final Response response = client + .target(createURL("/access")) + .request() + .header("Authorization", "Bearer " + adminAuthToken) + .get(Response.class); + + // Then: the server returns a 200 OK with the expected current user + assertEquals(200, response.getStatus()); + String actualJson = response.readEntity(String.class); + JSONAssert.assertEquals(expectedJson, actualJson, false); + + } + + @Test + public void testLogout() { + + // Given: the client is connected to an unsecured NiFi Registry + // and the /access endpoint is queried using a JWT for the nifiadmin LDAP user + final Response response = client + .target(createURL("/access")) + .request() + .header("Authorization", "Bearer " + adminAuthToken) + .get(Response.class); + + // and the server returns a 200 OK with the expected current user + assertEquals(200, response.getStatus()); + + // When: the /access/logout endpoint with the JWT for the nifiadmin logs out the user + final Response logout_response = client + .target(createURL("/access/logout")) + .request() + .header("Authorization", "Bearer " + adminAuthToken) + .delete(Response.class); + + assertEquals(200, logout_response.getStatus()); + + // Then: the /access endpoint is queried using the logged out JWT + LOGGER.info("*** THE FOLLOWING JwtException IS EXPECTED ***"); + LOGGER.info("*** We are validating the access token no longer works following logout ***"); + final Response retryResponse = client + .target(createURL("/access")) + .request() + .header("Authorization", "Bearer " + adminAuthToken) + .get(Response.class); + + // and the server returns a 401 Unauthorized as the user is now logged out + assertEquals(401, retryResponse.getStatus()); + String retryJson = retryResponse.readEntity(String.class); + assertEquals("Unable to validate the access token. Contact the system administrator.\n", retryJson); + + // Reset: We successfully logged out our user. Run setup to fix up the user, so the @After code can run to re-establish authorizations. + setup(); + } + + @Test + public void testLogoutWithJWTSignedByWrongKey() throws Exception { + + // Given: use the /access/logout endpoint with the JWT for the nifiadmin LDAP user to log out + LOGGER.info("*** THE FOLLOWING JwtException IS EXPECTED ***"); + final Response logoutResponse = client + .target(createURL("/access")) + .request() + .header("Authorization", "Bearer " + SIGNED_BY_WRONG_KEY) + .delete(Response.class); + + assertEquals(401, logoutResponse.getStatus()); + String responseMessage = logoutResponse.readEntity(String.class); + assertEquals("Unable to validate the access token. Contact the system administrator.\n", responseMessage); + } + + @Test + public void testUsers() throws Exception { + + // Given: the client and server have been configured correctly for LDAP authentication + String expectedJson = "[" + + "{\"identity\":\"nifiadmin\",\"userGroups\":[],\"configurable\":false," + + "\"resourcePermissions\":{" + + "\"anyTopLevelResource\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + + "\"buckets\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + + "\"tenants\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + + "\"policies\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + + "\"proxy\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}}}," + + "{\"identity\":\"euler\",\"userGroups\":[{\"identity\":\"mathematicians\"}],\"accessPolicies\":[],\"configurable\":false}," + + "{\"identity\":\"euclid\",\"userGroups\":[{\"identity\":\"mathematicians\"}],\"accessPolicies\":[],\"configurable\":false}," + + "{\"identity\":\"boyle\",\"userGroups\":[{\"identity\":\"chemists\"}],\"accessPolicies\":[],\"configurable\":false}," + + "{\"identity\":\"newton\",\"userGroups\":[{\"identity\":\"scientists\"}],\"accessPolicies\":[],\"configurable\":false}," + + "{\"identity\":\"riemann\",\"userGroups\":[{\"identity\":\"mathematicians\"}],\"accessPolicies\":[],\"configurable\":false}," + + "{\"identity\":\"gauss\",\"userGroups\":[{\"identity\":\"mathematicians\"}],\"accessPolicies\":[],\"configurable\":false}," + + "{\"identity\":\"galileo\",\"userGroups\":[{\"identity\":\"scientists\"},{\"identity\":\"italians\"}],\"accessPolicies\":[],\"configurable\":false}," + + "{\"identity\":\"nobel\",\"userGroups\":[{\"identity\":\"chemists\"}],\"accessPolicies\":[],\"configurable\":false}," + + "{\"identity\":\"pasteur\",\"userGroups\":[{\"identity\":\"chemists\"}],\"accessPolicies\":[],\"configurable\":false}," + + "{\"identity\":\"tesla\",\"userGroups\":[{\"identity\":\"scientists\"}],\"accessPolicies\":[],\"configurable\":false}," + + "{\"identity\":\"nogroup\",\"userGroups\":[],\"accessPolicies\":[],\"configurable\":false}," + + "{\"identity\":\"einstein\",\"userGroups\":[{\"identity\":\"scientists\"}],\"accessPolicies\":[],\"configurable\":false}," + + "{\"identity\":\"curie\",\"userGroups\":[{\"identity\":\"chemists\"}],\"accessPolicies\":[],\"configurable\":false}]"; + + // When: the /tenants/users endpoint is queried + final String usersJson = client + .target(createURL("tenants/users")) + .request() + .header("Authorization", "Bearer " + adminAuthToken) + .get(String.class); + + // Then: the server returns a list of all users (see test-ldap-data.ldif) + JSONAssert.assertEquals(expectedJson, usersJson, false); + } + + @Test + public void testUserGroups() throws Exception { + + // Given: the client and server have been configured correctly for LDAP authentication + String expectedJson = "[" + + "{" + + "\"identity\":\"chemists\"," + + "\"users\":[{\"identity\":\"pasteur\"},{\"identity\":\"boyle\"},{\"identity\":\"curie\"},{\"identity\":\"nobel\"}]," + + "\"accessPolicies\":[]," + + "\"configurable\":false" + + "}," + + "{" + + "\"identity\":\"mathematicians\"," + + "\"users\":[{\"identity\":\"gauss\"},{\"identity\":\"euclid\"},{\"identity\":\"riemann\"},{\"identity\":\"euler\"}]," + + "\"accessPolicies\":[]," + + "\"configurable\":false" + + "}," + + "{" + + "\"identity\":\"scientists\"," + + "\"users\":[{\"identity\":\"einstein\"},{\"identity\":\"tesla\"},{\"identity\":\"newton\"},{\"identity\":\"galileo\"}]," + + "\"accessPolicies\":[]," + + "\"configurable\":false" + + "}," + + "{" + + "\"identity\":\"italians\"," + + "\"users\":[{\"identity\":\"galileo\"}]," + + "\"accessPolicies\":[]," + + "\"configurable\":false" + + "}]"; + + // When: the /tenants/users endpoint is queried + final String groupsJson = client + .target(createURL("tenants/user-groups")) + .request() + .header("Authorization", "Bearer " + adminAuthToken) + .get(String.class); + + // Then: the server returns a list of all users (see test-ldap-data.ldif) + JSONAssert.assertEquals(expectedJson, groupsJson, false); + } + + @Test + public void testCreateTenantFails() throws Exception { + Long initialVersion = new Long(0); + String clientId = UUID.randomUUID().toString(); + RevisionInfo initialRevisionInfo = new RevisionInfo(clientId, initialVersion); + + // Given: the server has been configured with the LdapUserGroupProvider, which is non-configurable, + // and: the client wants to create a tenant + Tenant tenant = new Tenant(); + tenant.setIdentity("new_tenant"); + tenant.setRevision(initialRevisionInfo); + + // When: the POST /tenants/users endpoint is accessed + final Response createUserResponse = client + .target(createURL("tenants/users")) + .request() + .header("Authorization", "Bearer " + adminAuthToken) + .post(Entity.entity(tenant, MediaType.APPLICATION_JSON_TYPE), Response.class); + + // Then: an error is returned + assertEquals(409, createUserResponse.getStatus()); + + // When: the POST /tenants/users endpoint is accessed + final Response createUserGroupResponse = client + .target(createURL("tenants/user-groups")) + .request() + .header("Authorization", "Bearer " + adminAuthToken) + .post(Entity.entity(tenant, MediaType.APPLICATION_JSON_TYPE), Response.class); + + // Then: an error is returned because the UserGroupProvider is non-configurable + assertEquals(409, createUserGroupResponse.getStatus()); + } + + @Test + public void testAccessPolicyCreation() throws Exception { + Long initialVersion = new Long(0); + String clientId = UUID.randomUUID().toString(); + RevisionInfo initialRevisionInfo = new RevisionInfo(clientId, initialVersion); + + // Given: the server has been configured with an initial admin "nifiadmin" and a user with no accessPolicies "nobel" + String nobelId = getTenantIdentifierByIdentity("nobel"); + String chemistsId = getTenantIdentifierByIdentity("chemists"); // a group containing user "nobel" + + final String basicAuthCredentials = encodeCredentialsForBasicAuth("nobel", "password"); + final String nobelAuthToken = client + .target(createURL(tokenIdentityProviderPath)) + .request() + .header("Authorization", "Basic " + basicAuthCredentials) + .post(null, String.class); + + // When: user nobel re-checks top-level permissions + final CurrentUser currentUser = client + .target(createURL("/access")) + .request() + .header("Authorization", "Bearer " + nobelAuthToken) + .get(CurrentUser.class); + + // Then: 200 OK is returned indicating user has access to no top-level resources + assertEquals(new Permissions(), currentUser.getResourcePermissions().getBuckets()); + assertEquals(new Permissions(), currentUser.getResourcePermissions().getTenants()); + assertEquals(new Permissions(), currentUser.getResourcePermissions().getPolicies()); + assertEquals(new Permissions(), currentUser.getResourcePermissions().getProxy()); + + // When: nifiadmin creates a bucket + final Bucket bucket = new Bucket(); + bucket.setName("Integration Test Bucket"); + bucket.setDescription("A bucket created by an integration test."); + bucket.setRevision(new RevisionInfo(null, 0L)); + + Response adminCreatesBucketResponse = client + .target(createURL("buckets")) + .request() + .header("Authorization", "Bearer " + adminAuthToken) + .post(Entity.entity(bucket, MediaType.APPLICATION_JSON), Response.class); + + // Then: the server returns a 200 OK + assertEquals(200, adminCreatesBucketResponse.getStatus()); + Bucket createdBucket = adminCreatesBucketResponse.readEntity(Bucket.class); + + + // When: user nobel initial queries /buckets + final Bucket[] buckets1 = client + .target(createURL("buckets")) + .request() + .header("Authorization", "Bearer " + nobelAuthToken) + .get(Bucket[].class); + + // Then: an empty list is returned (nobel has no read access yet) + assertNotNull(buckets1); + assertEquals(0, buckets1.length); + + + // When: nifiadmin grants read access on createdBucket to 'chemists' a group containing nobel + AccessPolicy readPolicy = new AccessPolicy(); + readPolicy.setResource("/buckets/" + createdBucket.getIdentifier()); + readPolicy.setAction("read"); + readPolicy.addUserGroups(Arrays.asList(new Tenant(chemistsId, "chemists"))); + readPolicy.setRevision(initialRevisionInfo); + + Response adminGrantsReadAccessResponse = client + .target(createURL("policies")) + .request() + .header("Authorization", "Bearer " + adminAuthToken) + .post(Entity.entity(readPolicy, MediaType.APPLICATION_JSON), Response.class); + + // Then: the server returns a 201 Created + assertEquals(201, adminGrantsReadAccessResponse.getStatus()); + + + // When: nifiadmin tries to list all buckets + final Bucket[] adminBuckets = client + .target(createURL("buckets")) + .request() + .header("Authorization", "Bearer " + adminAuthToken) + .get(Bucket[].class); + + // Then: the full list is returned (verifies that per-bucket access policies are additive to base /buckets policy) + assertNotNull(adminBuckets); + assertEquals(1, adminBuckets.length); + assertEquals(createdBucket.getIdentifier(), adminBuckets[0].getIdentifier()); + assertEquals(new Permissions().withCanRead(true).withCanWrite(true).withCanDelete(true), adminBuckets[0].getPermissions()); + + + // When: user nobel re-queries /buckets + final Bucket[] buckets2 = client + .target(createURL("buckets")) + .request() + .header("Authorization", "Bearer " + nobelAuthToken) + .get(Bucket[].class); + + // Then: the created bucket is now present + assertNotNull(buckets2); + assertEquals(1, buckets2.length); + assertEquals(createdBucket.getIdentifier(), buckets2[0].getIdentifier()); + assertEquals(new Permissions().withCanRead(true), buckets2[0].getPermissions()); + + + // When: nifiadmin grants write access on createdBucket to user 'nobel' + AccessPolicy writePolicy = new AccessPolicy(); + writePolicy.setResource("/buckets/" + createdBucket.getIdentifier()); + writePolicy.setAction("write"); + writePolicy.addUsers(Arrays.asList(new Tenant(nobelId, "nobel"))); + writePolicy.setRevision(initialRevisionInfo); + + Response adminGrantsWriteAccessResponse = client + .target(createURL("policies")) + .request() + .header("Authorization", "Bearer " + adminAuthToken) + .post(Entity.entity(writePolicy, MediaType.APPLICATION_JSON), Response.class); + + // Then: the server returns a 201 Created + assertEquals(201, adminGrantsWriteAccessResponse.getStatus()); + + + // When: user nobel re-queries /buckets + final Bucket[] buckets3 = client + .target(createURL("buckets")) + .request() + .header("Authorization", "Bearer " + nobelAuthToken) + .get(Bucket[].class); + + // Then: the authorizedActions are updated + assertNotNull(buckets3); + assertEquals(1, buckets3.length); + assertEquals(createdBucket.getIdentifier(), buckets3[0].getIdentifier()); + assertEquals(new Permissions().withCanRead(true).withCanWrite(true), buckets3[0].getPermissions()); + + } + + @Test + public void testAccessClient() throws IOException, NiFiRegistryException { + final String baseUrl = createBaseURL(); + LOGGER.info("Using base url = " + baseUrl); + + final NiFiRegistryClientConfig clientConfig = createClientConfig(baseUrl); + Assert.assertNotNull(clientConfig); + + final NiFiRegistryClient client = new JerseyNiFiRegistryClient.Builder() + .config(clientConfig) + .build(); + + final String username = "pasteur"; + final String password = "password"; + + // authenticate with the username and password to obtain a token + final AccessClient accessClient = client.getAccessClient(); + final String token = accessClient.getToken(username, password); + assertNotNull(token); + + // use the token to check the status of the current user + final RequestConfig requestConfig = new BearerTokenRequestConfig(token); + final UserClient userClient = client.getUserClient(requestConfig); + assertEquals(username, userClient.getAccessStatus().getIdentity()); + + // use the token to logout + accessClient.logout(token); + + // check the status of the current user again and should be unauthorized + LOGGER.info("*** THE FOLLOWING JwtException IS EXPECTED ***"); + LOGGER.info("*** We are validating the access token no longer works following logout ***"); + try { + userClient.getAccessStatus(); + Assert.fail("Should have failed with an unauthorized exception"); + } catch (Exception e) { + //LOGGER.error(e.getMessage(), e); + } + + // try to get a token with an invalid username and password + try { + accessClient.getToken("user-does-not-exist", "bad-password"); + Assert.fail("Should have failed with an unauthorized exception"); + } catch (Exception e) { + + } + } + + /** A helper method to lookup identifiers for tenant identities using the REST API + * + * @param tenantIdentity - the identity to lookup + * @return A string containing the identifier of the tenant, or null if the tenant identity is not found. + */ + private String getTenantIdentifierByIdentity(String tenantIdentity) { + + final Tenant[] users = client + .target(createURL("tenants/users")) + .request() + .header("Authorization", "Bearer " + adminAuthToken) + .get(Tenant[].class); + + final Tenant[] groups = client + .target(createURL("tenants/user-groups")) + .request() + .header("Authorization", "Bearer " + adminAuthToken) + .get(Tenant[].class); + + final Tenant matchedTenant = Stream.concat(Arrays.stream(users), Arrays.stream(groups)) + .filter(tenant -> tenant.getIdentity().equalsIgnoreCase(tenantIdentity)) + .findFirst() + .orElse(null); + + return matchedTenant != null ? matchedTenant.getIdentifier() : null; + } + + /** A helper method to lookup access policies + * + * @return A string containing the identifier of the policy, or null if the policy identity is not found. + */ + private AccessPolicy getPolicyByResourceAction(String action, String resource) { + + final AccessPolicySummary[] policies = client + .target(createURL("policies")) + .request() + .header("Authorization", "Bearer " + adminAuthToken) + .get(AccessPolicySummary[].class); + + final AccessPolicySummary matchedPolicy = Arrays.stream(policies) + .filter(p -> p.getAction().equalsIgnoreCase(action) && p.getResource().equalsIgnoreCase(resource)) + .findFirst() + .orElse(null); + + if (matchedPolicy == null) { + return null; + } + + String policyId = matchedPolicy.getIdentifier(); + + final AccessPolicy policy = client + .target(createURL("policies/" + policyId)) + .request() + .header("Authorization", "Bearer " + adminAuthToken) + .get(AccessPolicy.class); + + return policy; + } + + private List createAccessPoliciesSnapshot() { + + final AccessPolicySummary[] policySummaries = client + .target(createURL("policies")) + .request() + .header("Authorization", "Bearer " + adminAuthToken) + .get(AccessPolicySummary[].class); + + final List policies = new ArrayList<>(policySummaries.length); + for (AccessPolicySummary s : policySummaries) { + AccessPolicy policy = client + .target(createURL("policies/" + s.getIdentifier())) + .request() + .header("Authorization", "Bearer " + adminAuthToken) + .get(AccessPolicy.class); + policies.add(policy); + } + + return policies; + } + + private void restoreAccessPoliciesSnapshot(List accessPoliciesSnapshot) { + + List currentAccessPolicies = createAccessPoliciesSnapshot(); + + Set policiesToRestore = accessPoliciesSnapshot.stream() + .map(AccessPolicy::getIdentifier) + .collect(Collectors.toSet()); + + Set policiesToDelete = currentAccessPolicies.stream() + .filter(p -> !policiesToRestore.contains(p.getIdentifier())) + .collect(Collectors.toSet()); + + for (AccessPolicy originalPolicy : accessPoliciesSnapshot) { + + Response getCurrentPolicy = client + .target(createURL("policies/" + originalPolicy.getIdentifier())) + .request() + .header("Authorization", "Bearer " + adminAuthToken) + .get(Response.class); + + if (getCurrentPolicy.getStatus() == 200) { + // update policy to match original + client.target(createURL("policies/" + originalPolicy.getIdentifier())) + .request() + .header("Authorization", "Bearer " + adminAuthToken) + .put(Entity.entity(originalPolicy, MediaType.APPLICATION_JSON)); + } else { + // post the original policy + client.target(createURL("policies")) + .request() + .header("Authorization", "Bearer " + adminAuthToken) + .post(Entity.entity(originalPolicy, MediaType.APPLICATION_JSON)); + } + + } + + for (AccessPolicy policyToDelete : policiesToDelete) { + try { + final RevisionInfo revisionInfo = policyToDelete.getRevision(); + final Long version = revisionInfo == null ? 0 : revisionInfo.getVersion(); + client.target(createURL("policies/" + policyToDelete.getIdentifier())) + .queryParam("version", version.longValue()) + .request() + .header("Authorization", "Bearer " + adminAuthToken) + .delete(); + } catch (Exception e) { + LOGGER.error("Error cleaning up policies after test due to: " + e.getMessage(), e); + } + } + + } + + private static String encodeCredentialsForBasicAuth(String username, String password) { + final String credentials = username + ":" + password; + final String base64credentials = new String(Base64.getEncoder().encode(credentials.getBytes(Charset.forName("UTF-8")))); + return base64credentials; + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureNiFiRegistryClientIT.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureNiFiRegistryClientIT.java new file mode 100644 index 0000000000..140183810c --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureNiFiRegistryClientIT.java @@ -0,0 +1,314 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.api; + +import org.apache.nifi.registry.NiFiRegistryTestApiApplication; +import org.apache.nifi.registry.authorization.CurrentUser; +import org.apache.nifi.registry.authorization.Permissions; +import org.apache.nifi.registry.authorization.User; +import org.apache.nifi.registry.authorization.UserGroup; +import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.client.BucketClient; +import org.apache.nifi.registry.client.FlowClient; +import org.apache.nifi.registry.client.FlowSnapshotClient; +import org.apache.nifi.registry.client.NiFiRegistryClient; +import org.apache.nifi.registry.client.NiFiRegistryClientConfig; +import org.apache.nifi.registry.client.NiFiRegistryException; +import org.apache.nifi.registry.client.RequestConfig; +import org.apache.nifi.registry.client.TenantsClient; +import org.apache.nifi.registry.client.UserClient; +import org.apache.nifi.registry.client.impl.JerseyNiFiRegistryClient; +import org.apache.nifi.registry.client.impl.request.ProxiedEntityRequestConfig; +import org.apache.nifi.registry.flow.VersionedFlow; +import org.apache.nifi.registry.flow.VersionedFlowSnapshot; +import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata; +import org.apache.nifi.registry.flow.VersionedProcessGroup; +import org.apache.nifi.registry.revision.entity.RevisionInfo; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.junit4.SpringRunner; + +import javax.ws.rs.ForbiddenException; +import java.io.IOException; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +@RunWith(SpringRunner.class) +@SpringBootTest( + classes = NiFiRegistryTestApiApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = "spring.profiles.include=ITSecureFile") +@Import(SecureITClientConfiguration.class) +@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = {"classpath:db/clearDB.sql", "classpath:db/FlowsIT.sql"}) +public class SecureNiFiRegistryClientIT extends IntegrationTestBase { + + static final Logger LOGGER = LoggerFactory.getLogger(SecureNiFiRegistryClientIT.class); + + static final String INITIAL_ADMIN_IDENTITY = "CN=user1, OU=nifi"; + static final String NO_ACCESS_IDENTITY = "CN=no-access, OU=nifi"; + + private NiFiRegistryClient client; + + @Before + public void setup() { + final String baseUrl = createBaseURL(); + LOGGER.info("Using base url = " + baseUrl); + + final NiFiRegistryClientConfig clientConfig = createClientConfig(baseUrl); + Assert.assertNotNull(clientConfig); + + final NiFiRegistryClient client = new JerseyNiFiRegistryClient.Builder() + .config(clientConfig) + .build(); + Assert.assertNotNull(client); + this.client = client; + } + + @After + public void teardown() { + try { + client.close(); + } catch (Exception e) { + + } + } + + @Test + public void testGetAccessStatus() throws IOException, NiFiRegistryException { + final UserClient userClient = client.getUserClient(); + final CurrentUser currentUser = userClient.getAccessStatus(); + assertEquals(INITIAL_ADMIN_IDENTITY, currentUser.getIdentity()); + assertFalse(currentUser.isAnonymous()); + assertNotNull(currentUser.getResourcePermissions()); + final Permissions fullAccess = new Permissions().withCanRead(true).withCanWrite(true).withCanDelete(true); + assertEquals(fullAccess, currentUser.getResourcePermissions().getAnyTopLevelResource()); + assertEquals(fullAccess, currentUser.getResourcePermissions().getBuckets()); + assertEquals(fullAccess, currentUser.getResourcePermissions().getTenants()); + assertEquals(fullAccess, currentUser.getResourcePermissions().getPolicies()); + assertEquals(fullAccess, currentUser.getResourcePermissions().getProxy()); + } + + @Test + public void testCrudOperations() throws IOException, NiFiRegistryException { + final Bucket bucket = new Bucket(); + bucket.setName("Bucket 1 " + System.currentTimeMillis()); + bucket.setDescription("This is bucket 1"); + bucket.setRevision(new RevisionInfo(null, 0L)); + + final BucketClient bucketClient = client.getBucketClient(); + final Bucket createdBucket = bucketClient.create(bucket); + assertNotNull(createdBucket); + assertNotNull(createdBucket.getIdentifier()); + assertNotNull(createdBucket.getRevision()); + + final List buckets = bucketClient.getAll(); + Assert.assertEquals(4, buckets.size()); + buckets.forEach(b -> assertNotNull(b.getRevision())); + + final VersionedFlow flow = new VersionedFlow(); + flow.setBucketIdentifier(createdBucket.getIdentifier()); + flow.setName("Flow 1 - " + System.currentTimeMillis()); + flow.setRevision(new RevisionInfo(null, 0L)); + + final FlowClient flowClient = client.getFlowClient(); + final VersionedFlow createdFlow = flowClient.create(flow); + assertNotNull(createdFlow); + assertNotNull(createdFlow.getIdentifier()); + assertNotNull(createdFlow.getRevision()); + + final VersionedFlowSnapshotMetadata snapshotMetadata = new VersionedFlowSnapshotMetadata(); + snapshotMetadata.setBucketIdentifier(createdFlow.getBucketIdentifier()); + snapshotMetadata.setFlowIdentifier(createdFlow.getIdentifier()); + snapshotMetadata.setVersion(1); + snapshotMetadata.setComments("This is snapshot #1"); + + final VersionedProcessGroup rootProcessGroup = new VersionedProcessGroup(); + rootProcessGroup.setIdentifier("root-pg"); + rootProcessGroup.setName("Root Process Group"); + + final VersionedFlowSnapshot snapshot = new VersionedFlowSnapshot(); + snapshot.setSnapshotMetadata(snapshotMetadata); + snapshot.setFlowContents(rootProcessGroup); + + final FlowSnapshotClient snapshotClient = client.getFlowSnapshotClient(); + final VersionedFlowSnapshot createdSnapshot = snapshotClient.create(snapshot); + assertNotNull(createdSnapshot); + assertEquals(INITIAL_ADMIN_IDENTITY, createdSnapshot.getSnapshotMetadata().getAuthor()); + } + + @Test + public void testGetAccessStatusWithProxiedEntity() throws IOException, NiFiRegistryException { + final String proxiedEntity = "user2"; + final RequestConfig requestConfig = new ProxiedEntityRequestConfig(proxiedEntity); + final UserClient userClient = client.getUserClient(requestConfig); + final CurrentUser status = userClient.getAccessStatus(); + assertEquals("user2", status.getIdentity()); + assertFalse(status.isAnonymous()); + } + + @Test + public void testCreatedBucketWithProxiedEntity() throws IOException, NiFiRegistryException { + final String proxiedEntity = "user2"; + final RequestConfig requestConfig = new ProxiedEntityRequestConfig(proxiedEntity); + final BucketClient bucketClient = client.getBucketClient(requestConfig); + + final Bucket bucket = new Bucket(); + bucket.setName("Bucket 1"); + bucket.setDescription("This is bucket 1"); + bucket.setRevision(new RevisionInfo(null, 0L)); + + try { + bucketClient.create(bucket); + fail("Shouldn't have been able to create a bucket"); + } catch (Exception e) { + + } + } + + @Test + public void testDirectFlowAccess() throws IOException { + // this user shouldn't have access to anything + final String proxiedEntity = NO_ACCESS_IDENTITY; + + final RequestConfig requestConfig = new ProxiedEntityRequestConfig(proxiedEntity); + final FlowClient proxiedFlowClient = client.getFlowClient(requestConfig); + final FlowSnapshotClient proxiedFlowSnapshotClient = client.getFlowSnapshotClient(requestConfig); + + try { + proxiedFlowClient.get("1"); + fail("Shouldn't have been able to retrieve flow"); + } catch (NiFiRegistryException e) { + assertTrue(e.getCause() instanceof ForbiddenException); + } + + try { + proxiedFlowSnapshotClient.getLatest("1"); + fail("Shouldn't have been able to retrieve flow"); + } catch (NiFiRegistryException e) { + assertTrue(e.getCause() instanceof ForbiddenException); + } + + try { + proxiedFlowSnapshotClient.getLatestMetadata("1"); + fail("Shouldn't have been able to retrieve flow"); + } catch (NiFiRegistryException e) { + assertTrue(e.getCause() instanceof ForbiddenException); + } + + try { + proxiedFlowSnapshotClient.get("1", 1); + fail("Shouldn't have been able to retrieve flow"); + } catch (NiFiRegistryException e) { + assertTrue(e.getCause() instanceof ForbiddenException); + } + + try { + proxiedFlowSnapshotClient.getSnapshotMetadata("1"); + fail("Shouldn't have been able to retrieve flow"); + } catch (NiFiRegistryException e) { + assertTrue(e.getCause() instanceof ForbiddenException); + } + + } + + @Test + public void testTenantsClientUsers() throws Exception { + final TenantsClient tenantsClient = client.getTenantsClient(); + + // get all users + final List users = tenantsClient.getUsers(); + assertEquals(2, users.size()); + + final User initialAdminUser = users.stream() + .filter(u -> u.getIdentity().equals(INITIAL_ADMIN_IDENTITY)) + .findFirst() + .orElse(null); + assertNotNull(initialAdminUser); + + // get user by id + final User retrievedInitialAdminUser = tenantsClient.getUser(initialAdminUser.getIdentifier()); + assertNotNull(retrievedInitialAdminUser); + assertEquals(initialAdminUser.getIdentity(), retrievedInitialAdminUser.getIdentity()); + + // add user + final User userToAdd = new User(); + userToAdd.setIdentity("some-new-user"); + userToAdd.setRevision(new RevisionInfo(null, 0L)); + + final User createdUser = tenantsClient.createUser(userToAdd); + assertNotNull(createdUser); + assertEquals(3, tenantsClient.getUsers().size()); + + // update user + createdUser.setIdentity(createdUser.getIdentity() + "-updated"); + final User updatedUser = tenantsClient.updateUser(createdUser); + assertNotNull(updatedUser); + assertEquals(createdUser.getIdentity(), updatedUser.getIdentity()); + + // delete user + final User deletedUser = tenantsClient.deleteUser(updatedUser.getIdentifier(), updatedUser.getRevision()); + assertNotNull(deletedUser); + assertEquals(updatedUser.getIdentifier(), deletedUser.getIdentifier()); + } + + @Test + public void testTenantsClientGroups() throws Exception { + final TenantsClient tenantsClient = client.getTenantsClient(); + + // get all groups + final List groups = tenantsClient.getUserGroups(); + assertEquals(0, groups.size()); + + // create group + final UserGroup userGroup = new UserGroup(); + userGroup.setIdentity("some-new group"); + userGroup.setRevision(new RevisionInfo(null, 0L)); + + final UserGroup createdGroup = tenantsClient.createUserGroup(userGroup); + assertNotNull(createdGroup); + assertEquals(userGroup.getIdentity(), createdGroup.getIdentity()); + + // get group by id + final UserGroup retrievedGroup = tenantsClient.getUserGroup(createdGroup.getIdentifier()); + assertNotNull(retrievedGroup); + assertEquals(createdGroup.getIdentifier(), retrievedGroup.getIdentifier()); + + // update group + retrievedGroup.setIdentity(retrievedGroup.getIdentity() + "-updated"); + final UserGroup updatedGroup = tenantsClient.updateUserGroup(retrievedGroup); + assertEquals(retrievedGroup.getIdentity(), updatedGroup.getIdentity()); + + // delete group + final UserGroup deletedGroup = tenantsClient.deleteUserGroup(updatedGroup.getIdentifier(), updatedGroup.getRevision()); + assertNotNull(deletedGroup); + assertEquals(retrievedGroup.getIdentifier(), deletedGroup.getIdentifier()); + + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureProxyIT.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureProxyIT.java new file mode 100644 index 0000000000..0401eceec9 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureProxyIT.java @@ -0,0 +1,229 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.api; + +import org.apache.nifi.registry.NiFiRegistryTestApiApplication; +import org.apache.nifi.registry.authorization.CurrentUser; +import org.apache.nifi.registry.authorization.Permissions; +import org.apache.nifi.registry.client.NiFiRegistryClient; +import org.apache.nifi.registry.client.NiFiRegistryClientConfig; +import org.apache.nifi.registry.client.RequestConfig; +import org.apache.nifi.registry.client.UserClient; +import org.apache.nifi.registry.client.impl.JerseyNiFiRegistryClient; +import org.apache.nifi.registry.client.impl.request.ProxiedEntityRequestConfig; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.skyscreamer.jsonassert.JSONAssert; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.junit4.SpringRunner; + +import javax.ws.rs.core.Response; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@RunWith(SpringRunner.class) +@SpringBootTest( + classes = NiFiRegistryTestApiApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = "spring.profiles.include=ITSecureProxy") +@Import(SecureITClientConfiguration.class) +@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = {"classpath:db/clearDB.sql"}) +public class SecureProxyIT extends IntegrationTestBase { + + private static final Logger LOGGER = LoggerFactory.getLogger(SecureProxyIT.class); + + private static final String INITIAL_ADMIN_IDENTITY = "CN=user1, OU=nifi"; + private static final String PROXY_IDENTITY = "CN=proxy, OU=nifi"; + private static final String NEW_USER_IDENTITY = "CN=user2, OU=nifi"; + private static final String UTF8_USER_IDENTITY = "CN=Алйс, OU=nifi"; + private static final String ANONYMOUS_USER_IDENTITY = ""; + + private NiFiRegistryClient registryClient; + + @Before + public void setup() { + final String baseUrl = createBaseURL(); + LOGGER.info("Using base url = " + baseUrl); + + final NiFiRegistryClientConfig clientConfig = createClientConfig(baseUrl); + Assert.assertNotNull(clientConfig); + + final NiFiRegistryClient client = new JerseyNiFiRegistryClient.Builder() + .config(clientConfig) + .build(); + Assert.assertNotNull(client); + this.registryClient = client; + } + + @After + public void teardown() { + try { + registryClient.close(); + } catch (final Exception e) { + // do nothing + } + } + + @Test + public void testAccessStatus() throws Exception { + + // Given: the client and server have been configured correctly for two-way TLS + final String expectedJson = "{" + + "\"identity\":\"CN=proxy, OU=nifi\"," + + "\"anonymous\":false," + + "\"resourcePermissions\":{" + + "\"anyTopLevelResource\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + + "\"buckets\":{\"canRead\":true,\"canWrite\":false,\"canDelete\":false}," + + "\"tenants\":{\"canRead\":false,\"canWrite\":false,\"canDelete\":false}," + + "\"policies\":{\"canRead\":false,\"canWrite\":false,\"canDelete\":false}," + + "\"proxy\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}}" + + "}"; + + // When: the /access endpoint is queried + final Response response = client + .target(createURL("access")) + .request() + .get(Response.class); + + // Then: the server returns 200 OK with the expected client identity + assertEquals(200, response.getStatus()); + final String actualJson = response.readEntity(String.class); + JSONAssert.assertEquals(expectedJson, actualJson, false); + } + + @Test + public void testAccessStatusUsingRegistryClient() throws Exception { + + // Given: the client and server have been configured correctly for two-way TLS + final Permissions fullAccess = new Permissions().withCanRead(true).withCanWrite(true).withCanDelete(true); + final Permissions readAccess = new Permissions().withCanRead(true).withCanWrite(false).withCanDelete(false); + final Permissions noAccess = new Permissions().withCanRead(false).withCanWrite(false).withCanDelete(false); + + // When: the /access endpoint is queried + final UserClient userClient = registryClient.getUserClient(); + final CurrentUser currentUser = userClient.getAccessStatus(); + + // Then: the server returns the proxy identity with default nifi node access + assertEquals(PROXY_IDENTITY, currentUser.getIdentity()); + assertFalse(currentUser.isAnonymous()); + assertNotNull(currentUser.getResourcePermissions()); + assertEquals(fullAccess, currentUser.getResourcePermissions().getAnyTopLevelResource()); + assertEquals(readAccess, currentUser.getResourcePermissions().getBuckets()); + assertEquals(noAccess, currentUser.getResourcePermissions().getTenants()); + assertEquals(noAccess, currentUser.getResourcePermissions().getPolicies()); + assertEquals(fullAccess, currentUser.getResourcePermissions().getProxy()); + } + + @Test + public void testAccessStatusAsProxiedAdmin() throws Exception { + + // Given: the client and server have been configured correctly for two-way TLS + final Permissions fullAccess = new Permissions().withCanRead(true).withCanWrite(true).withCanDelete(true); + final RequestConfig proxiedEntityRequestConfig = new ProxiedEntityRequestConfig(INITIAL_ADMIN_IDENTITY); + + // When: the /access endpoint is queried using X-ProxiedEntitiesChain + final UserClient userClient = registryClient.getUserClient(proxiedEntityRequestConfig); + final CurrentUser currentUser = userClient.getAccessStatus(); + + // Then: the server returns the admin identity and access policies + assertEquals(INITIAL_ADMIN_IDENTITY, currentUser.getIdentity()); + assertFalse(currentUser.isAnonymous()); + assertNotNull(currentUser.getResourcePermissions()); + assertEquals(fullAccess, currentUser.getResourcePermissions().getAnyTopLevelResource()); + assertEquals(fullAccess, currentUser.getResourcePermissions().getBuckets()); + assertEquals(fullAccess, currentUser.getResourcePermissions().getTenants()); + assertEquals(fullAccess, currentUser.getResourcePermissions().getPolicies()); + assertEquals(fullAccess, currentUser.getResourcePermissions().getProxy()); + } + + @Test + public void testAccessStatusAsProxiedUser() throws Exception { + + // Given: the client and server have been configured correctly for two-way TLS + final Permissions noAccess = new Permissions().withCanRead(false).withCanWrite(false).withCanDelete(false); + final RequestConfig proxiedEntityRequestConfig = new ProxiedEntityRequestConfig(NEW_USER_IDENTITY); + + // When: the /access endpoint is queried using X-ProxiedEntitiesChain + final UserClient userClient = registryClient.getUserClient(proxiedEntityRequestConfig); + final CurrentUser currentUser = userClient.getAccessStatus(); + + // Then: the server returns the user identity ad + assertEquals(NEW_USER_IDENTITY, currentUser.getIdentity()); + assertFalse(currentUser.isAnonymous()); + assertNotNull(currentUser.getResourcePermissions()); + assertEquals(noAccess, currentUser.getResourcePermissions().getAnyTopLevelResource()); + assertEquals(noAccess, currentUser.getResourcePermissions().getBuckets()); + assertEquals(noAccess, currentUser.getResourcePermissions().getTenants()); + assertEquals(noAccess, currentUser.getResourcePermissions().getPolicies()); + assertEquals(noAccess, currentUser.getResourcePermissions().getProxy()); + } + + @Test + public void testAccessStatusAsProxiedAnonymousUser() throws Exception { + + // Given: the client and server have been configured correctly for two-way TLS + final Permissions noAccess = new Permissions().withCanRead(false).withCanWrite(false).withCanDelete(false); + final RequestConfig proxiedEntityRequestConfig = new ProxiedEntityRequestConfig(ANONYMOUS_USER_IDENTITY); + + // When: the /access endpoint is queried using X-ProxiedEntitiesChain + final UserClient userClient = registryClient.getUserClient(proxiedEntityRequestConfig); + final CurrentUser currentUser = userClient.getAccessStatus(); + + // Then: the server returns the proxy identity with default nifi node access + assertEquals("anonymous", currentUser.getIdentity()); + assertTrue(currentUser.isAnonymous()); + assertNotNull(currentUser.getResourcePermissions()); + assertEquals(noAccess, currentUser.getResourcePermissions().getAnyTopLevelResource()); + assertEquals(noAccess, currentUser.getResourcePermissions().getBuckets()); + assertEquals(noAccess, currentUser.getResourcePermissions().getTenants()); + assertEquals(noAccess, currentUser.getResourcePermissions().getPolicies()); + assertEquals(noAccess, currentUser.getResourcePermissions().getProxy()); + } + + @Test + public void testAccessStatusAsProxiedUtf8User() throws Exception { + + // Given: the client and server have been configured correctly for two-way TLS + final Permissions noAccess = new Permissions().withCanRead(false).withCanWrite(false).withCanDelete(false); + final RequestConfig proxiedEntityRequestConfig = new ProxiedEntityRequestConfig(UTF8_USER_IDENTITY); + + // When: the /access endpoint is queried using X-ProxiedEntitiesChain + final UserClient userClient = registryClient.getUserClient(proxiedEntityRequestConfig); + final CurrentUser currentUser = userClient.getAccessStatus(); + + // Then: the server returns the proxy identity with default nifi node access + assertEquals(UTF8_USER_IDENTITY, currentUser.getIdentity()); + assertFalse(currentUser.isAnonymous()); + assertNotNull(currentUser.getResourcePermissions()); + assertEquals(noAccess, currentUser.getResourcePermissions().getAnyTopLevelResource()); + assertEquals(noAccess, currentUser.getResourcePermissions().getBuckets()); + assertEquals(noAccess, currentUser.getResourcePermissions().getTenants()); + assertEquals(noAccess, currentUser.getResourcePermissions().getPolicies()); + assertEquals(noAccess, currentUser.getResourcePermissions().getProxy()); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/TenantResourceTest.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/TenantResourceTest.java new file mode 100644 index 0000000000..99350bb738 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/TenantResourceTest.java @@ -0,0 +1,165 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.api; + +import org.apache.nifi.registry.authorization.User; +import org.apache.nifi.registry.authorization.UserGroup; +import org.apache.nifi.registry.event.EventFactory; +import org.apache.nifi.registry.event.EventService; +import org.apache.nifi.registry.revision.entity.RevisionInfo; +import org.apache.nifi.registry.revision.web.ClientIdParameter; +import org.apache.nifi.registry.revision.web.LongParameter; +import org.apache.nifi.registry.web.service.ServiceFacade; +import org.junit.Before; +import org.junit.Test; + +import javax.servlet.http.HttpServletRequest; +import java.net.URI; +import java.net.URISyntaxException; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class TenantResourceTest { + + private TenantResource tenantResource; + private EventService eventService; + private ServiceFacade serviceFacade; + + @Before + public void setUp() { + eventService = mock(EventService.class); + serviceFacade = mock(ServiceFacade.class); + + tenantResource = new TenantResource(serviceFacade, eventService) { + + @Override + protected URI getBaseUri() { + try { + return new URI("http://registry.nifi.apache.org/nifi-registry"); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + }; + } + + @Test + public void testCreateUser() { + HttpServletRequest request = mock(HttpServletRequest.class); + + RevisionInfo revisionInfo = new RevisionInfo("client1", 0L); + User user = new User(null, "identity"); + user.setRevision(revisionInfo); + + RevisionInfo resultRevisionInfo = new RevisionInfo("client1", 1L); + User result = new User("identifier", user.getIdentity()); + result.setRevision(resultRevisionInfo); + + when(serviceFacade.createUser(user)).thenReturn(result); + + tenantResource.createUser(request, user); + + verify(serviceFacade).createUser(user); + verify(eventService).publish(eq(EventFactory.userCreated(result))); + } + + @Test + public void testUpdateUser() { + HttpServletRequest request = mock(HttpServletRequest.class); + + RevisionInfo revisionInfo = new RevisionInfo("client1", 1L); + User user = new User("identifier", "new-identity"); + user.setRevision(revisionInfo); + + when(serviceFacade.updateUser(user)).thenReturn(user); + + tenantResource.updateUser(request, user.getIdentifier(), user); + + verify(serviceFacade).updateUser(user); + verify(eventService).publish(eq(EventFactory.userUpdated(user))); + } + + @Test + public void testDeleteUser() { + HttpServletRequest request = mock(HttpServletRequest.class); + User user = new User("identifier", "identity"); + Long version = new Long(0); + ClientIdParameter clientId = new ClientIdParameter(); + + when(serviceFacade.deleteUser(eq(user.getIdentifier()), any(RevisionInfo.class))).thenReturn(user); + + tenantResource.removeUser(request, new LongParameter(version.toString()), clientId, user.getIdentifier()); + + verify(serviceFacade).deleteUser(eq(user.getIdentifier()), any(RevisionInfo.class)); + verify(eventService).publish(eq(EventFactory.userDeleted(user))); + } + + @Test + public void testCreateUserGroup() { + HttpServletRequest request = mock(HttpServletRequest.class); + + RevisionInfo revisionInfo = new RevisionInfo("client1", 0L); + UserGroup userGroup = new UserGroup(null, "identity"); + userGroup.setRevision(revisionInfo); + + RevisionInfo resultRevisionInfo = new RevisionInfo("client1", 1L); + UserGroup result = new UserGroup("identifier", userGroup.getIdentity()); + result.setRevision(resultRevisionInfo); + + when(serviceFacade.createUserGroup(userGroup)).thenReturn(result); + + tenantResource.createUserGroup(request, userGroup); + + verify(serviceFacade).createUserGroup(userGroup); + verify(eventService).publish(eq(EventFactory.userGroupCreated(result))); + } + + @Test + public void testUpdateUserGroup() { + HttpServletRequest request = mock(HttpServletRequest.class); + + RevisionInfo revisionInfo = new RevisionInfo("client1", 1L); + UserGroup userGroup = new UserGroup("identifier", "new-identity"); + userGroup.setRevision(revisionInfo); + + when(serviceFacade.updateUserGroup(userGroup)).thenReturn(userGroup); + + tenantResource.updateUserGroup(request, userGroup.getIdentifier(), userGroup); + + verify(serviceFacade).updateUserGroup(userGroup); + verify(eventService).publish(eq(EventFactory.userGroupUpdated(userGroup))); + } + + @Test + public void testDeleteUserGroup() { + HttpServletRequest request = mock(HttpServletRequest.class); + UserGroup userGroup = new UserGroup("identifier", "identity"); + Long version = new Long(0); + ClientIdParameter clientId = new ClientIdParameter(); + + when(serviceFacade.deleteUserGroup(eq(userGroup.getIdentifier()), any(RevisionInfo.class))).thenReturn(userGroup); + + tenantResource.removeUserGroup(request, new LongParameter(version.toString()), clientId, userGroup.getIdentifier()); + + verify(serviceFacade).deleteUserGroup(eq(userGroup.getIdentifier()), any(RevisionInfo.class)); + verify(eventService).publish(eq(EventFactory.userGroupDeleted(userGroup))); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredITBase.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredITBase.java new file mode 100644 index 0000000000..a0c981b22a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredITBase.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.api; + +import org.apache.nifi.registry.NiFiRegistryTestApiApplication; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * Deploy the Web API Application using an embedded Jetty Server for local integration testing, with the follow characteristics: + * + * - A NiFiRegistryProperties has to be explicitly provided to the ApplicationContext using a profile unique to this test suite. + * - The database is embed H2 using volatile (in-memory) persistence + * - Custom SQL is clearing the DB before each test method by default, unless method overrides this behavior + */ +@RunWith(SpringRunner.class) +@SpringBootTest( + classes = NiFiRegistryTestApiApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = "spring.profiles.include=ITUnsecured") +@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:db/clearDB.sql") +public class UnsecuredITBase extends IntegrationTestBase { + + // Tests cases defined in subclasses + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredNiFiRegistryClientIT.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredNiFiRegistryClientIT.java new file mode 100644 index 0000000000..4c5ebf1df0 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredNiFiRegistryClientIT.java @@ -0,0 +1,1041 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.api; + +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.authorization.CurrentUser; +import org.apache.nifi.registry.authorization.Permissions; +import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.bucket.BucketItem; +import org.apache.nifi.registry.bucket.BucketItemType; +import org.apache.nifi.registry.client.BucketClient; +import org.apache.nifi.registry.client.BundleClient; +import org.apache.nifi.registry.client.BundleVersionClient; +import org.apache.nifi.registry.client.ExtensionClient; +import org.apache.nifi.registry.client.ExtensionRepoClient; +import org.apache.nifi.registry.client.FlowClient; +import org.apache.nifi.registry.client.FlowSnapshotClient; +import org.apache.nifi.registry.client.ItemsClient; +import org.apache.nifi.registry.client.NiFiRegistryClient; +import org.apache.nifi.registry.client.NiFiRegistryClientConfig; +import org.apache.nifi.registry.client.NiFiRegistryException; +import org.apache.nifi.registry.client.UserClient; +import org.apache.nifi.registry.client.impl.JerseyNiFiRegistryClient; +import org.apache.nifi.registry.diff.VersionedFlowDifference; +import org.apache.nifi.registry.extension.bundle.BuildInfo; +import org.apache.nifi.registry.extension.bundle.Bundle; +import org.apache.nifi.registry.extension.bundle.BundleFilterParams; +import org.apache.nifi.registry.extension.bundle.BundleType; +import org.apache.nifi.registry.extension.bundle.BundleVersion; +import org.apache.nifi.registry.extension.bundle.BundleVersionDependency; +import org.apache.nifi.registry.extension.bundle.BundleVersionFilterParams; +import org.apache.nifi.registry.extension.bundle.BundleVersionMetadata; +import org.apache.nifi.registry.extension.component.ExtensionFilterParams; +import org.apache.nifi.registry.extension.component.ExtensionMetadataContainer; +import org.apache.nifi.registry.extension.component.TagCount; +import org.apache.nifi.registry.extension.component.manifest.Extension; +import org.apache.nifi.registry.extension.component.ExtensionMetadata; +import org.apache.nifi.registry.extension.component.manifest.ExtensionType; +import org.apache.nifi.registry.extension.component.manifest.ProvidedServiceAPI; +import org.apache.nifi.registry.extension.repo.ExtensionRepoArtifact; +import org.apache.nifi.registry.extension.repo.ExtensionRepoBucket; +import org.apache.nifi.registry.extension.repo.ExtensionRepoExtensionMetadata; +import org.apache.nifi.registry.extension.repo.ExtensionRepoGroup; +import org.apache.nifi.registry.extension.repo.ExtensionRepoVersion; +import org.apache.nifi.registry.extension.repo.ExtensionRepoVersionSummary; +import org.apache.nifi.registry.field.Fields; +import org.apache.nifi.registry.flow.ExternalControllerServiceReference; +import org.apache.nifi.registry.flow.VersionedFlow; +import org.apache.nifi.registry.flow.VersionedFlowSnapshot; +import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata; +import org.apache.nifi.registry.flow.VersionedParameter; +import org.apache.nifi.registry.flow.VersionedParameterContext; +import org.apache.nifi.registry.flow.VersionedProcessGroup; +import org.apache.nifi.registry.flow.VersionedProcessor; +import org.apache.nifi.registry.flow.VersionedPropertyDescriptor; +import org.apache.nifi.registry.revision.entity.RevisionInfo; +import org.apache.nifi.registry.util.FileUtils; +import org.bouncycastle.util.encoders.Hex; +import org.glassfish.jersey.media.multipart.MultiPartFeature; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MediaType; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * Test all basic functionality of JerseyNiFiRegistryClient. + */ +public class UnsecuredNiFiRegistryClientIT extends UnsecuredITBase { + + static final Logger LOGGER = LoggerFactory.getLogger(UnsecuredNiFiRegistryClientIT.class); + + static final String CLIENT_ID = "UnsecuredNiFiRegistryClientIT"; + + private NiFiRegistryClient client; + + @Before + public void setup() { + final String baseUrl = createBaseURL(); + LOGGER.info("Using base url = " + baseUrl); + + final NiFiRegistryClientConfig clientConfig = new NiFiRegistryClientConfig.Builder() + .baseUrl(baseUrl) + .build(); + + assertNotNull(clientConfig); + + final NiFiRegistryClient client = new JerseyNiFiRegistryClient.Builder() + .config(clientConfig) + .build(); + + assertNotNull(client); + this.client = client; + + // Clear the extension bundles storage directory in case previous tests left data + final File extensionsStorageDir = new File("./target/test-classes/extension_bundles"); + if (extensionsStorageDir.exists()) { + try { + FileUtils.deleteFile(extensionsStorageDir, true); + } catch (Exception e) { + LOGGER.warn("Unable to delete extensions storage dir due to: " + e.getMessage(), e); + } + } + } + + @After + public void teardown() { + try { + client.close(); + } catch (Exception e) { + + } + } + + @Test + public void testGetAccessStatus() throws IOException, NiFiRegistryException { + final UserClient userClient = client.getUserClient(); + final CurrentUser currentUser = userClient.getAccessStatus(); + assertEquals("anonymous", currentUser.getIdentity()); + assertTrue(currentUser.isAnonymous()); + assertNotNull(currentUser.getResourcePermissions()); + Permissions fullAccess = new Permissions().withCanRead(true).withCanWrite(true).withCanDelete(true); + assertEquals(fullAccess, currentUser.getResourcePermissions().getAnyTopLevelResource()); + assertEquals(fullAccess, currentUser.getResourcePermissions().getBuckets()); + assertEquals(fullAccess, currentUser.getResourcePermissions().getTenants()); + assertEquals(fullAccess, currentUser.getResourcePermissions().getPolicies()); + assertEquals(fullAccess, currentUser.getResourcePermissions().getProxy()); + } + + @Test + public void testNiFiRegistryClient() throws IOException, NiFiRegistryException, NoSuchAlgorithmException { + // ---------------------- TEST BUCKETS --------------------------// + + final BucketClient bucketClient = client.getBucketClient(); + + // create buckets + final int numBuckets = 10; + final List createdBuckets = new ArrayList<>(); + + for (int i=0; i < numBuckets; i++) { + final Bucket createdBucket = createBucket(bucketClient, i); + LOGGER.info("Created bucket # " + i + " with id " + createdBucket.getIdentifier()); + createdBuckets.add(createdBucket); + } + + // get each bucket + for (final Bucket bucket : createdBuckets) { + final Bucket retrievedBucket = bucketClient.get(bucket.getIdentifier()); + assertNotNull(retrievedBucket); + assertNotNull(retrievedBucket.getRevision()); + assertFalse(retrievedBucket.isAllowBundleRedeploy()); + LOGGER.info("Retrieved bucket " + retrievedBucket.getIdentifier()); + } + + //final Bucket nonExistentBucket = bucketClient.get("does-not-exist"); + //assertNull(nonExistentBucket); + + // get bucket fields + final Fields bucketFields = bucketClient.getFields(); + assertNotNull(bucketFields); + LOGGER.info("Retrieved bucket fields, size = " + bucketFields.getFields().size()); + assertTrue(bucketFields.getFields().size() > 0); + + // get all buckets + final List allBuckets = bucketClient.getAll(); + LOGGER.info("Retrieved buckets, size = " + allBuckets.size()); + assertEquals(numBuckets, allBuckets.size()); + allBuckets.stream().forEach(b -> System.out.println("Retrieve bucket " + b.getIdentifier())); + + // update each bucket + for (final Bucket bucket : createdBuckets) { + final Bucket bucketUpdate = new Bucket(); + bucketUpdate.setIdentifier(bucket.getIdentifier()); + bucketUpdate.setDescription(bucket.getDescription() + " UPDATE"); + bucketUpdate.setRevision(bucket.getRevision()); + + final Bucket updatedBucket = bucketClient.update(bucketUpdate); + assertNotNull(updatedBucket); + LOGGER.info("Updated bucket " + updatedBucket.getIdentifier()); + } + + // ---------------------- TEST FLOWS --------------------------// + + final FlowClient flowClient = client.getFlowClient(); + + // create flows + final Bucket flowsBucket = createdBuckets.get(0); + + final VersionedFlow flow1 = createFlow(flowClient, flowsBucket, 1); + LOGGER.info("Created flow # 1 with id " + flow1.getIdentifier()); + + final VersionedFlow flow2 = createFlow(flowClient, flowsBucket, 2); + LOGGER.info("Created flow # 2 with id " + flow2.getIdentifier()); + + // get flow + final VersionedFlow retrievedFlow1 = flowClient.get(flowsBucket.getIdentifier(), flow1.getIdentifier()); + assertNotNull(retrievedFlow1); + LOGGER.info("Retrieved flow # 1 with id " + retrievedFlow1.getIdentifier()); + + final VersionedFlow retrievedFlow2 = flowClient.get(flowsBucket.getIdentifier(), flow2.getIdentifier()); + assertNotNull(retrievedFlow2); + LOGGER.info("Retrieved flow # 2 with id " + retrievedFlow2.getIdentifier()); + + // get flow without bucket + final VersionedFlow retrievedFlow1WithoutBucket = flowClient.get(flow1.getIdentifier()); + assertNotNull(retrievedFlow1WithoutBucket); + assertEquals(flow1.getIdentifier(), retrievedFlow1WithoutBucket.getIdentifier()); + LOGGER.info("Retrieved flow # 1 without bucket id, with id " + retrievedFlow1WithoutBucket.getIdentifier()); + + // update flows + final VersionedFlow flow1Update = new VersionedFlow(); + flow1Update.setIdentifier(flow1.getIdentifier()); + flow1Update.setName(flow1.getName() + " UPDATED"); + flow1Update.setRevision(retrievedFlow1.getRevision()); + + final VersionedFlow updatedFlow1 = flowClient.update(flowsBucket.getIdentifier(), flow1Update); + assertNotNull(updatedFlow1); + LOGGER.info("Updated flow # 1 with id " + updatedFlow1.getIdentifier()); + + // get flow fields + final Fields flowFields = flowClient.getFields(); + assertNotNull(flowFields); + LOGGER.info("Retrieved flow fields, size = " + flowFields.getFields().size()); + assertTrue(flowFields.getFields().size() > 0); + + // get flows in bucket + final List flowsInBucket = flowClient.getByBucket(flowsBucket.getIdentifier()); + assertNotNull(flowsInBucket); + assertEquals(2, flowsInBucket.size()); + flowsInBucket.stream().forEach(f -> LOGGER.info("Flow in bucket, flow id " + f.getIdentifier())); + + // ---------------------- TEST SNAPSHOTS --------------------------// + + final FlowSnapshotClient snapshotClient = client.getFlowSnapshotClient(); + + // create snapshots + final VersionedFlow snapshotFlow = flow1; + + final VersionedFlowSnapshot snapshot1 = createSnapshot(snapshotClient, snapshotFlow, 1); + assertNotNull(snapshot1); + assertNotNull(snapshot1.getSnapshotMetadata()); + assertEquals(snapshotFlow.getIdentifier(), snapshot1.getSnapshotMetadata().getFlowIdentifier()); + LOGGER.info("Created snapshot # 1 with version " + snapshot1.getSnapshotMetadata().getVersion()); + + final VersionedFlowSnapshot snapshot2 = createSnapshot(snapshotClient, snapshotFlow, 2); + LOGGER.info("Created snapshot # 2 with version " + snapshot2.getSnapshotMetadata().getVersion()); + + // get snapshot + final VersionedFlowSnapshot retrievedSnapshot1 = snapshotClient.get(snapshotFlow.getBucketIdentifier(), snapshotFlow.getIdentifier(), 1); + assertNotNull(retrievedSnapshot1); + assertFalse(retrievedSnapshot1.isLatest()); + LOGGER.info("Retrieved snapshot # 1 with version " + retrievedSnapshot1.getSnapshotMetadata().getVersion()); + + final VersionedFlowSnapshot retrievedSnapshot2 = snapshotClient.get(snapshotFlow.getBucketIdentifier(), snapshotFlow.getIdentifier(), 2); + assertNotNull(retrievedSnapshot2); + assertTrue(retrievedSnapshot2.isLatest()); + LOGGER.info("Retrieved snapshot # 2 with version " + retrievedSnapshot2.getSnapshotMetadata().getVersion()); + + // get snapshot without bucket + final VersionedFlowSnapshot retrievedSnapshot1WithoutBucket = snapshotClient.get(snapshotFlow.getIdentifier(), 1); + assertNotNull(retrievedSnapshot1WithoutBucket); + assertFalse(retrievedSnapshot1WithoutBucket.isLatest()); + assertEquals(snapshotFlow.getIdentifier(), retrievedSnapshot1WithoutBucket.getSnapshotMetadata().getFlowIdentifier()); + assertEquals(1, retrievedSnapshot1WithoutBucket.getSnapshotMetadata().getVersion()); + LOGGER.info("Retrieved snapshot # 1 without using bucket id, with version " + retrievedSnapshot1WithoutBucket.getSnapshotMetadata().getVersion()); + + // get latest + final VersionedFlowSnapshot retrievedSnapshotLatest = snapshotClient.getLatest(snapshotFlow.getBucketIdentifier(), snapshotFlow.getIdentifier()); + assertNotNull(retrievedSnapshotLatest); + assertEquals(snapshot2.getSnapshotMetadata().getVersion(), retrievedSnapshotLatest.getSnapshotMetadata().getVersion()); + assertTrue(retrievedSnapshotLatest.isLatest()); + LOGGER.info("Retrieved latest snapshot with version " + retrievedSnapshotLatest.getSnapshotMetadata().getVersion()); + + // get latest without bucket + final VersionedFlowSnapshot retrievedSnapshotLatestWithoutBucket = snapshotClient.getLatest(snapshotFlow.getIdentifier()); + assertNotNull(retrievedSnapshotLatestWithoutBucket); + assertEquals(snapshot2.getSnapshotMetadata().getVersion(), retrievedSnapshotLatestWithoutBucket.getSnapshotMetadata().getVersion()); + assertTrue(retrievedSnapshotLatestWithoutBucket.isLatest()); + LOGGER.info("Retrieved latest snapshot without bucket, with version " + retrievedSnapshotLatestWithoutBucket.getSnapshotMetadata().getVersion()); + + // get metadata + final List retrievedMetadata = snapshotClient.getSnapshotMetadata(snapshotFlow.getBucketIdentifier(), snapshotFlow.getIdentifier()); + assertNotNull(retrievedMetadata); + assertEquals(2, retrievedMetadata.size()); + assertEquals(2, retrievedMetadata.get(0).getVersion()); + assertEquals(1, retrievedMetadata.get(1).getVersion()); + retrievedMetadata.stream().forEach(s -> LOGGER.info("Retrieved snapshot metadata " + s.getVersion())); + + // get metadata without bucket + final List retrievedMetadataWithoutBucket = snapshotClient.getSnapshotMetadata(snapshotFlow.getIdentifier()); + assertNotNull(retrievedMetadataWithoutBucket); + assertEquals(2, retrievedMetadataWithoutBucket.size()); + assertEquals(2, retrievedMetadataWithoutBucket.get(0).getVersion()); + assertEquals(1, retrievedMetadataWithoutBucket.get(1).getVersion()); + retrievedMetadataWithoutBucket.stream().forEach(s -> LOGGER.info("Retrieved snapshot metadata " + s.getVersion())); + + // get latest metadata + final VersionedFlowSnapshotMetadata latestMetadata = snapshotClient.getLatestMetadata(snapshotFlow.getBucketIdentifier(), snapshotFlow.getIdentifier()); + assertNotNull(latestMetadata); + assertEquals(2, latestMetadata.getVersion()); + + // get latest metadata that doesn't exist + try { + snapshotClient.getLatestMetadata(snapshotFlow.getBucketIdentifier(), "DOES-NOT-EXIST"); + fail("Should have thrown exception"); + } catch (NiFiRegistryException nfe) { + assertEquals("Error retrieving latest snapshot metadata: The specified flow ID does not exist in this bucket.", nfe.getMessage()); + } + + // get latest metadata without bucket + final VersionedFlowSnapshotMetadata latestMetadataWithoutBucket = snapshotClient.getLatestMetadata(snapshotFlow.getIdentifier()); + assertNotNull(latestMetadataWithoutBucket); + assertEquals(snapshotFlow.getIdentifier(), latestMetadataWithoutBucket.getFlowIdentifier()); + assertEquals(2, latestMetadataWithoutBucket.getVersion()); + + // ---------------------- TEST EXTENSIONS ----------------------// + + // verify we have no bundles yet + final BundleClient bundleClient = client.getBundleClient(); + final List allBundles = bundleClient.getAll(); + assertEquals(0, allBundles.size()); + + final Bucket bundlesBucket = createdBuckets.get(1); + final Bucket bundlesBucket2 = createdBuckets.get(2); + final BundleVersionClient bundleVersionClient = client.getBundleVersionClient(); + + // create version 1.0.0 of nifi-test-nar + final String testNar1 = "src/test/resources/extensions/nars/nifi-test-nar-1.0.0.nar"; + final BundleVersion createdTestNarV1 = createExtensionBundleVersionWithStream(bundlesBucket, bundleVersionClient, testNar1, null); + + final Bundle testNarV1Bundle = createdTestNarV1.getBundle(); + LOGGER.info("Created bundle with id {}", new Object[]{testNarV1Bundle.getIdentifier()}); + + assertEquals("org.apache.nifi", testNarV1Bundle.getGroupId()); + assertEquals("nifi-test-nar", testNarV1Bundle.getArtifactId()); + assertEquals(BundleType.NIFI_NAR, testNarV1Bundle.getBundleType()); + assertEquals(1, testNarV1Bundle.getVersionCount()); + + assertEquals("org.apache.nifi:nifi-test-nar", testNarV1Bundle.getName()); + assertEquals(bundlesBucket.getIdentifier(), testNarV1Bundle.getBucketIdentifier()); + assertEquals(bundlesBucket.getName(), testNarV1Bundle.getBucketName()); + assertNotNull(testNarV1Bundle.getPermissions()); + assertTrue(testNarV1Bundle.getCreatedTimestamp() > 0); + assertTrue(testNarV1Bundle.getModifiedTimestamp() > 0); + + final BundleVersionMetadata testNarV1Metadata = createdTestNarV1.getVersionMetadata(); + assertEquals("1.0.0", testNarV1Metadata.getVersion()); + assertNotNull(testNarV1Metadata.getId()); + assertNotNull(testNarV1Metadata.getSha256()); + assertNotNull(testNarV1Metadata.getAuthor()); + assertEquals(testNarV1Bundle.getIdentifier(), testNarV1Metadata.getBundleId()); + assertEquals(bundlesBucket.getIdentifier(), testNarV1Metadata.getBucketId()); + assertTrue(testNarV1Metadata.getTimestamp() > 0); + assertFalse(testNarV1Metadata.getSha256Supplied()); + assertTrue(testNarV1Metadata.getContentSize() > 1); + + final BuildInfo testNarV1BuildInfo = testNarV1Metadata.getBuildInfo(); + assertNotNull(testNarV1BuildInfo); + assertTrue(testNarV1BuildInfo.getBuilt() > 0); + assertNotNull(testNarV1BuildInfo.getBuildTool()); + assertNotNull(testNarV1BuildInfo.getBuildRevision()); + + final Set dependencies = createdTestNarV1.getDependencies(); + assertNotNull(dependencies); + assertEquals(1, dependencies.size()); + + final BundleVersionDependency testNarV1Dependency = dependencies.stream().findFirst().get(); + assertEquals("org.apache.nifi", testNarV1Dependency.getGroupId()); + assertEquals("nifi-test-api-nar", testNarV1Dependency.getArtifactId()); + assertEquals("1.0.0", testNarV1Dependency.getVersion()); + + final String testNar2 = "src/test/resources/extensions/nars/nifi-test-nar-2.0.0.nar"; + + // try to create version 2.0.0 of nifi-test-nar when the supplied SHA-256 does not match server's + final String madeUpSha256 = "MADE-UP-SHA-256"; + try { + createExtensionBundleVersionWithStream(bundlesBucket, bundleVersionClient, testNar2, madeUpSha256); + fail("Should have thrown exception"); + } catch (Exception e) { + // should have thrown exception from mismatched SHA-256 + } + + // create version 2.0.0 of nifi-test-nar using correct supplied SHA-256 + final String testNar2Sha256 = calculateSha256Hex(testNar2); + final BundleVersion createdTestNarV2 = createExtensionBundleVersionWithStream(bundlesBucket, bundleVersionClient, testNar2, testNar2Sha256); + assertTrue(createdTestNarV2.getVersionMetadata().getSha256Supplied()); + + final Bundle testNarV2Bundle = createdTestNarV2.getBundle(); + LOGGER.info("Created bundle with id {}", new Object[]{testNarV2Bundle.getIdentifier()}); + + // create version 1.0.0 of nifi-foo-nar, use the file variant + final String fooNar = "src/test/resources/extensions/nars/nifi-foo-nar-1.0.0.nar"; + final BundleVersion createdFooNarV1 = createExtensionBundleVersionWithFile(bundlesBucket, bundleVersionClient, fooNar, null); + assertFalse(createdFooNarV1.getVersionMetadata().getSha256Supplied()); + + final Bundle fooNarV1Bundle = createdFooNarV1.getBundle(); + LOGGER.info("Created bundle with id {}", new Object[]{fooNarV1Bundle.getIdentifier()}); + + // verify that bucket 1 currently does not allow redeploying non-snapshot artifacts + assertFalse(bundlesBucket.isAllowBundleRedeploy()); + + // try to re-deploy version 1.0.0 of nifi-foo-nar, should fail + try { + createExtensionBundleVersionWithFile(bundlesBucket, bundleVersionClient, fooNar, null); + fail("Should have thrown exception when re-deploying foo nar"); + } catch (Exception e) { + // Should throw exception + } + + // now update bucket 1 to allow redeploy + bundlesBucket.setAllowBundleRedeploy(true); + final Bucket updatedBundlesBucket = bucketClient.update(bundlesBucket); + assertTrue(updatedBundlesBucket.isAllowBundleRedeploy()); + + // try to re-deploy version 1.0.0 of nifi-foo-nar again, this time should work + assertNotNull(createExtensionBundleVersionWithFile(bundlesBucket, bundleVersionClient, fooNar, null)); + + // verify there are 2 bundles now + final List allBundlesAfterCreate = bundleClient.getAll(); + assertEquals(2, allBundlesAfterCreate.size()); + + // create version 2.0.0-SNAPSHOT (build 1 content) of nifi-foor-nar in the first bucket + final String fooNarV2SnapshotB1 = "src/test/resources/extensions/nars/nifi-foo-nar-2.0.0-SNAPSHOT-BUILD1.nar"; + final BundleVersion createdFooNarV2SnapshotB1 = createExtensionBundleVersionWithFile(bundlesBucket, bundleVersionClient, fooNarV2SnapshotB1, null); + assertFalse(createdFooNarV2SnapshotB1.getVersionMetadata().getSha256Supplied()); + + // create version 2.0.0-SNAPSHOT (build 2 content) of nifi-foor-nar in the second bucket + // proves that snapshots can have different checksums across buckets, non-snapshots can't + final String fooNarV2SnapshotB2 = "src/test/resources/extensions/nars/nifi-foo-nar-2.0.0-SNAPSHOT-BUILD2.nar"; + final BundleVersion createdFooNarV2SnapshotB2 = createExtensionBundleVersionWithFile(bundlesBucket2, bundleVersionClient, fooNarV2SnapshotB2, null); + assertFalse(createdFooNarV2SnapshotB2.getVersionMetadata().getSha256Supplied()); + + // create version 2.0.0-SNAPSHOT (build 2 content) of nifi-foor-nar in the second bucket + // proves that we can overwrite a snapshot in a given bucket + final String fooNarV2SnapshotB3 = "src/test/resources/extensions/nars/nifi-foo-nar-2.0.0-SNAPSHOT-BUILD3.nar"; + final BundleVersion createdFooNarV2SnapshotB3 = createExtensionBundleVersionWithFile(bundlesBucket2, bundleVersionClient, fooNarV2SnapshotB3, null); + assertFalse(createdFooNarV2SnapshotB3.getVersionMetadata().getSha256Supplied()); + + // verify retrieving nifi-foo-nar 2.0.0-SNAPSHOT from second bucket returns the build 3 content + final BundleVersion retrievedFooNarV2SnapshotB3 = bundleVersionClient.getBundleVersion( + createdFooNarV2SnapshotB3.getVersionMetadata().getBundleId(), + createdFooNarV2SnapshotB3.getVersionMetadata().getVersion()); + assertEquals(calculateSha256Hex(fooNarV2SnapshotB3), retrievedFooNarV2SnapshotB3.getVersionMetadata().getSha256()); + + final List fooNarV2SnapshotB3Extensions = bundleVersionClient.getExtensions( + createdFooNarV2SnapshotB3.getVersionMetadata().getBundleId(), + createdFooNarV2SnapshotB3.getVersionMetadata().getVersion()); + assertNotNull(fooNarV2SnapshotB3Extensions); + assertEquals(1, fooNarV2SnapshotB3Extensions.size()); + checkExtensionMetadata(fooNarV2SnapshotB3Extensions); + + // verify getting an extension for a specific bundle version + final String fooNarV2SnapshotB3ExtensionName = fooNarV2SnapshotB3Extensions.get(0).getName(); + final Extension fooNarV2SnapshotB3Extension = bundleVersionClient.getExtension( + createdFooNarV2SnapshotB3.getVersionMetadata().getBundleId(), + createdFooNarV2SnapshotB3.getVersionMetadata().getVersion(), + fooNarV2SnapshotB3ExtensionName + ); + assertNotNull(fooNarV2SnapshotB3Extension); + assertEquals(fooNarV2SnapshotB3ExtensionName, fooNarV2SnapshotB3Extension.getName()); + + // verify getting the docs for an extension for a specific bundle version + try (final InputStream docsInput = bundleVersionClient.getExtensionDocs( + createdFooNarV2SnapshotB3.getVersionMetadata().getBundleId(), + createdFooNarV2SnapshotB3.getVersionMetadata().getVersion(), + fooNarV2SnapshotB3ExtensionName + )) { + final String docsContent = IOUtils.toString(docsInput, StandardCharsets.UTF_8); + assertNotNull(docsContent); + assertTrue(docsContent.startsWith("")); + } + + // verify getting bundles by bucket + assertEquals(2, bundleClient.getByBucket(bundlesBucket.getIdentifier()).size()); + assertEquals(0, bundleClient.getByBucket(flowsBucket.getIdentifier()).size()); + + // verify getting bundles by id + final Bundle retrievedBundle = bundleClient.get(testNarV1Bundle.getIdentifier()); + assertNotNull(retrievedBundle); + assertEquals(testNarV1Bundle.getIdentifier(), retrievedBundle.getIdentifier()); + assertEquals(testNarV1Bundle.getGroupId(), retrievedBundle.getGroupId()); + assertEquals(testNarV1Bundle.getArtifactId(), retrievedBundle.getArtifactId()); + + // verify getting list of version metadata for a bundle + final List bundleVersions = bundleVersionClient.getBundleVersions(testNarV1Bundle.getIdentifier()); + assertNotNull(bundleVersions); + assertEquals(2, bundleVersions.size()); + + // verify getting a bundle version by the bundle id + version string + final BundleVersion bundleVersion1 = bundleVersionClient.getBundleVersion(testNarV1Bundle.getIdentifier(), "1.0.0"); + assertNotNull(bundleVersion1); + assertEquals("1.0.0", bundleVersion1.getVersionMetadata().getVersion()); + assertNotNull(bundleVersion1.getDependencies()); + assertEquals(1, bundleVersion1.getDependencies().size()); + + final BundleVersion bundleVersion2 = bundleVersionClient.getBundleVersion(testNarV1Bundle.getIdentifier(), "2.0.0"); + assertNotNull(bundleVersion2); + assertEquals("2.0.0", bundleVersion2.getVersionMetadata().getVersion()); + + // verify getting the input stream for a bundle version + try (final InputStream bundleVersion1InputStream = bundleVersionClient.getBundleVersionContent(testNarV1Bundle.getIdentifier(), "1.0.0")) { + final String sha256Hex = DigestUtils.sha256Hex(bundleVersion1InputStream); + assertEquals(testNarV1Metadata.getSha256(), sha256Hex); + } + + // verify writing a bundle version to an output stream + final File targetDir = new File("./target"); + final File bundleFile = bundleVersionClient.writeBundleVersionContent(testNarV1Bundle.getIdentifier(), "1.0.0", targetDir); + assertNotNull(bundleFile); + + try (final InputStream bundleInputStream = new FileInputStream(bundleFile)) { + final String sha256Hex = DigestUtils.sha256Hex(bundleInputStream); + assertEquals(testNarV1Metadata.getSha256(), sha256Hex); + } + + // Verify deleting a bundle version + final BundleVersion deletedBundleVersion2 = bundleVersionClient.delete(testNarV1Bundle.getIdentifier(), "2.0.0"); + assertNotNull(deletedBundleVersion2); + assertEquals(testNarV1Bundle.getIdentifier(), deletedBundleVersion2.getBundle().getIdentifier()); + assertEquals("2.0.0", deletedBundleVersion2.getVersionMetadata().getVersion()); + + try { + bundleVersionClient.getBundleVersion(testNarV1Bundle.getIdentifier(), "2.0.0"); + fail("Should have thrown exception"); + } catch (Exception e) { + // should catch exception + } + + // Verify getting bundles with filter params + assertEquals(3, bundleClient.getAll(BundleFilterParams.empty()).size()); + + final List filteredBundles = bundleClient.getAll(BundleFilterParams.of("org.apache.nifi", "nifi-test-nar")); + assertEquals(1, filteredBundles.size()); + + // Verify getting bundle versions with filter params + assertEquals(4, bundleVersionClient.getBundleVersions(BundleVersionFilterParams.empty()).size()); + + final List filteredVersions = bundleVersionClient.getBundleVersions( + BundleVersionFilterParams.of("org.apache.nifi", "nifi-foo-nar", "1.0.0")); + assertEquals(1, filteredVersions.size()); + + final List filteredVersions2 = bundleVersionClient.getBundleVersions( + BundleVersionFilterParams.of("org.apache.nifi", null, null)); + assertEquals(4, filteredVersions2.size()); + + filteredVersions2.forEach(bvm -> { + assertNotNull(bvm.getGroupId()); + assertNotNull(bvm.getArtifactId()); + assertNotNull(bvm.getVersion()); + }); + + // ---------------------- TEST EXTENSION REPO ----------------------// + + final ExtensionRepoClient extensionRepoClient = client.getExtensionRepoClient(); + + final List repoBuckets = extensionRepoClient.getBuckets(); + assertEquals(createdBuckets.size(), repoBuckets.size()); + + final String bundlesBucketName = bundlesBucket.getName(); + final List repoGroups = extensionRepoClient.getGroups(bundlesBucketName); + assertEquals(1, repoGroups.size()); + + final String repoGroupId = "org.apache.nifi"; + final ExtensionRepoGroup repoGroup = repoGroups.get(0); + assertEquals(repoGroupId, repoGroup.getGroupId()); + + final List repoArtifacts = extensionRepoClient.getArtifacts(bundlesBucketName, repoGroupId); + assertEquals(2, repoArtifacts.size()); + + final String repoArtifactId = "nifi-test-nar"; + final List repoVersions = extensionRepoClient.getVersions(bundlesBucketName, repoGroupId, repoArtifactId); + assertEquals(1, repoVersions.size()); + + final String repoVersionString = "1.0.0"; + final ExtensionRepoVersion repoVersion = extensionRepoClient.getVersion(bundlesBucketName, repoGroupId, repoArtifactId, repoVersionString); + assertNotNull(repoVersion); + assertNotNull(repoVersion.getDownloadLink()); + assertNotNull(repoVersion.getSha256Link()); + assertNotNull(repoVersion.getExtensionsLink()); + + // verify the version links for content and sha256 + final Client jerseyClient = ClientBuilder.newBuilder().register(MultiPartFeature.class).build(); + + final WebTarget downloadLinkTarget = jerseyClient.target(repoVersion.getDownloadLink().getUri()); + try (final InputStream downloadLinkInputStream = downloadLinkTarget.request() + .accept(MediaType.APPLICATION_OCTET_STREAM_TYPE).get().readEntity(InputStream.class)) { + final String sha256DownloadResult = DigestUtils.sha256Hex(downloadLinkInputStream); + + final WebTarget sha256LinkTarget = jerseyClient.target(repoVersion.getSha256Link().getUri()); + final String sha256LinkResult = sha256LinkTarget.request().get(String.class); + assertEquals(sha256DownloadResult, sha256LinkResult); + } + + // verify the version link for extension metadata works + final WebTarget extensionsLinkTarget = jerseyClient.target(repoVersion.getExtensionsLink().getUri()); + final ExtensionRepoExtensionMetadata[] extensions = extensionsLinkTarget.request() + .accept(MediaType.APPLICATION_JSON) + .get(ExtensionRepoExtensionMetadata[].class); + assertNotNull(extensions); + assertTrue(extensions.length > 0); + checkExtensionMetadata(Stream.of(extensions).map(e -> e.getExtensionMetadata()).collect(Collectors.toSet())); + Stream.of(extensions).forEach(e -> { + assertNotNull(e.getLink()); + assertNotNull(e.getLinkDocs()); + }); + + // verify the client methods for content input stream, content sha256, and extensions + try (final InputStream repoVersionInputStream = extensionRepoClient.getVersionContent(bundlesBucketName, repoGroupId, repoArtifactId, repoVersionString)) { + final String sha256Hex = DigestUtils.sha256Hex(repoVersionInputStream); + + final String repoSha256Hex = extensionRepoClient.getVersionSha256(bundlesBucketName, repoGroupId, repoArtifactId, repoVersionString); + assertEquals(sha256Hex, repoSha256Hex); + + final Optional repoSha256HexOptional = extensionRepoClient.getVersionSha256(repoGroupId, repoArtifactId, repoVersionString); + assertTrue(repoSha256HexOptional.isPresent()); + assertEquals(sha256Hex, repoSha256HexOptional.get()); + + final List extensionList = extensionRepoClient.getVersionExtensions(bundlesBucketName, repoGroupId, repoArtifactId, repoVersionString); + assertNotNull(extensionList); + assertTrue(extensionList.size() > 0); + extensionList.forEach(em -> { + assertNotNull(em.getExtensionMetadata()); + assertNotNull(em.getLink()); + assertNotNull(em.getLinkDocs()); + }); + + final String extensionName = extensionList.get(0).getExtensionMetadata().getName(); + final Extension extension = extensionRepoClient.getVersionExtension( + bundlesBucketName, repoGroupId, repoArtifactId, repoVersionString, extensionName); + assertNotNull(extension); + assertEquals(extensionName, extension.getName()); + + // verify getting the docs for an extension from extension repo + try (final InputStream docsInput = extensionRepoClient.getVersionExtensionDocs( + bundlesBucketName, repoGroupId, repoArtifactId, repoVersionString, extensionName) + ) { + final String docsContent = IOUtils.toString(docsInput, StandardCharsets.UTF_8); + assertNotNull(docsContent); + assertTrue(docsContent.startsWith("")); + } + } + + final Optional repoSha256HexDoesNotExist = extensionRepoClient.getVersionSha256(repoGroupId, repoArtifactId, "DOES-NOT-EXIST"); + assertFalse(repoSha256HexDoesNotExist.isPresent()); + + // since we uploaded two snapshot versions, make sure when we retrieve the sha that it's for the second snapshot that replaced the first + final Optional fooNarV2SnapshotLatestSha = extensionRepoClient.getVersionSha256( + createdFooNarV2SnapshotB3.getBundle().getGroupId(), + createdFooNarV2SnapshotB3.getBundle().getArtifactId(), + createdFooNarV2SnapshotB2.getVersionMetadata().getVersion()); + assertTrue(fooNarV2SnapshotLatestSha.isPresent()); + assertEquals(calculateSha256Hex(fooNarV2SnapshotB3), fooNarV2SnapshotLatestSha.get()); + + // ---------------------- TEST EXTENSIONS -------------------------- // + + final ExtensionClient extensionClient = client.getExtensionClient(); + + final List tagCounts = extensionClient.getTagCounts(); + assertNotNull(tagCounts); + assertTrue(tagCounts.size() > 0); + tagCounts.forEach(tc -> { + assertNotNull(tc.getTag()); + }); + + final ExtensionMetadataContainer allExtensions = extensionClient.findExtensions((ExtensionFilterParams)null); + assertNotNull(allExtensions); + assertNotNull(allExtensions.getExtensions()); + assertEquals(4, allExtensions.getNumResults()); + assertEquals(4, allExtensions.getExtensions().size()); + + allExtensions.getExtensions().forEach(e -> { + assertNotNull(e.getName()); + assertNotNull(e.getDisplayName()); + assertNotNull(e.getLink()); + assertNotNull(e.getLinkDocs()); + }); + + final ExtensionMetadataContainer processorExtensions = extensionClient.findExtensions( + new ExtensionFilterParams.Builder().extensionType(ExtensionType.PROCESSOR).build()); + assertNotNull(processorExtensions); + assertNotNull(processorExtensions.getExtensions()); + assertEquals(3, processorExtensions.getNumResults()); + assertEquals(3, processorExtensions.getExtensions().size()); + + final ExtensionMetadataContainer serviceExtensions = extensionClient.findExtensions( + new ExtensionFilterParams.Builder().extensionType(ExtensionType.CONTROLLER_SERVICE).build()); + assertNotNull(serviceExtensions); + assertNotNull(serviceExtensions.getExtensions()); + assertEquals(1, serviceExtensions.getNumResults()); + assertEquals(1, serviceExtensions.getExtensions().size()); + + final ExtensionMetadataContainer narExtensions = extensionClient.findExtensions( + new ExtensionFilterParams.Builder().bundleType(BundleType.NIFI_NAR).build()); + assertNotNull(narExtensions); + assertNotNull(narExtensions.getExtensions()); + assertEquals(4, narExtensions.getNumResults()); + assertEquals(4, narExtensions.getExtensions().size()); + + final ProvidedServiceAPI serviceAPI = new ProvidedServiceAPI(); + serviceAPI.setClassName("org.apache.nifi.service.TestService"); + serviceAPI.setGroupId("org.apache.nifi"); + serviceAPI.setArtifactId("nifi-test-service-api-nar"); + serviceAPI.setVersion("1.0.0"); + + final ExtensionMetadataContainer providedTestServiceApi = extensionClient.findExtensions(serviceAPI); + assertNotNull(providedTestServiceApi); + assertNotNull(providedTestServiceApi.getExtensions()); + assertEquals(1, providedTestServiceApi.getNumResults()); + assertEquals(1, providedTestServiceApi.getExtensions().size()); + assertEquals("org.apache.nifi.service.TestServiceImpl", providedTestServiceApi.getExtensions().first().getName()); + + // ---------------------- TEST ITEMS -------------------------- // + + final ItemsClient itemsClient = client.getItemsClient(); + + // get fields + final Fields itemFields = itemsClient.getFields(); + assertNotNull(itemFields.getFields()); + assertTrue(itemFields.getFields().size() > 0); + + // get all items + final List allItems = itemsClient.getAll(); + assertEquals(5, allItems.size()); + allItems.stream().forEach(i -> { + assertNotNull(i.getBucketName()); + assertNotNull(i.getLink()); + }); + allItems.stream().forEach(i -> LOGGER.info("All items, item " + i.getIdentifier())); + + // verify 2 flow items + final List flowItems = allItems.stream() + .filter(i -> i.getType() == BucketItemType.Flow) + .collect(Collectors.toList()); + assertEquals(2, flowItems.size()); + + // verify 3 bundle items + final List extensionBundleItems = allItems.stream() + .filter(i -> i.getType() == BucketItemType.Bundle) + .collect(Collectors.toList()); + assertEquals(3, extensionBundleItems.size()); + + // get items for bucket + final List bucketItems = itemsClient.getByBucket(flowsBucket.getIdentifier()); + assertEquals(2, bucketItems.size()); + allItems.stream().forEach(i -> assertNotNull(i.getBucketName())); + bucketItems.stream().forEach(i -> LOGGER.info("Items in bucket, item " + i.getIdentifier())); + + // ----------------------- TEST DIFF ---------------------------// + + final VersionedFlowSnapshot snapshot3 = buildSnapshot(snapshotFlow, 3); + final VersionedProcessGroup newlyAddedPG = new VersionedProcessGroup(); + newlyAddedPG.setIdentifier("new-pg"); + newlyAddedPG.setName("NEW Process Group"); + snapshot3.getFlowContents().getProcessGroups().add(newlyAddedPG); + snapshotClient.create(snapshot3); + + VersionedFlowDifference diff = flowClient.diff(snapshotFlow.getBucketIdentifier(), snapshotFlow.getIdentifier(), 3, 2); + assertNotNull(diff); + assertEquals(1, diff.getComponentDifferenceGroups().size()); + + // ---------------------- DELETE DATA --------------------------// + + final VersionedFlow deletedFlow1 = flowClient.delete(flowsBucket.getIdentifier(), updatedFlow1.getIdentifier(), updatedFlow1.getRevision()); + assertNotNull(deletedFlow1); + LOGGER.info("Deleted flow " + deletedFlow1.getIdentifier()); + + final VersionedFlow deletedFlow2 = flowClient.delete(flowsBucket.getIdentifier(), flow2.getIdentifier(), flow2.getRevision()); + assertNotNull(deletedFlow2); + LOGGER.info("Deleted flow " + deletedFlow2.getIdentifier()); + + final Bundle deletedBundle1 = bundleClient.delete(testNarV1Bundle.getIdentifier()); + assertNotNull(deletedBundle1); + LOGGER.info("Deleted extension bundle " + deletedBundle1.getIdentifier()); + + final Bundle deletedBundle2 = bundleClient.delete(fooNarV1Bundle.getIdentifier()); + assertNotNull(deletedBundle2); + LOGGER.info("Deleted extension bundle " + deletedBundle2.getIdentifier()); + + // delete each bucket + for (final Bucket bucket : createdBuckets) { + final Bucket deletedBucket = bucketClient.delete(bucket.getIdentifier(), bucket.getRevision()); + assertNotNull(deletedBucket); + LOGGER.info("Deleted bucket " + deletedBucket.getIdentifier()); + } + assertEquals(0, bucketClient.getAll().size()); + + LOGGER.info("!!! SUCCESS !!!"); + + } + + @Test + public void testFlowSnapshotsWithParameterContextAndEncodingVersion() throws IOException, NiFiRegistryException { + final RevisionInfo initialRevision = new RevisionInfo(null, 0L); + + // Create a bucket + final Bucket bucket = new Bucket(); + bucket.setName("Test Bucket"); + bucket.setRevision(initialRevision); + + final Bucket createdBucket = client.getBucketClient().create(bucket); + assertNotNull(createdBucket); + + // Create the flow + final VersionedFlow flow = new VersionedFlow(); + flow.setName("My Flow"); + flow.setBucketIdentifier(createdBucket.getIdentifier()); + flow.setRevision(initialRevision); + + final VersionedFlow createdFlow = client.getFlowClient().create(flow); + assertNotNull(createdFlow); + + // Create a param context + final VersionedParameter param1 = new VersionedParameter(); + param1.setName("Param 1"); + param1.setValue("Param 1 Value"); + param1.setDescription("Description"); + + final VersionedParameter param2 = new VersionedParameter(); + param2.setName("Param 2"); + param2.setValue("Param 2 Value"); + param2.setDescription("Description"); + param2.setSensitive(true); + + final VersionedParameterContext context1 = new VersionedParameterContext(); + context1.setName("Parameter Context 1"); + context1.setParameters(new HashSet<>(Arrays.asList(param1, param2))); + + final Map contexts = new HashMap<>(); + contexts.put(context1.getName(), context1); + + // Create an external controller service reference + final ExternalControllerServiceReference serviceReference = new ExternalControllerServiceReference(); + serviceReference.setName("External Service 1"); + serviceReference.setIdentifier(UUID.randomUUID().toString()); + + final Map serviceReferences = new HashMap<>(); + serviceReferences.put(serviceReference.getIdentifier(), serviceReference); + + // Create the snapshot + final VersionedFlowSnapshot snapshot = buildSnapshot(createdFlow, 1); + snapshot.setFlowEncodingVersion("2.0.0"); + snapshot.setParameterContexts(contexts); + snapshot.setExternalControllerServices(serviceReferences); + + final VersionedFlowSnapshot createdSnapshot = client.getFlowSnapshotClient().create(snapshot); + assertNotNull(createdSnapshot); + assertNotNull(createdSnapshot.getFlowEncodingVersion()); + assertNotNull(createdSnapshot.getParameterContexts()); + assertNotNull(createdSnapshot.getExternalControllerServices()); + assertEquals(snapshot.getFlowEncodingVersion(), createdSnapshot.getFlowEncodingVersion()); + assertEquals(1, createdSnapshot.getParameterContexts().size()); + assertEquals(1, createdSnapshot.getExternalControllerServices().size()); + + // Retrieve the snapshot + final VersionedFlowSnapshot retrievedSnapshot = client.getFlowSnapshotClient().get( + createdSnapshot.getSnapshotMetadata().getFlowIdentifier(), + createdSnapshot.getSnapshotMetadata().getVersion()); + assertNotNull(retrievedSnapshot); + assertNotNull(retrievedSnapshot.getFlowEncodingVersion()); + assertNotNull(retrievedSnapshot.getParameterContexts()); + assertNotNull(retrievedSnapshot.getExternalControllerServices()); + assertEquals(snapshot.getFlowEncodingVersion(), retrievedSnapshot.getFlowEncodingVersion()); + assertEquals(1, retrievedSnapshot.getParameterContexts().size()); + assertEquals(1, retrievedSnapshot.getExternalControllerServices().size()); + } + + private void checkExtensionMetadata(Collection extensions) { + extensions.forEach(e -> { + assertNotNull(e.getBundleInfo()); + assertNotNull(e.getBundleInfo().getBucketId()); + assertNotNull(e.getBundleInfo().getBucketName()); + assertNotNull(e.getBundleInfo().getBundleId()); + assertNotNull(e.getBundleInfo().getGroupId()); + assertNotNull(e.getBundleInfo().getArtifactId()); + assertNotNull(e.getBundleInfo().getVersion()); + }); + } + + private BundleVersion createExtensionBundleVersionWithStream(final Bucket bundlesBucket, + final BundleVersionClient bundleVersionClient, + final String narFile, final String sha256) + throws IOException, NiFiRegistryException { + + final BundleVersion createdBundleVersion; + try (final InputStream bundleInputStream = new FileInputStream(narFile)) { + if (StringUtils.isBlank(sha256)) { + createdBundleVersion = bundleVersionClient.create( + bundlesBucket.getIdentifier(), BundleType.NIFI_NAR, bundleInputStream); + } else { + createdBundleVersion = bundleVersionClient.create( + bundlesBucket.getIdentifier(), BundleType.NIFI_NAR, bundleInputStream, sha256); + } + } + + assertNotNull(createdBundleVersion); + assertNotNull(createdBundleVersion.getBucket()); + assertNotNull(createdBundleVersion.getBundle()); + assertNotNull(createdBundleVersion.getVersionMetadata()); + + return createdBundleVersion; + } + + private BundleVersion createExtensionBundleVersionWithFile(final Bucket bundlesBucket, + final BundleVersionClient bundleVersionClient, + final String narFile, final String sha256) + throws IOException, NiFiRegistryException { + + final BundleVersion createdBundleVersion; + if (StringUtils.isBlank(sha256)) { + createdBundleVersion = bundleVersionClient.create( + bundlesBucket.getIdentifier(), BundleType.NIFI_NAR, new File(narFile)); + } else { + createdBundleVersion = bundleVersionClient.create( + bundlesBucket.getIdentifier(), BundleType.NIFI_NAR, new File(narFile), sha256); + } + + assertNotNull(createdBundleVersion); + assertNotNull(createdBundleVersion.getBucket()); + assertNotNull(createdBundleVersion.getBundle()); + assertNotNull(createdBundleVersion.getVersionMetadata()); + + return createdBundleVersion; + } + + private String calculateSha256Hex(final String narFile) throws IOException { + try (final InputStream bundleInputStream = new FileInputStream(narFile)) { + return Hex.toHexString(DigestUtils.sha256(bundleInputStream)); + } + } + + private static Bucket createBucket(BucketClient bucketClient, int num) throws IOException, NiFiRegistryException { + final Bucket bucket = new Bucket(); + bucket.setName("Bucket #" + num); + bucket.setDescription("This is bucket #" + num); + bucket.setRevision(new RevisionInfo(CLIENT_ID, 0L)); + return bucketClient.create(bucket); + } + + private static VersionedFlow createFlow(FlowClient client, Bucket bucket, int num) throws IOException, NiFiRegistryException { + final VersionedFlow versionedFlow = new VersionedFlow(); + versionedFlow.setName(bucket.getName() + " Flow #" + num); + versionedFlow.setDescription("This is " + bucket.getName() + " flow #" + num); + versionedFlow.setBucketIdentifier(bucket.getIdentifier()); + versionedFlow.setRevision(new RevisionInfo(CLIENT_ID, 0L)); + return client.create(versionedFlow); + } + + private static VersionedFlowSnapshot buildSnapshot(VersionedFlow flow, int num) { + final VersionedFlowSnapshotMetadata snapshotMetadata = new VersionedFlowSnapshotMetadata(); + snapshotMetadata.setBucketIdentifier(flow.getBucketIdentifier()); + snapshotMetadata.setFlowIdentifier(flow.getIdentifier()); + snapshotMetadata.setVersion(num); + snapshotMetadata.setComments("This is snapshot #" + num); + + final VersionedProcessGroup rootProcessGroup = new VersionedProcessGroup(); + rootProcessGroup.setIdentifier("root-pg"); + rootProcessGroup.setName("Root Process Group"); + + final VersionedProcessGroup subProcessGroup = new VersionedProcessGroup(); + subProcessGroup.setIdentifier("sub-pg"); + subProcessGroup.setName("Sub Process Group"); + rootProcessGroup.getProcessGroups().add(subProcessGroup); + + final Map processorProperties = new HashMap<>(); + processorProperties.put("Prop 1", "Val 1"); + processorProperties.put("Prop 2", "Val 2"); + + final Map propertyDescriptors = new HashMap<>(); + + final VersionedProcessor processor1 = new VersionedProcessor(); + processor1.setIdentifier("p1"); + processor1.setName("Processor 1"); + processor1.setProperties(processorProperties); + processor1.setPropertyDescriptors(propertyDescriptors); + + final VersionedProcessor processor2 = new VersionedProcessor(); + processor2.setIdentifier("p2"); + processor2.setName("Processor 2"); + processor2.setProperties(processorProperties); + processor2.setPropertyDescriptors(propertyDescriptors); + + subProcessGroup.getProcessors().add(processor1); + subProcessGroup.getProcessors().add(processor2); + + final VersionedFlowSnapshot snapshot = new VersionedFlowSnapshot(); + snapshot.setSnapshotMetadata(snapshotMetadata); + snapshot.setFlowContents(rootProcessGroup); + return snapshot; + } + + private static VersionedFlowSnapshot createSnapshot(FlowSnapshotClient client, VersionedFlow flow, int num) throws IOException, NiFiRegistryException { + final VersionedFlowSnapshot snapshot = buildSnapshot(flow, num); + + return client.create(snapshot); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredNoRevisionsITBase.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredNoRevisionsITBase.java new file mode 100644 index 0000000000..cabe25edc7 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredNoRevisionsITBase.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.api; + +import org.apache.nifi.registry.NiFiRegistryTestApiApplication; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * Deploy the Web API Application using an embedded Jetty Server for local integration testing, with the follow characteristics: + * + * - A NiFiRegistryProperties has to be explicitly provided to the ApplicationContext using a profile unique to this test suite. + * - The database is embed H2 using volatile (in-memory) persistence + * - Custom SQL is clearing the DB before each test method by default, unless method overrides this behavior + */ +@RunWith(SpringRunner.class) +@SpringBootTest( + classes = NiFiRegistryTestApiApplication.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = "spring.profiles.include=ITUnsecuredNoRevisions") +@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:db/clearDB.sql") +public class UnsecuredNoRevisionsITBase extends IntegrationTestBase { + + // Tests cases defined in subclasses + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/link/TestLinkService.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/link/TestLinkService.java new file mode 100644 index 0000000000..5303146287 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/link/TestLinkService.java @@ -0,0 +1,361 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.link; + +import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.bucket.BucketItem; +import org.apache.nifi.registry.bucket.BucketItemType; +import org.apache.nifi.registry.extension.bundle.Bundle; +import org.apache.nifi.registry.extension.bundle.BundleInfo; +import org.apache.nifi.registry.extension.bundle.BundleVersionMetadata; +import org.apache.nifi.registry.extension.component.ExtensionMetadata; +import org.apache.nifi.registry.extension.repo.ExtensionRepoArtifact; +import org.apache.nifi.registry.extension.repo.ExtensionRepoBucket; +import org.apache.nifi.registry.extension.repo.ExtensionRepoExtensionMetadata; +import org.apache.nifi.registry.extension.repo.ExtensionRepoGroup; +import org.apache.nifi.registry.extension.repo.ExtensionRepoVersionSummary; +import org.apache.nifi.registry.flow.VersionedFlow; +import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import javax.ws.rs.core.UriBuilder; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + +public class TestLinkService { + + private static final String BASE_URI = "http://localhost:18080/nifi-registry-api"; + private URI baseUri = UriBuilder.fromUri(BASE_URI).build(); + + private LinkService linkService; + + private List buckets; + private List flows; + private List snapshots; + private List items; + + private List bundles; + private List bundleVersionMetadata; + private List extensionMetadata; + + private List extensionRepoBuckets; + private List extensionRepoGroups; + private List extensionRepoArtifacts; + private List extensionRepoVersions; + private List extensionRepoExtensionMetadata; + + @Before + public void setup() { + linkService = new LinkService(); + + // setup buckets + final Bucket bucket1 = new Bucket(); + bucket1.setIdentifier("b1"); + bucket1.setName("Bucket_1"); + + final Bucket bucket2 = new Bucket(); + bucket2.setIdentifier("b2"); + bucket2.setName("Bucket_2"); + + buckets = new ArrayList<>(); + buckets.add(bucket1); + buckets.add(bucket2); + + // setup flows + final VersionedFlow flow1 = new VersionedFlow(); + flow1.setIdentifier("f1"); + flow1.setName("Flow_1"); + flow1.setBucketIdentifier(bucket1.getIdentifier()); + + final VersionedFlow flow2 = new VersionedFlow(); + flow2.setIdentifier("f2"); + flow2.setName("Flow_2"); + flow2.setBucketIdentifier(bucket1.getIdentifier()); + + flows = new ArrayList<>(); + flows.add(flow1); + flows.add(flow2); + + //setup snapshots + final VersionedFlowSnapshotMetadata snapshotMetadata1 = new VersionedFlowSnapshotMetadata(); + snapshotMetadata1.setFlowIdentifier(flow1.getIdentifier()); + snapshotMetadata1.setVersion(1); + snapshotMetadata1.setBucketIdentifier(bucket1.getIdentifier()); + + final VersionedFlowSnapshotMetadata snapshotMetadata2 = new VersionedFlowSnapshotMetadata(); + snapshotMetadata2.setFlowIdentifier(flow1.getIdentifier()); + snapshotMetadata2.setVersion(2); + snapshotMetadata2.setBucketIdentifier(bucket1.getIdentifier()); + + snapshots = new ArrayList<>(); + snapshots.add(snapshotMetadata1); + snapshots.add(snapshotMetadata2); + + // setup extension bundles + final Bundle bundle1 = new Bundle(); + bundle1.setIdentifier("eb1"); + + final Bundle bundle2 = new Bundle(); + bundle2.setIdentifier("eb2"); + + bundles = new ArrayList<>(); + bundles.add(bundle1); + bundles.add(bundle2); + + // setup extension bundle versions + final BundleVersionMetadata bundleVersion1 = new BundleVersionMetadata(); + bundleVersion1.setBundleId(bundle1.getIdentifier()); + bundleVersion1.setVersion("1.0.0"); + + final BundleVersionMetadata bundleVersion2 = new BundleVersionMetadata(); + bundleVersion2.setBundleId(bundle1.getIdentifier()); + bundleVersion2.setVersion("2.0.0"); + + bundleVersionMetadata = new ArrayList<>(); + bundleVersionMetadata.add(bundleVersion1); + bundleVersionMetadata.add(bundleVersion2); + + // setup extension metadata + final BundleInfo bundleInfo1 = new BundleInfo(); + bundleInfo1.setBucketName("Bucket1"); + bundleInfo1.setBucketId("Bucket1"); + bundleInfo1.setBundleId("bundle1"); + bundleInfo1.setGroupId("Group1"); + bundleInfo1.setArtifactId("Artifact1"); + bundleInfo1.setVersion("1"); + + final ExtensionMetadata extensionMetadata1 = new ExtensionMetadata(); + extensionMetadata1.setName("Extension1"); + extensionMetadata1.setBundleInfo(bundleInfo1); + + final ExtensionMetadata extensionMetadata2 = new ExtensionMetadata(); + extensionMetadata2.setName("Extension2"); + extensionMetadata2.setBundleInfo(bundleInfo1); + + extensionMetadata = new ArrayList<>(); + extensionMetadata.add(extensionMetadata1); + extensionMetadata.add(extensionMetadata2); + + // setup extension repo buckets + final ExtensionRepoBucket rb1 = new ExtensionRepoBucket(); + rb1.setBucketName(bucket1.getName()); + + final ExtensionRepoBucket rb2 = new ExtensionRepoBucket(); + rb2.setBucketName(bucket2.getName()); + + extensionRepoBuckets = new ArrayList<>(); + extensionRepoBuckets.add(rb1); + extensionRepoBuckets.add(rb2); + + // setup extension repo groups + final ExtensionRepoGroup rg1 = new ExtensionRepoGroup(); + rg1.setBucketName(rb1.getBucketName()); + rg1.setGroupId("g1"); + + final ExtensionRepoGroup rg2 = new ExtensionRepoGroup(); + rg2.setBucketName(rb1.getBucketName()); + rg2.setGroupId("g2"); + + extensionRepoGroups = new ArrayList<>(); + extensionRepoGroups.add(rg1); + extensionRepoGroups.add(rg2); + + // setup extension repo artifacts + final ExtensionRepoArtifact ra1 = new ExtensionRepoArtifact(); + ra1.setBucketName(rb1.getBucketName()); + ra1.setGroupId(rg1.getGroupId()); + ra1.setArtifactId("a1"); + + final ExtensionRepoArtifact ra2 = new ExtensionRepoArtifact(); + ra2.setBucketName(rb1.getBucketName()); + ra2.setGroupId(rg1.getGroupId()); + ra2.setArtifactId("a2"); + + extensionRepoArtifacts = new ArrayList<>(); + extensionRepoArtifacts.add(ra1); + extensionRepoArtifacts.add(ra2); + + // setup extension repo versions + final ExtensionRepoVersionSummary rv1 = new ExtensionRepoVersionSummary(); + rv1.setBucketName(rb1.getBucketName()); + rv1.setGroupId(rg1.getGroupId()); + rv1.setArtifactId(ra1.getArtifactId()); + rv1.setVersion("1.0.0"); + + final ExtensionRepoVersionSummary rv2 = new ExtensionRepoVersionSummary(); + rv2.setBucketName(rb1.getBucketName()); + rv2.setGroupId(rg1.getGroupId()); + rv2.setArtifactId(ra1.getArtifactId()); + rv2.setVersion("2.0.0"); + + extensionRepoVersions = new ArrayList<>(); + extensionRepoVersions.add(rv1); + extensionRepoVersions.add(rv2); + + // setup extension repo extension metadata + extensionRepoExtensionMetadata = new ArrayList<>(); + extensionRepoExtensionMetadata.add(new ExtensionRepoExtensionMetadata(extensionMetadata1)); + extensionRepoExtensionMetadata.add(new ExtensionRepoExtensionMetadata(extensionMetadata2)); + + // setup items + items = new ArrayList<>(); + items.add(flow1); + items.add(flow2); + items.add(bundle1); + items.add(bundle2); + } + + @Test + public void testPopulateBucketLinks() { + buckets.forEach(b -> Assert.assertNull(b.getLink())); + linkService.populateLinks(buckets); + buckets.forEach(b -> Assert.assertEquals( + "buckets/" + b.getIdentifier(), b.getLink().getUri().toString())); + } + + @Test + public void testPopulateFlowLinks() { + flows.forEach(f -> Assert.assertNull(f.getLink())); + linkService.populateLinks(flows); + flows.forEach(f -> Assert.assertEquals( + "buckets/" + f.getBucketIdentifier() + "/flows/" + f.getIdentifier(), f.getLink().getUri().toString())); + } + + @Test + public void testPopulateSnapshotLinks() { + snapshots.forEach(s -> Assert.assertNull(s.getLink())); + linkService.populateLinks(snapshots); + snapshots.forEach(s -> Assert.assertEquals( + "buckets/" + s.getBucketIdentifier() + "/flows/" + s.getFlowIdentifier() + "/versions/" + s.getVersion(), s.getLink().getUri().toString())); + } + + @Test + public void testPopulateItemLinks() { + items.forEach(i -> Assert.assertNull(i.getLink())); + linkService.populateLinks(items); + items.forEach(i -> { + if (i.getType() == BucketItemType.Flow) { + Assert.assertEquals("buckets/" + i.getBucketIdentifier() + "/flows/" + i.getIdentifier(), i.getLink().getUri().toString()); + } else { + Assert.assertEquals("bundles/" + i.getIdentifier(), i.getLink().getUri().toString()); + } + }); + } + + @Test + public void testPopulateExtensionBundleLinks() { + bundles.forEach(i -> Assert.assertNull(i.getLink())); + linkService.populateLinks(bundles); + bundles.forEach(eb -> Assert.assertEquals("bundles/" + eb.getIdentifier(), eb.getLink().getUri().toString())); + } + + @Test + public void testPopulateExtensionBundleVersionLinks() { + bundleVersionMetadata.forEach(i -> Assert.assertNull(i.getLink())); + linkService.populateLinks(bundleVersionMetadata); + bundleVersionMetadata.forEach(eb -> Assert.assertEquals( + "bundles/" + eb.getBundleId() + "/versions/" + eb.getVersion(), eb.getLink().getUri().toString())); + } + + @Test + public void testPopulateExtensionBundleVersionExtensionMetadataLinks() { + extensionMetadata.forEach(i -> Assert.assertNull(i.getLink())); + extensionMetadata.forEach(i -> Assert.assertNull(i.getLinkDocs())); + + linkService.populateLinks(extensionMetadata); + + extensionMetadata.forEach(e -> { + final String extensionUri = "bundles/" + e.getBundleInfo().getBundleId() + + "/versions/" + e.getBundleInfo().getVersion() + + "/extensions/" + e.getName(); + Assert.assertEquals(extensionUri, e.getLink().getUri().toString()); + Assert.assertEquals(extensionUri + "/docs", e.getLinkDocs().getUri().toString()); + }); + } + + @Test + public void testPopulateExtensionRepoBucketLinks() { + extensionRepoBuckets.forEach(i -> Assert.assertNull(i.getLink())); + linkService.populateLinks(extensionRepoBuckets); + extensionRepoBuckets.forEach(i -> Assert.assertEquals( + "extension-repository/" + i.getBucketName(), + i.getLink().getUri().toString()) + ); + } + + @Test + public void testPopulateExtensionRepoGroupLinks() { + extensionRepoGroups.forEach(i -> Assert.assertNull(i.getLink())); + linkService.populateLinks(extensionRepoGroups); + extensionRepoGroups.forEach(i -> { + Assert.assertEquals( + "extension-repository/" + i.getBucketName() + "/" + i.getGroupId(), + i.getLink().getUri().toString()); } + ); + } + + @Test + public void testPopulateExtensionRepoArtifactLinks() { + extensionRepoArtifacts.forEach(i -> Assert.assertNull(i.getLink())); + linkService.populateLinks(extensionRepoArtifacts); + extensionRepoArtifacts.forEach(i -> { + Assert.assertEquals( + "extension-repository/" + i.getBucketName() + "/" + i.getGroupId() + "/" + i.getArtifactId(), + i.getLink().getUri().toString()); } + ); + } + + @Test + public void testPopulateExtensionRepoVersionLinks() { + extensionRepoVersions.forEach(i -> Assert.assertNull(i.getLink())); + linkService.populateLinks(extensionRepoVersions); + extensionRepoVersions.forEach(i -> { + Assert.assertEquals( + "extension-repository/" + i.getBucketName() + "/" + i.getGroupId() + "/" + i.getArtifactId() + "/" + i.getVersion(), + i.getLink().getUri().toString()); } + ); + } + + @Test + public void testPopulateExtensionRepoVersionFullLinks() { + extensionRepoVersions.forEach(i -> Assert.assertNull(i.getLink())); + linkService.populateFullLinks(extensionRepoVersions, baseUri); + extensionRepoVersions.forEach(i -> { + Assert.assertEquals( + BASE_URI + "/extension-repository/" + i.getBucketName() + "/" + i.getGroupId() + "/" + i.getArtifactId() + "/" + i.getVersion(), + i.getLink().getUri().toString()); } + ); + } + + @Test + public void testPopulateExtensionRepoExtensionMetdataFullLinks() { + extensionRepoExtensionMetadata.forEach(i -> Assert.assertNull(i.getLink())); + extensionRepoExtensionMetadata.forEach(i -> Assert.assertNull(i.getLinkDocs())); + + linkService.populateFullLinks(extensionRepoExtensionMetadata, baseUri); + extensionRepoExtensionMetadata.forEach(i -> { + final BundleInfo bi = i.getExtensionMetadata().getBundleInfo(); + final String extensionUri = BASE_URI + "/extension-repository/" + bi.getBucketName() + "/" + bi.getGroupId() + "/" + + bi.getArtifactId() + "/" + bi.getVersion() + "/extensions/" + i.getExtensionMetadata().getName(); + Assert.assertEquals(extensionUri, i.getLink().getUri().toString()); + Assert.assertEquals(extensionUri + "/docs", i.getLinkDocs().getUri().toString()); + }); + } +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/security/authentication/oidc/OidcServiceTest.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/security/authentication/oidc/OidcServiceTest.java new file mode 100644 index 0000000000..c3e5701d7c --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/security/authentication/oidc/OidcServiceTest.java @@ -0,0 +1,154 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.security.authentication.oidc; + +import com.nimbusds.oauth2.sdk.AuthorizationCode; +import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant; +import com.nimbusds.oauth2.sdk.id.State; +import org.junit.Test; + +import java.net.URI; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class OidcServiceTest { + + public static final String TEST_REQUEST_IDENTIFIER = "test-request-identifier"; + public static final String TEST_STATE = "test-state"; + + @Test(expected = IllegalStateException.class) + public void testOidcNotEnabledCreateState() throws Exception { + final OidcService service = getServiceWithNoOidcSupport(); + service.createState(TEST_REQUEST_IDENTIFIER); + } + + @Test(expected = IllegalStateException.class) + public void testCreateStateMultipleInvocations() throws Exception { + final OidcService service = getServiceWithOidcSupport(); + service.createState(TEST_REQUEST_IDENTIFIER); + service.createState(TEST_REQUEST_IDENTIFIER); + } + + @Test(expected = IllegalStateException.class) + public void testOidcNotEnabledValidateState() throws Exception { + final OidcService service = getServiceWithNoOidcSupport(); + service.isStateValid(TEST_REQUEST_IDENTIFIER, new State(TEST_STATE)); + } + + @Test + public void testOidcUnknownState() throws Exception { + final OidcService service = getServiceWithOidcSupport(); + assertFalse(service.isStateValid(TEST_REQUEST_IDENTIFIER, new State(TEST_STATE))); + } + + @Test + public void testValidateState() throws Exception { + final OidcService service = getServiceWithOidcSupport(); + final State state = service.createState(TEST_REQUEST_IDENTIFIER); + assertTrue(service.isStateValid(TEST_REQUEST_IDENTIFIER, state)); + } + + @Test + public void testValidateStateExpiration() throws Exception { + final OidcService service = getServiceWithOidcSupportAndCustomExpiration(1, TimeUnit.SECONDS); + final State state = service.createState(TEST_REQUEST_IDENTIFIER); + + Thread.sleep(3 * 1000); + + assertFalse(service.isStateValid(TEST_REQUEST_IDENTIFIER, state)); + } + + @Test(expected = IllegalStateException.class) + public void testOidcNotEnabledExchangeCode() throws Exception { + final OidcService service = getServiceWithNoOidcSupport(); + service.exchangeAuthorizationCode(TEST_REQUEST_IDENTIFIER, getAuthorizationCodeGrant()); + } + + @Test(expected = IllegalStateException.class) + public void testExchangeCodeMultipleInvocation() throws Exception { + final OidcService service = getServiceWithOidcSupport(); + service.exchangeAuthorizationCode(TEST_REQUEST_IDENTIFIER, getAuthorizationCodeGrant()); + service.exchangeAuthorizationCode(TEST_REQUEST_IDENTIFIER, getAuthorizationCodeGrant()); + } + + @Test(expected = IllegalStateException.class) + public void testOidcNotEnabledGetJwt() throws Exception { + final OidcService service = getServiceWithNoOidcSupport(); + service.getJwt(TEST_REQUEST_IDENTIFIER); + } + + @Test + public void testGetJwt() throws Exception { + final OidcService service = getServiceWithOidcSupport(); + service.exchangeAuthorizationCode(TEST_REQUEST_IDENTIFIER, getAuthorizationCodeGrant()); + assertNotNull(service.getJwt(TEST_REQUEST_IDENTIFIER)); + } + + @Test + public void testGetJwtExpiration() throws Exception { + final OidcService service = getServiceWithOidcSupportAndCustomExpiration(1, TimeUnit.SECONDS); + service.exchangeAuthorizationCode(TEST_REQUEST_IDENTIFIER, getAuthorizationCodeGrant()); + + Thread.sleep(3 * 1000); + + assertNull(service.getJwt(TEST_REQUEST_IDENTIFIER)); + } + + private OidcService getServiceWithNoOidcSupport() { + final OidcIdentityProvider provider = mock(OidcIdentityProvider.class); + when(provider.isOidcEnabled()).thenReturn(false); + + final OidcService service = new OidcService(provider); + assertFalse(service.isOidcEnabled()); + + return service; + } + + private OidcService getServiceWithOidcSupport() throws Exception { + final OidcIdentityProvider provider = mock(OidcIdentityProvider.class); + when(provider.isOidcEnabled()).thenReturn(true); + when(provider.exchangeAuthorizationCode(any())).then(invocation -> UUID.randomUUID().toString()); + + final OidcService service = new OidcService(provider); + assertTrue(service.isOidcEnabled()); + + return service; + } + + private OidcService getServiceWithOidcSupportAndCustomExpiration(final int duration, final TimeUnit units) throws Exception { + final OidcIdentityProvider provider = mock(OidcIdentityProvider.class); + when(provider.isOidcEnabled()).thenReturn(true); + when(provider.exchangeAuthorizationCode(any())).then(invocation -> UUID.randomUUID().toString()); + + final OidcService service = new OidcService(provider, duration, units); + assertTrue(service.isOidcEnabled()); + + return service; + } + + private AuthorizationCodeGrant getAuthorizationCodeGrant() { + return new AuthorizationCodeGrant(new AuthorizationCode("code"), URI.create("http://localhost:8080/nifi-registry")); + } +} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/security/authentication/oidc/StandardOidcIdentityProviderTest.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/security/authentication/oidc/StandardOidcIdentityProviderTest.java new file mode 100644 index 0000000000..c0b496fec0 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/security/authentication/oidc/StandardOidcIdentityProviderTest.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.security.authentication.oidc; + +import com.nimbusds.oauth2.sdk.Scope; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class StandardOidcIdentityProviderTest { + + @Test + public void testValidateScopes() throws IllegalAccessException { + final String additionalScope_profile = "profile"; + final String additionalScope_abc = "abc"; + + final StandardOidcIdentityProvider provider = createOidcProviderWithAdditionalScopes(additionalScope_profile, + additionalScope_abc); + Scope scope = provider.getScope(); + + // two additional scopes are set, two (openid, email) are hard-coded + assertEquals(scope.toArray().length, 4); + assertTrue(scope.contains("openid")); + assertTrue(scope.contains("email")); + assertTrue(scope.contains(additionalScope_profile)); + assertTrue(scope.contains(additionalScope_abc)); + } + + @Test + public void testNoDuplicatedScopes() throws IllegalAccessException { + final String additionalScopeDuplicate = "abc"; + + final StandardOidcIdentityProvider provider = createOidcProviderWithAdditionalScopes(additionalScopeDuplicate, + "def", additionalScopeDuplicate); + Scope scope = provider.getScope(); + + // three additional scopes are set but one is duplicated and mustn't be returned; note that there is + // another one inserted in between the duplicated; two (openid, email) are hard-coded + assertEquals(scope.toArray().length, 4); + } + + private StandardOidcIdentityProvider createOidcProviderWithAdditionalScopes(String... additionalScopes) throws IllegalAccessException { + final StandardOidcIdentityProvider provider = mock(StandardOidcIdentityProvider.class); + NiFiRegistryProperties properties = createNiFiPropertiesMockWithAdditionalScopes(Arrays.asList(additionalScopes)); + Field propertiesField = FieldUtils.getDeclaredField(StandardOidcIdentityProvider.class, "properties", true); + propertiesField.set(provider, properties); + + when(provider.isOidcEnabled()).thenReturn(true); + when(provider.getScope()).thenCallRealMethod(); + + return provider; + } + + private NiFiRegistryProperties createNiFiPropertiesMockWithAdditionalScopes(List additionalScopes) { + NiFiRegistryProperties properties = mock(NiFiRegistryProperties.class); + when(properties.getOidcAdditionalScopes()).thenReturn(additionalScopes); + return properties; + } +} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITDBFlowStorage.properties b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITDBFlowStorage.properties new file mode 100644 index 0000000000..91e653cc6c --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITDBFlowStorage.properties @@ -0,0 +1,21 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# Integration Test Profile for running an unsecured NiFi Registry instance + +# Custom (non-standard to Spring Boot) properties +nifi.registry.properties.file = src/test/resources/conf/db-flow-storage/nifi-registry.properties \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureDatabase.properties b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureDatabase.properties new file mode 100644 index 0000000000..b5e851e538 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureDatabase.properties @@ -0,0 +1,36 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + + +# Properties for Spring Boot integration tests +# Documentation for common Spring Boot application properties can be found at: +# https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html + + +# Custom (non-standard to Spring Boot) properties +nifi.registry.properties.file: src/test/resources/conf/secure-database/nifi-registry.properties +nifi.registry.client.properties.file: src/test/resources/conf/secure-database/nifi-registry-client.properties + + +# Embedded Server SSL Context Config +server.ssl.client-auth: need +server.ssl.key-store: ./target/test-classes/keys/registry-ks.jks +server.ssl.key-store-password: password +server.ssl.key-password: password +server.ssl.protocol: TLS +server.ssl.trust-store: ./target/test-classes/keys/ca-ts.jks +server.ssl.trust-store-password: password \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureFile.properties b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureFile.properties new file mode 100644 index 0000000000..cea51c6e8e --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureFile.properties @@ -0,0 +1,36 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + + +# Properties for Spring Boot integration tests +# Documentation for common Spring Boot application properties can be found at: +# https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html + + +# Custom (non-standard to Spring Boot) properties +nifi.registry.properties.file: src/test/resources/conf/secure-file/nifi-registry.properties +nifi.registry.client.properties.file: src/test/resources/conf/secure-file/nifi-registry-client.properties + + +# Embedded Server SSL Context Config +server.ssl.client-auth: need +server.ssl.key-store: ./target/test-classes/keys/registry-ks.jks +server.ssl.key-store-password: password +server.ssl.key-password: password +server.ssl.protocol: TLS +server.ssl.trust-store: ./target/test-classes/keys/ca-ts.jks +server.ssl.trust-store-password: password diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureKerberos.properties b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureKerberos.properties new file mode 100644 index 0000000000..fb1c928232 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureKerberos.properties @@ -0,0 +1,36 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + + +# Properties for Spring Boot integration tests +# Documentation for common Spring Boot application properties can be found at: +# https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html + + +# Custom (non-standard to Spring Boot) properties +nifi.registry.properties.file: src/test/resources/conf/secure-kerberos/nifi-registry.properties +nifi.registry.client.properties.file: src/test/resources/conf/secure-kerberos/nifi-registry-client.properties + + +# Embedded Server SSL Context Config +#server.ssl.client-auth: need # server does not require two-way TLS +server.ssl.key-store: ./target/test-classes/keys/registry-ks.jks +server.ssl.key-store-password: password +server.ssl.key-password: password +server.ssl.protocol: TLS +server.ssl.trust-store: ./target/test-classes/keys/ca-ts.jks +server.ssl.trust-store-password: password diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureLdap.properties b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureLdap.properties new file mode 100644 index 0000000000..25b749d98f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureLdap.properties @@ -0,0 +1,48 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + + +# Properties for Spring Boot integration tests +# Documentation for common Spring Boot application properties can be found at: +# https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html + + +# Custom (non-standard to Spring Boot) properties +nifi.registry.properties.file: src/test/resources/conf/secure-ldap/nifi-registry.properties +nifi.registry.client.properties.file: src/test/resources/conf/secure-ldap/nifi-registry-client.properties + + +# Embedded Server SSL Context Config +#server.ssl.client-auth: need # LDAP-configured server does not require two-way TLS +server.ssl.key-store: ./target/test-classes/keys/registry-ks.jks +server.ssl.key-store-password: password +server.ssl.key-password: password +server.ssl.protocol: TLS +server.ssl.trust-store: ./target/test-classes/keys/ca-ts.jks +server.ssl.trust-store-password: password + +# Embedded LDAP Config +spring.ldap.embedded.base-dn: dc=example,dc=com +spring.ldap.embedded.credential.username: cn=read-only-admin,dc=example,dc=com +spring.ldap.embedded.credential.password: password +spring.ldap.embedded.ldif: classpath:conf/secure-ldap/test-ldap-data.ldif +spring.ldap.embedded.port: 8389 +spring.ldap.embedded.validation.enabled: false + +# Additional Logging Config +logging.level.org.springframework.security.ldap: DEBUG +logging.level.org.springframework.ldap: DEBUG \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureProxy.properties b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureProxy.properties new file mode 100644 index 0000000000..cab6d4195f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureProxy.properties @@ -0,0 +1,36 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + + +# Properties for Spring Boot integration tests +# Documentation for common Spring Boot application properties can be found at: +# https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html + + +# Custom (non-standard to Spring Boot) properties +nifi.registry.properties.file: src/test/resources/conf/secure-proxy/nifi-registry.properties +nifi.registry.client.properties.file: src/test/resources/conf/secure-proxy/nifi-registry-client.properties + + +# Embedded Server SSL Context Config +server.ssl.client-auth: need +server.ssl.key-store: ./target/test-classes/keys/registry-ks.jks +server.ssl.key-store-password: password +server.ssl.key-password: password +server.ssl.protocol: TLS +server.ssl.trust-store: ./target/test-classes/keys/ca-ts.jks +server.ssl.trust-store-password: password diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITUnsecured.properties b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITUnsecured.properties new file mode 100644 index 0000000000..bcd338c978 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITUnsecured.properties @@ -0,0 +1,21 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# Integration Test Profile for running an unsecured NiFi Registry instance + +# Custom (non-standard to Spring Boot) properties +nifi.registry.properties.file = src/test/resources/conf/unsecured/nifi-registry.properties diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITUnsecuredNoRevisions.properties b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITUnsecuredNoRevisions.properties new file mode 100644 index 0000000000..5e16e0428e --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITUnsecuredNoRevisions.properties @@ -0,0 +1,21 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# Integration Test Profile for running an unsecured NiFi Registry instance + +# Custom (non-standard to Spring Boot) properties +nifi.registry.properties.file = src/test/resources/conf/unsecured-no-revisions/nifi-registry.properties diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/application.properties b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/application.properties new file mode 100644 index 0000000000..721b949c9e --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/application.properties @@ -0,0 +1,28 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# Properties for Spring Boot integration tests +# Documentation for commoon Spring Boot application properties can be found at: +# https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html + +# These verbose log levels can be enabled locally for dev testing, but disable them in the repo to minimize travis logs. +#logging.level.org.springframework.core.io.support: DEBUG +#logging.level.org.springframework.context.annotation: DEBUG +#logging.level.org.springframework.web: DEBUG + +# Need to allow overriding of beans in integration tests +spring.main.allow-bean-definition-overriding=true \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/banner.txt b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/banner.txt new file mode 100644 index 0000000000..2f5464461f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/banner.txt @@ -0,0 +1,8 @@ + + Apache NiFi _ _ + _ __ ___ __ _(_)___| |_ _ __ _ _ +| '__/ _ \/ _` | / __| __| '__| | | | +| | | __/ (_| | \__ \ |_| | | |_| | +|_| \___|\__, |_|___/\__|_| \__, | +==========|___/================|___/= + Integration Test diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/db-flow-storage/nifi-registry.properties b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/db-flow-storage/nifi-registry.properties new file mode 100644 index 0000000000..dd13679bf6 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/db-flow-storage/nifi-registry.properties @@ -0,0 +1,31 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# web properties # +nifi.registry.web.http.host=localhost + +# providers properties # +nifi.registry.providers.configuration.file=./target/test-classes/conf/providers-db-flow-storage.xml + +# extensions working dir # +nifi.registry.extensions.working.directory=./target/work/extensions + +# database properties +nifi.registry.db.url.append=;LOCK_TIMEOUT=25000;WRITE_DELAY=0;AUTO_SERVER=FALSE + +# enabled revision checking # +nifi.registry.revisions.enabled=true \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/providers-db-flow-storage.xml b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/providers-db-flow-storage.xml new file mode 100644 index 0000000000..2ec132a3a9 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/providers-db-flow-storage.xml @@ -0,0 +1,29 @@ + + + + + + org.apache.nifi.registry.provider.flow.DatabaseFlowPersistenceProvider + + + + org.apache.nifi.registry.provider.extension.FileSystemBundlePersistenceProvider + ./target/test-classes/extension_bundles + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/providers.xml b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/providers.xml new file mode 100644 index 0000000000..89d35a1779 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/providers.xml @@ -0,0 +1,30 @@ + + + + + + org.apache.nifi.registry.provider.flow.FileSystemFlowPersistenceProvider + ./target/test-classes/flow_storage + + + + org.apache.nifi.registry.provider.extension.FileSystemBundlePersistenceProvider + ./target/test-classes/extension_bundles + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-database/authorizers.xml b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-database/authorizers.xml new file mode 100644 index 0000000000..22e9ca3e8c --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-database/authorizers.xml @@ -0,0 +1,80 @@ + + + + + + + + database-user-group-provider + org.apache.nifi.registry.security.authorization.database.DatabaseUserGroupProvider + CN=user1, OU=nifi + CN=user2, OU=nifi + + + + + database-access-policy-provider + org.apache.nifi.registry.security.authorization.database.DatabaseAccessPolicyProvider + database-user-group-provider + CN=user1, OU=nifi + + + + + + + managed-authorizer + org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer + database-access-policy-provider + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-database/nifi-registry-client.properties b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-database/nifi-registry-client.properties new file mode 100644 index 0000000000..5a31413b6b --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-database/nifi-registry-client.properties @@ -0,0 +1,25 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# client security properties # +nifi.registry.security.keystore=./target/test-classes/keys/user1-ks.jks +nifi.registry.security.keystoreType=JKS +nifi.registry.security.keystorePasswd=password +nifi.registry.security.keyPasswd=password +nifi.registry.security.truststore=./target/test-classes/keys/ca-ts.jks +nifi.registry.security.truststoreType=JKS +nifi.registry.security.truststorePasswd=password diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-database/nifi-registry.properties b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-database/nifi-registry.properties new file mode 100644 index 0000000000..bbc16f8942 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-database/nifi-registry.properties @@ -0,0 +1,33 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# web properties # +nifi.registry.web.https.host=localhost +nifi.registry.web.https.port=0 + +# security properties # +# +# ** Server KeyStore and TrustStore configuration set in Spring profile properties for embedded Jetty ** +# +nifi.registry.security.authorizers.configuration.file=./target/test-classes/conf/secure-database/authorizers.xml +nifi.registry.security.authorizer=managed-authorizer + +# providers properties # +nifi.registry.providers.configuration.file=./target/test-classes/conf/providers-db-flow-storage.xml + +# enabled revision checking # +nifi.registry.revisions.enabled=true \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-file/authorizers.xml b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-file/authorizers.xml new file mode 100644 index 0000000000..40911b2675 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-file/authorizers.xml @@ -0,0 +1,143 @@ + + + + + + + + file-user-group-provider + org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider + ./target/test-classes/conf/secure-file/users.xml + CN=user1, OU=nifi + CN=no-access, OU=nifi + + + + + + + + + + + file-access-policy-provider + org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider + file-user-group-provider + ./target/test-classes/conf/secure-file/authorizations.xml + CN=user1, OU=nifi + + + + + + + managed-authorizer + org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer + file-access-policy-provider + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-file/nifi-registry-client.properties b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-file/nifi-registry-client.properties new file mode 100644 index 0000000000..5a31413b6b --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-file/nifi-registry-client.properties @@ -0,0 +1,25 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# client security properties # +nifi.registry.security.keystore=./target/test-classes/keys/user1-ks.jks +nifi.registry.security.keystoreType=JKS +nifi.registry.security.keystorePasswd=password +nifi.registry.security.keyPasswd=password +nifi.registry.security.truststore=./target/test-classes/keys/ca-ts.jks +nifi.registry.security.truststoreType=JKS +nifi.registry.security.truststorePasswd=password diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-file/nifi-registry.properties b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-file/nifi-registry.properties new file mode 100644 index 0000000000..fa2ad4c5e7 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-file/nifi-registry.properties @@ -0,0 +1,33 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# web properties # +nifi.registry.web.https.host=localhost +nifi.registry.web.https.port=0 + +# security properties # +# +# ** Server KeyStore and TrustStore configuration set in Spring profile properties for embedded Jetty ** +# +nifi.registry.security.authorizers.configuration.file=./target/test-classes/conf/secure-file/authorizers.xml +nifi.registry.security.authorizer=managed-authorizer + +# providers properties # +nifi.registry.providers.configuration.file=./target/test-classes/conf/providers.xml + +# enabled revision checking # +nifi.registry.revisions.enabled=true \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/authorizers.xml b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/authorizers.xml new file mode 100644 index 0000000000..d54869647f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/authorizers.xml @@ -0,0 +1,101 @@ + + + + + + + + file-user-group-provider + org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider + ./target/test-classes/conf/secure-kerberos/users.xml + kerberosUser@LOCALHOST + + + + + file-access-policy-provider + org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider + file-user-group-provider + ./target/test-classes/conf/secure-kerberos/authorizations.xml + kerberosUser@LOCALHOST + + + + + + + managed-authorizer + org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer + file-access-policy-provider + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/identity-providers.xml b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/identity-providers.xml new file mode 100644 index 0000000000..cd101ea483 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/identity-providers.xml @@ -0,0 +1,31 @@ + + + + + + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/nifi-registry-client.properties b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/nifi-registry-client.properties new file mode 100644 index 0000000000..59f1243ba3 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/nifi-registry-client.properties @@ -0,0 +1,22 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# client security properties # +# Don't use a client cert for one-way TLS. Client identity will be provided via Kerberos SPNEGO to get JWT +nifi.registry.security.truststore=./target/test-classes/keys/ca-ts.jks +nifi.registry.security.truststoreType=JKS +nifi.registry.security.truststorePasswd=password diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/nifi-registry.properties b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/nifi-registry.properties new file mode 100644 index 0000000000..79a41e321b --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/nifi-registry.properties @@ -0,0 +1,39 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# web properties # +nifi.registry.web.https.host=localhost +nifi.registry.web.https.port=0 + +# security properties # +# +# ** Server KeyStore and TrustStore configuration set in Spring profile properties for embedded Jetty ** +# +nifi.registry.security.authorizers.configuration.file=./target/test-classes/conf/secure-kerberos/authorizers.xml +nifi.registry.security.authorizer=managed-authorizer + +# providers properties # +nifi.registry.providers.configuration.file=./target/test-classes/conf/providers.xml + +# kerberos properties # (aside from expiration, these don't actually matter as the KerberosServiceAuthenticationProvider will be mocked) +nifi.registry.kerberos.krb5.file=/path/to/krb5.conf +nifi.registry.kerberos.spnego.authentication.expiration=12 hours +nifi.registry.kerberos.spnego.principal=HTTP/localhost@LOCALHOST +nifi.registry.kerberos.spnego.keytab.location=/path/to/keytab + +# enabled revision checking # +nifi.registry.revisions.enabled=true \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/authorizers.protected.xml b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/authorizers.protected.xml new file mode 100644 index 0000000000..44007bdd53 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/authorizers.protected.xml @@ -0,0 +1,221 @@ + + + + + + + + ldap-user-group-provider + org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider + SIMPLE + + cn=read-only-admin,dc=example,dc=com + + oVU8w3uH7yZlKscG||Hu4ZtRgZWKISn3DyGuB50rKL1qGceWZp + + + + FOLLOW + 10 secs + 10 secs + + ldap://localhost:8389 + + 30 mins + + dc=example,dc=com + person + ONE_LEVEL + (uid=*) + uid + + + dc=example,dc=com + groupOfUniqueNames + ONE_LEVEL + (ou=*) + ou + uniqueMember + + + + + + + + + + + file-access-policy-provider + org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider + ldap-user-group-provider + ./target/test-classes/conf/secure-ldap/authorizations.xml + nifiadmin + + + + + managed-authorizer + org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer + file-access-policy-provider + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/authorizers.xml b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/authorizers.xml new file mode 100644 index 0000000000..c55e5a24ef --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/authorizers.xml @@ -0,0 +1,242 @@ + + + + + + + + + + + ldap-user-group-provider + org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider + SIMPLE + + cn=read-only-admin,dc=example,dc=com + password + + + + FOLLOW + 10 secs + 10 secs + + ldap://localhost:8389 + + 30 mins + + dc=example,dc=com + person + ONE_LEVEL + (uid=*) + uid + + + dc=example,dc=com + groupOfUniqueNames + ONE_LEVEL + (ou=*) + ou + uniqueMember + + + + + + + + + + + file-access-policy-provider + org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider + ldap-user-group-provider + ./target/test-classes/conf/secure-ldap/authorizations.xml + nifiadmin + + + + + managed-authorizer + org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer + file-access-policy-provider + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/bootstrap.conf b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/bootstrap.conf new file mode 100644 index 0000000000..4bd28baf8b --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/bootstrap.conf @@ -0,0 +1,60 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# Java command to use when running nifi-registry +java=java + +# Username to use when running nifi-registry. This value will be ignored on Windows. +run.as= + +# Configure where nifi-registry's lib and conf directories live +lib.dir=./lib +conf.dir=./conf + +# How long to wait after telling nifi-registry to shutdown before explicitly killing the Process +graceful.shutdown.seconds=20 + +# Disable JSR 199 so that we can use JSP's without running a JDK +java.arg.1=-Dorg.apache.jasper.compiler.disablejsr199=true + +# JVM memory settings +java.arg.2=-Xms512m +java.arg.3=-Xmx512m + +# Enable Remote Debugging +java.arg.debug=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000 + +java.arg.4=-Djava.net.preferIPv4Stack=true + +# allowRestrictedHeaders is required for Cluster/Node communications to work properly +java.arg.5=-Dsun.net.http.allowRestrictedHeaders=true +java.arg.6=-Djava.protocol.handler.pkgs=sun.net.www.protocol + +# Java 7 and below have issues with Code Cache. The following lines allow us to run well even with +# many classes loaded in the JVM. +#java.arg.7=-XX:ReservedCodeCacheSize=256m +#java.arg.8=-XX:CodeCacheFlushingMinimumFreeSpace=10m +#java.arg.9=-XX:+UseCodeCacheFlushing +#java.arg.11=-XX:PermSize=128M +#java.arg.12=-XX:MaxPermSize=128M + +# The G1GC is still considered experimental but has proven to be very advantageous in providing great +# performance without significant "stop-the-world" delays. +#java.arg.10=-XX:+UseG1GC + +# Master key in hexadecimal format for encrypted sensitive configuration values +nifi.registry.bootstrap.sensitive.key=0123456789ABCDEFFEDCBA9876543210 diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/identity-providers.protected.xml b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/identity-providers.protected.xml new file mode 100644 index 0000000000..51336e22c9 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/identity-providers.protected.xml @@ -0,0 +1,89 @@ + + + + + + + ldap-identity-provider + org.apache.nifi.registry.security.ldap.LdapIdentityProvider + SIMPLE + + cn=read-only-admin,dc=example,dc=com + + p43I3jRcK+wPhR3c||oaqMg3YGo2WblTxBJSgI8H9fLMBwQiaM + + FOLLOW + 10 secs + 10 secs + + ldap://localhost:8389 + dc=example,dc=com + (uid={0}) + + USE_USERNAME + 12 hours + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/identity-providers.xml b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/identity-providers.xml new file mode 100644 index 0000000000..90c77770d3 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/identity-providers.xml @@ -0,0 +1,88 @@ + + + + + + + ldap-identity-provider + org.apache.nifi.registry.security.ldap.LdapIdentityProvider + SIMPLE + + cn=read-only-admin,dc=example,dc=com + password + + FOLLOW + 10 secs + 10 secs + + ldap://localhost:8389 + dc=example,dc=com + (uid={0}) + + USE_USERNAME + 12 hours + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/nifi-registry-client.properties b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/nifi-registry-client.properties new file mode 100644 index 0000000000..996e6d5524 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/nifi-registry-client.properties @@ -0,0 +1,22 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# client security properties # +# Don't use a client cert for one-way TLS. Client identity will be provided via LDAP user/pass to get JWT +nifi.registry.security.truststore=./target/test-classes/keys/ca-ts.jks +nifi.registry.security.truststoreType=JKS +nifi.registry.security.truststorePasswd=password diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/nifi-registry.properties b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/nifi-registry.properties new file mode 100644 index 0000000000..b3512f30b6 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/nifi-registry.properties @@ -0,0 +1,35 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# web properties # +nifi.registry.web.https.host=localhost +nifi.registry.web.https.port=0 + +# security properties # +# +# ** Server KeyStore and TrustStore configuration set in Spring profile properties for embedded Jetty ** +# +nifi.registry.security.authorizers.configuration.file=./target/test-classes/conf/secure-ldap/authorizers.protected.xml +nifi.registry.security.authorizer=managed-authorizer +nifi.registry.security.identity.providers.configuration.file=./target/test-classes/conf/secure-ldap/identity-providers.protected.xml +nifi.registry.security.identity.provider=ldap-identity-provider + +# providers properties # +nifi.registry.providers.configuration.file=./target/test-classes/conf/providers.xml + +# enabled revision checking # +nifi.registry.revisions.enabled=true \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/test-ldap-data.ldif b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/test-ldap-data.ldif new file mode 100644 index 0000000000..db45689c07 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/test-ldap-data.ldif @@ -0,0 +1,261 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# extended LDIF +# +# LDAPv3 +# base with scope subtree +# filter: objectclass=* +# requesting: ALL +# +# Adapted from Forum Systems' LDAP Test Server +# + +# example.com +dn: dc=example,dc=com +objectClass: top +objectClass: dcObject +objectClass: organization +o: example.com +dc: example + +# read-only-admin, example.com +dn: cn=read-only-admin,dc=example,dc=com +sn: Read Only Admin +cn: read-only-admin +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top + +# nifiadmin, example.com +dn: uid=nifiadmin,dc=example,dc=com +sn: nifiadmin +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +uid: nifiadmin +cn: NiFi Admin +userPassword: password + +# newton, example.com +dn: uid=newton,dc=example,dc=com +sn: Newton +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +uid: newton +cn: Isaac Newton +userPassword: password + +# einstein, example.com +dn: uid=einstein,dc=example,dc=com +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Albert Einstein +sn: Einstein +uid: einstein +userPassword: password + +# tesla, example.com +dn: uid=tesla,dc=example,dc=com +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +objectClass: posixAccount +cn: Nikola Tesla +sn: Tesla +uid: tesla +uidNumber: 88888 +gidNumber: 99999 +homeDirectory: home +userPassword: password + +# galileo, example.com +dn: uid=galileo,dc=example,dc=com +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Galileo Galilei +sn: Galilei +uid: galileo +mail: galileo@example.com +userPassword: password + +# euler, example.com +dn: uid=euler,dc=example,dc=com +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +uid: euler +sn: Euler +cn: Leonhard Euler +userPassword: password + +# gauss, example.com +dn: uid=gauss,dc=example,dc=com +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Carl Friedrich Gauss +sn: Gauss +uid: gauss +userPassword: password + +# riemann, example.com +dn: uid=riemann,dc=example,dc=com +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Bernhard Riemann +sn: Riemann +uid: riemann +userPassword: password + +# euclid, example.com +dn: uid=euclid,dc=example,dc=com +uid: euclid +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Euclid +sn: Euclid +userPassword: password + +# curie, example.com +dn: uid=curie,dc=example,dc=com +uid: curie +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Marie Curie +sn: Curie +userPassword: password + +# nobel, example.com +dn: uid=nobel,dc=example,dc=com +uid: nobel +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +sn: Nobel +cn: Alfred Nobel +userPassword: password + +# boyle, example.com +dn: uid=boyle,dc=example,dc=com +uid: boyle +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Robert Boyle +sn: Boyle +telephoneNumber: 999-867-5309 +userPassword: password + +# pasteur, example.com +dn: uid=pasteur,dc=example,dc=com +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +sn: Pasteur +cn: Louis Pasteur +uid: pasteur +telephoneNumber: 602-214-4978 +userPassword: password + +# nogroup, example.com +dn: uid=nogroup,dc=example,dc=com +uid: nogroup +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: No Group +sn: Group + +# test, example.com +dn: uid=test,dc=example,dc=com +objectClass: posixAccount +objectClass: top +objectClass: inetOrgPerson +gidNumber: 0 +givenName: Test +sn: Test +displayName: Test +uid: test +initials: TS +homeDirectory: home +cn: Test +uidNumber: 24601 +o: Company + +# mathematicians, example.com +dn: ou=mathematicians,dc=example,dc=com +uniqueMember: uid=euclid,dc=example,dc=com +uniqueMember: uid=riemann,dc=example,dc=com +uniqueMember: uid=euler,dc=example,dc=com +uniqueMember: uid=gauss,dc=example,dc=com +uniqueMember: uid=test,dc=example,dc=com +ou: mathematicians +cn: Mathematicians +objectClass: groupOfUniqueNames +objectClass: top + +# scientists, example.com +dn: ou=scientists,dc=example,dc=com +uniqueMember: uid=einstein,dc=example,dc=com +uniqueMember: uid=galileo,dc=example,dc=com +uniqueMember: uid=tesla,dc=example,dc=com +uniqueMember: uid=newton,dc=example,dc=com +ou: scientists +cn: Scientists +objectClass: groupOfUniqueNames +objectClass: top + +# italians, example.com +dn: ou=italians,dc=example,dc=com +uniqueMember: uid=galileo,dc=example,dc=com +ou: italians +cn: Italians +objectClass: groupOfUniqueNames +objectClass: top + +# chemists, example.com +dn: ou=chemists,dc=example,dc=com +ou: chemists +objectClass: groupOfUniqueNames +objectClass: top +uniqueMember: uid=curie,dc=example,dc=com +uniqueMember: uid=boyle,dc=example,dc=com +uniqueMember: uid=nobel,dc=example,dc=com +uniqueMember: uid=pasteur,dc=example,dc=com +cn: Chemists diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-proxy/authorizers.xml b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-proxy/authorizers.xml new file mode 100644 index 0000000000..d4fedab223 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-proxy/authorizers.xml @@ -0,0 +1,103 @@ + + + + + + + + file-user-group-provider + org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider + ./target/test-classes/conf/secure-proxy/users.xml + CN=user1, OU=nifi + CN=user2, OU=nifi + CN=Алйс, OU=nifi + CN=proxy, OU=nifi + + + + + file-access-policy-provider + org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider + file-user-group-provider + ./target/test-classes/conf/secure-proxy/authorizations.xml + CN=user1, OU=nifi + CN=proxy, OU=nifi + + + + + managed-authorizer + org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer + file-access-policy-provider + + + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-proxy/nifi-registry-client.properties b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-proxy/nifi-registry-client.properties new file mode 100644 index 0000000000..b4c1a6a7f2 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-proxy/nifi-registry-client.properties @@ -0,0 +1,25 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# client security properties # +nifi.registry.security.keystore=./target/test-classes/keys/proxy-ks.jks +nifi.registry.security.keystoreType=JKS +nifi.registry.security.keystorePasswd=password +nifi.registry.security.keyPasswd=password +nifi.registry.security.truststore=./target/test-classes/keys/ca-ts.jks +nifi.registry.security.truststoreType=JKS +nifi.registry.security.truststorePasswd=password diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-proxy/nifi-registry.properties b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-proxy/nifi-registry.properties new file mode 100644 index 0000000000..1ae5cc3b44 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-proxy/nifi-registry.properties @@ -0,0 +1,33 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# web properties # +nifi.registry.web.https.host=localhost +nifi.registry.web.https.port=0 + +# security properties # +# +# ** Server KeyStore and TrustStore configuration set in Spring profile properties for embedded Jetty ** +# +nifi.registry.security.authorizers.configuration.file=./target/test-classes/conf/secure-proxy/authorizers.xml +nifi.registry.security.authorizer=managed-authorizer + +# providers properties # +nifi.registry.providers.configuration.file=./target/test-classes/conf/providers.xml + +# enabled revision checking # +nifi.registry.revisions.enabled=true \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/unsecured-no-revisions/nifi-registry.properties b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/unsecured-no-revisions/nifi-registry.properties new file mode 100644 index 0000000000..f87062cdcb --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/unsecured-no-revisions/nifi-registry.properties @@ -0,0 +1,31 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# web properties # +nifi.registry.web.http.host=localhost + +# providers properties # +nifi.registry.providers.configuration.file=./target/test-classes/conf/providers.xml + +# extensions working dir # +nifi.registry.extensions.working.directory=./target/work/extensions + +# database properties +nifi.registry.db.url.append=;LOCK_TIMEOUT=25000;WRITE_DELAY=0;AUTO_SERVER=FALSE + +# disable revision checking # +nifi.registry.revisions.enabled=false \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/unsecured/nifi-registry.properties b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/unsecured/nifi-registry.properties new file mode 100644 index 0000000000..451abc0ab1 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/unsecured/nifi-registry.properties @@ -0,0 +1,31 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +# web properties # +nifi.registry.web.http.host=localhost + +# providers properties # +nifi.registry.providers.configuration.file=./target/test-classes/conf/providers.xml + +# extensions working dir # +nifi.registry.extensions.working.directory=./target/work/extensions + +# database properties +nifi.registry.db.url.append=;LOCK_TIMEOUT=25000;WRITE_DELAY=0;AUTO_SERVER=FALSE + +# enabled revision checking # +nifi.registry.revisions.enabled=true \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/db/BucketsIT.sql b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/db/BucketsIT.sql new file mode 100644 index 0000000000..72385df229 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/db/BucketsIT.sql @@ -0,0 +1,26 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You 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. + +-- test data for buckets + +insert into BUCKET (id, name, description, created) + values ('1', 'Bucket 1', 'This is test bucket 1', TIMESTAMP'2017-09-11 12:51:00.000'); + +insert into BUCKET (id, name, description, created) + values ('2', 'Bucket 2', 'This is test bucket 2', TIMESTAMP'2017-09-11 12:52:00.000'); + +insert into BUCKET (id, name, description, created) + values ('3', 'Bucket 3', 'This is test bucket 3', TIMESTAMP'2017-09-11 12:53:00.000'); + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/db/FlowsIT.sql b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/db/FlowsIT.sql new file mode 100644 index 0000000000..1cb7c50bd7 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/db/FlowsIT.sql @@ -0,0 +1,50 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You 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. + +-- test data for buckets + +insert into BUCKET (id, name, description, created) + values ('1', 'Bucket 1', 'This is test bucket 1', '2017-09-11'); + +insert into BUCKET (id, name, description, created) + values ('2', 'Bucket 2', 'This is test bucket 2', '2017-09-12'); + +insert into BUCKET (id, name, description, created) + values ('3', 'Bucket 3', 'This is test bucket 3', '2017-09-13'); + +-- test data for flows + +insert into BUCKET_ITEM (id, name, description, created, modified, item_type, bucket_id) + values ('1', 'Flow 1', 'This is flow 1', '2017-09-11', '2017-09-11', 'FLOW', '1'); + +insert into FLOW (id) values ('1'); + +insert into BUCKET_ITEM (id, name, description, created, modified, item_type, bucket_id) + values ('2', 'Flow 2', 'This is flow 2', '2017-09-11', '2017-09-11', 'FLOW', '1'); + +insert into FLOW (id) values ('2'); + +insert into BUCKET_ITEM (id, name, description, created, modified, item_type, bucket_id) + values ('3', 'Flow 3', 'This is flow 3', '2017-09-11', '2017-09-11', 'FLOW', '2'); + +insert into FLOW (id) values ('3'); + +-- test data for flow snapshots + +insert into FLOW_SNAPSHOT (flow_id, version, created, created_by, comments) + values ('1', 1, '2017-09-11', 'user1', 'This is flow 1 snapshot 1'); + +insert into FLOW_SNAPSHOT (flow_id, version, created, created_by, comments) + values ('1', 2, '2017-09-12', 'user2', 'This is flow 1 snapshot 2'); diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/db/clearDB.sql b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/db/clearDB.sql new file mode 100644 index 0000000000..2031b2993c --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/db/clearDB.sql @@ -0,0 +1,21 @@ +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You 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. + +DELETE FROM FLOW_SNAPSHOT; +DELETE FROM FLOW; +DELETE FROM BUCKET_ITEM; +DELETE FROM BUCKET; + +DELETE FROM FLOW_PERSISTENCE_PROVIDER; \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-foo-nar-1.0.0.nar b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-foo-nar-1.0.0.nar new file mode 100644 index 0000000000..b43aa7b508 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-foo-nar-1.0.0.nar differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-foo-nar-2.0.0-SNAPSHOT-BUILD1.nar b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-foo-nar-2.0.0-SNAPSHOT-BUILD1.nar new file mode 100644 index 0000000000..4012a737d7 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-foo-nar-2.0.0-SNAPSHOT-BUILD1.nar differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-foo-nar-2.0.0-SNAPSHOT-BUILD2.nar b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-foo-nar-2.0.0-SNAPSHOT-BUILD2.nar new file mode 100644 index 0000000000..c94c20278d Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-foo-nar-2.0.0-SNAPSHOT-BUILD2.nar differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-foo-nar-2.0.0-SNAPSHOT-BUILD3.nar b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-foo-nar-2.0.0-SNAPSHOT-BUILD3.nar new file mode 100644 index 0000000000..226856b464 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-foo-nar-2.0.0-SNAPSHOT-BUILD3.nar differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-1.0.0.nar b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-1.0.0.nar new file mode 100644 index 0000000000..33939807b6 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-1.0.0.nar differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-bad-manifest.nar b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-bad-manifest.nar new file mode 100644 index 0000000000..3b8bfc8138 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-bad-manifest.nar differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-diff-checksum.nar b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-diff-checksum.nar new file mode 100644 index 0000000000..af56a232ff Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-diff-checksum.nar differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-missing-docs-descriptor.nar b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-missing-docs-descriptor.nar new file mode 100644 index 0000000000..8af4909968 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-missing-docs-descriptor.nar differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-missing-manifest.nar b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-missing-manifest.nar new file mode 100644 index 0000000000..32b7d6d2b9 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-missing-manifest.nar differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-no-dependency.nar b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-no-dependency.nar new file mode 100644 index 0000000000..1c82cd6e4f Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-no-dependency.nar differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0.nar b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0.nar new file mode 100644 index 0000000000..d9c5d3175d Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0.nar differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/README.md b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/README.md new file mode 100644 index 0000000000..129b504ec7 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/README.md @@ -0,0 +1,249 @@ + +# Test Keys + +The automated security tests require keys and certificates for TLS connections. +The keys in this directory can be used for that purpose. + +*** + +**NOTICE**: This directory contains keys and certificates for *development and testing* purposes only. + +**Never use these keystores and truststores in a real-world scenario where actual security is needed.** + +The CA and private keys (including their protection passwords) have been published on the Internet, so they should never be trusted. + +*** + +## Directory Contents + +### Certificate Authority (CA) + +| Hostname / DN | File | Description | Format | Password | +| --- | --- | --- | --- | --- | +| - | ca-cert.pem | CA public cert | PEM (unencrypted) | N/A | +| - | ca-key.pem | CA private (signing) key | PEM | password | +| - | ca-ts.jks | CA cert truststore (shared by clients and servers) | JKS | password | +| - | ca-ts.p12 | CA cert truststore (shared by clients and servers) | PKCS12 | password | +| registry, localhost | registry-cert.pem | NiFi Registry server public cert | PEM (unencrypted) | N/A | +| registry, localhost | registry-key.pem | NiFi Registry server private key | PEM | password | +| registry, localhost | registry-ks.jks | NiFi Registry server key/cert keystore | JKS | password | +| registry, localhost | registry-ks.p12 | NiFi Registry server key/cert keystore | PKCS12 | password | +| proxy, localhost | proxy-cert.pem | Proxy server public cert | PEM (unencrypted) | N/A | +| proxy, localhost | proxy-key.pem | Proxy server private key | PEM | password | +| proxy, localhost | proxy-ks.jks | Proxy server key/cert keystore | JKS | password | +| proxy, localhost | proxy-ks.p12 | Proxy server key/cert keystore | PKCS12 | password | +| CN=user1, OU=nifi | user1-cert.pem | client (user="user1") public cert | PEM (unencrypted) | N/A | +| CN=user1, OU=nifi | user1-key.pem | client (user="user1") private key | PEM | password | +| CN=user1, OU=nifi | user1-ks.jks | client (user="user1") key/cert keystore | JKS | password | +| CN=user1, OU=nifi | user1-ks.p12 | client (user="user1") key/cert keystore | PKCS12 | password | + +## Generating Additional Test Keys/Certs + +If we need to add a service or user to our test environment that requires a cert signed by the same CA, here are the steps for generating additional keys for this directory that are signed by the same CA key. + +Requirements: + +- docker +- keytool (included with Java) +- openssl (included/available on most platforms) + +If you do not have docker, you can substitute the nifi-toolkit binary, which is available for download from https://nifi.apache.org and should run on any platform with Java 1.8. + +### New Service Keys + +The steps for generating a new service key/cert pair are (using `proxy` as the example service): + +``` +# make working directory +WD="/tmp/test-keys-$(date +"%Y%m%d-%H%M%S")" +mkdir "$WD" +cd "$WD" + +# copy existing CA key/cert pair to working directory, rename to default tls-toolkit names +cp /path/to/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/ca-key.pem ./nifi-key.key +cp /path/to/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/ca-cert.pem ./nifi-cert.pem + +# use NiFi Toolkit Docker image to generate new keys/certs +docker run -v "$WD":/tmp -w /tmp apache/nifi-toolkit:latest tls-toolkit standalone \ + --hostnames proxy \ + --subjectAlternativeNames localhost \ + --nifiDnSuffix ", OU=nifi" \ + --keyStorePassword password \ + --trustStorePassword password \ + --days 9999 \ + -O + +# switch to output directory, create final output directory +cd "$WD" +mkdir keys + +# copy new service key/cert to final output dir in all formats +keytool -importkeystore \ + -srckeystore proxy/keystore.jks -srcstoretype jks -srcstorepass password -srcalias nifi-key \ + -destkeystore keys/proxy-ks.jks -deststoretype jks -deststorepass password -destalias proxy-key +keytool -importkeystore \ + -srckeystore keys/proxy-ks.jks -srcstoretype jks -srcstorepass password \ + -destkeystore keys/proxy-ks.p12 -deststoretype pkcs12 -deststorepass password +openssl pkcs12 -in keys/proxy-ks.p12 -passin pass:password -out keys/proxy-key.pem -passout pass:password +openssl pkcs12 -in keys/proxy-ks.p12 -passin pass:password -out keys/proxy-cert.pem -nokeys + +echo +echo "New keys written to ${WD}/keys" +echo "Copy to NiFi Registry test keys dir by running: " +echo " cp \"$WD/keys/*\" /path/to/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/" +``` + +You can verify the contents of the new keystore (and that the signature is done by the correct CA) using the following command: + + keytool -list -v -keystore "$WD/keys/proxy-ks.jks" -storepass password + +If you are satisfied with the results, you can copy the files from `/tmp/test-keys-YYYYMMDD-HHMMSS/keys` to this directory: + + cp "$WD/keys/*" /path/to/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/ + +### New Client or User Keys + +The steps for generating a new user key/cert pair are (using `user2` as the example user): + +``` +# make working directory +WD="/tmp/test-keys-$(date +"%Y%m%d-%H%M%S")" +mkdir "$WD" +cd "$WD" + +# copy existing CA key/cert pair to working directory, rename to default tls-toolkit names +cp /path/to/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/ca-key.pem ./nifi-key.key +cp /path/to/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/ca-cert.pem ./nifi-cert.pem + +# use NiFi Toolkit Docker image to generate new keys/certs +docker run -v "$WD":/tmp -w /tmp apache/nifi-toolkit:latest tls-toolkit standalone \ + --clientCertDn "CN=user2, OU=nifi" \ + --clientCertPassword password \ + --days 9999 \ + -O + +# switch to output directory, create final output directory +cd "$WD" +mkdir keys + +# transform tls-toolkit output to final output +keytool -importkeystore \ + -srckeystore CN=user2_OU=nifi.p12 -srcstoretype PKCS12 -srcstorepass password -srcalias nifi-key \ + -destkeystore keys/user2-ks.jks -deststoretype JKS -deststorepass password -destalias user2-key +keytool -importkeystore \ + -srckeystore keys/user2-ks.jks -srcstoretype jks -srcstorepass password \ + -destkeystore keys/user2-ks.p12 -deststoretype pkcs12 -deststorepass password +openssl pkcs12 -in keys/user2-ks.p12 -passin pass:password -out keys/user2-key.pem -passout pass:password +openssl pkcs12 -in keys/user2-ks.p12 -passin pass:password -out keys/user2-cert.pem -nokeys + +echo +echo "New keys written to ${WD}/keys" +echo "Copy to NiFi Registry test keys dir by running: " +echo " cp \"$WD/keys/*\" /path/to/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/" +``` + +You can verify the contents of the new keystore (and that the signature is done by the correct CA) using the following command: + + keytool -list -v -keystore "$WD/keys/user2-ks.jks" -storepass password + +If you are satisfied with the results, you can copy the files from `/tmp/test-keys-YYYYMMDD-HHMMSS/keys` to this directory: + + cp "$WD/keys/*" /path/to/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/ + + +## Regenerating All Test Keys/Certs + +In case you need to regenerate this entire directory, here are the steps that were used to first create it. +Follow these steps in order to recreate it. + +Requirements: + +- docker +- keytool (included with Java) +- openssl (included/available on most platforms) + +If you do not have docker, you can substitute the nifi-toolkit binary, which is available for download from https://nifi.apache.org and should run on any platform with Java 1.8. + +The steps for regenerating these test keys are: + +``` +# make working directory +WD="/tmp/test-keys-$(date +"%Y%m%d-%H%M%S")" +mkdir "$WD" +cd "$WD" + +# use NiFi Toolkit Docker image to generate new keys/certs +docker run -v "$WD":/tmp -w /tmp apache/nifi-toolkit:latest tls-toolkit standalone \ + --certificateAuthorityHostname "Test CA (Do Not Trust)" \ + --hostnames registry \ + --subjectAlternativeNames localhost \ + --nifiDnSuffix ", OU=nifi" \ + --keyStorePassword password \ + --trustStorePassword password \ + --clientCertDn "CN=user1, OU=nifi" \ + --clientCertPassword password \ + --days 9999 \ + -O + +# switch to output directory, create final output directory +cd "$WD" +mkdir keys + +# copy CA key/cert to final output dir in all formats +cp nifi-key.key keys/ca-key.pem +cp nifi-cert.pem keys/ca-cert.pem +keytool -importkeystore \ + -srckeystore registry/truststore.jks -srcstoretype jks -srcstorepass password -srcalias nifi-cert \ + -destkeystore keys/ca-ts.jks -deststoretype jks -deststorepass password -destalias ca-cert +keytool -importkeystore \ + -srckeystore keys/ca-ts.jks -srcstoretype jks -srcstorepass password \ + -destkeystore keys/ca-ts.p12 -deststoretype pkcs12 -deststorepass password + +# copy registry service key/cert to final output dir in all formats +keytool -importkeystore \ + -srckeystore registry/keystore.jks -srcstoretype jks -srcstorepass password -srcalias nifi-key \ + -destkeystore keys/registry-ks.jks -deststoretype jks -deststorepass password -destalias registry-key +keytool -importkeystore \ + -srckeystore keys/registry-ks.jks -srcstoretype jks -srcstorepass password \ + -destkeystore keys/registry-ks.p12 -deststoretype pkcs12 -deststorepass password +openssl pkcs12 -in keys/registry-ks.p12 -passin pass:password -out keys/registry-key.pem -passout pass:password +openssl pkcs12 -in keys/registry-ks.p12 -passin pass:password -out keys/registry-cert.pem -nokeys + +# copy user1 client key/cert to final output dir in all formats +keytool -importkeystore \ + -srckeystore CN=user1_OU=nifi.p12 -srcstoretype PKCS12 -srcstorepass password -srcalias nifi-key \ + -destkeystore keys/user1-ks.jks -deststoretype JKS -deststorepass password -destkeypass password -destalias user1-key +keytool -importkeystore \ + -srckeystore keys/user1-ks.jks -srcstoretype jks -srcstorepass password \ + -destkeystore keys/user1-ks.p12 -deststoretype pkcs12 -deststorepass password +openssl pkcs12 -in keys/user1-ks.p12 -passin pass:password -out keys/user1-key.pem -passout pass:password +openssl pkcs12 -in keys/user1-ks.p12 -passin pass:password -out keys/user1-cert.pem -nokeys + +echo +echo "New keys written to ${WD}/keys" +echo "Copy to NiFi Registry test keys dir by running: " +echo " cp -f \"$WD/keys/*\" /path/to/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/" +``` + +You should now have a `/tmp/test-keys-YYYYMMDD-HHMMSS/keys` directory with all the necessary keys for testing with various tools. + +You can verify the contents of a keystore using the following command: + + keytool -list -v -keystore "$WD/keys/registry-ks.jks" -storepass password + +If you are satisfied with the results, you can copy the files from `/tmp/test-keys-YYYYMMDD-HHMMSS/keys` to this directory: + + cp -f "$WD/keys/*" /path/to/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/ca-cert.pem b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/ca-cert.pem new file mode 100644 index 0000000000..c882f4e588 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/ca-cert.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDYzCCAkugAwIBAgIKAWfClyDGAAAAADANBgkqhkiG9w0BAQsFADAwMQ0wCwYD +VQQLDARuaWZpMR8wHQYDVQQDDBZUZXN0IENBIChEbyBOb3QgVHJ1c3QpMB4XDTE4 +MTIxODE4MzIyM1oXDTQ2MDUwNDE4MzIyM1owMDENMAsGA1UECwwEbmlmaTEfMB0G +A1UEAwwWVGVzdCBDQSAoRG8gTm90IFRydXN0KTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAIv7lVgGRHGaYmKkeTJpFzAp6QA7Anik/u1a+1ngGFWf9e6l +RkSX6US+nPbDRLJpSkO0c+/v8BwAKBiHUFaGF9XV7YvX92x/Gb3/FidSu+HAW/w/ +keIZ8PHvXbMtTvEur+nY1hSDvssdw1nAYAB9DG26HdRSg5c1DYgHLk9WCDWuIspU +n31YCb0lStWWbHM53i8xLfeV3IdOw9P3+d8bopzUUjk2quSxxekvzLCC1e14csJG +GIKLplRUq+zWRgkGYF8Fkx+kYGL62sehAdVcblxjwnXnmlPHvlxeaclsAVn4LCQj +gQzstzAv+s7sNSCxHba4vAusszWxOFiM1Vk8VvcCAwEAAaN/MH0wDgYDVR0PAQH/ +BAQDAgH+MAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFNukt0jKduJKyg8F+c/3j0w2 +AcnHMB8GA1UdIwQYMBaAFNukt0jKduJKyg8F+c/3j0w2AcnHMB0GA1UdJQQWMBQG +CCsGAQUFBwMCBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAQEAMvNsYLooq3zh +ts0fPU8dNcfe/NXFK6Uwg0RQPtq/l1ChGnZgXicx+RHMR5Q08pR62e+3gztk+LRE +iR9PpXqKFLM8slhR1z4sZ+Ja38ZHcOjsDPJeMKjUTrK8MNQN3YPKzoPE0AnLmsZI +Kf1eUIXXA3uXiXkIIVuxPPK96Q5Rla0xnbOpgejzGJ0BIMFP3odLlSahtT2Gl6wC +bdyImBkFntRJMoUx1fwUSKvIN5GUpaG6+E3mwgjckTUGZ15WrAllWqzhI06T73Yv +qR4FsQizqrqLimrIgvCBH6SWbOcsjCH/I58KqMRtG+kmfa/iwMfy0MMzuzx1Kwbr +qOi08D8F0w== +-----END CERTIFICATE----- diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/ca-key.pem b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/ca-key.pem new file mode 100644 index 0000000000..27d34eb1c0 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/ca-key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAi/uVWAZEcZpiYqR5MmkXMCnpADsCeKT+7Vr7WeAYVZ/17qVG +RJfpRL6c9sNEsmlKQ7Rz7+/wHAAoGIdQVoYX1dXti9f3bH8Zvf8WJ1K74cBb/D+R +4hnw8e9dsy1O8S6v6djWFIO+yx3DWcBgAH0Mbbod1FKDlzUNiAcuT1YINa4iylSf +fVgJvSVK1ZZsczneLzEt95Xch07D0/f53xuinNRSOTaq5LHF6S/MsILV7XhywkYY +goumVFSr7NZGCQZgXwWTH6RgYvrax6EB1VxuXGPCdeeaU8e+XF5pyWwBWfgsJCOB +DOy3MC/6zuw1ILEdtri8C6yzNbE4WIzVWTxW9wIDAQABAoIBAC3qizVBcQf2hRko +LB0N/a4twSDzOj9Kl9hRhKsZZ8IGY0wxaFgtoDWNdL04lfsTsGl+8pycjp0QrBZH +pGGNQJpCvtWlNKKhGleJKcIiUECfsUyPqZGJwtAJHSodzYwtLUS+fJJkGJxVmfOB +t7vRSNdhOlGf80wQ+exJtrYNWUoJ4oIjsYwObG1b5MuRU640zeamPa2DXlu7Ai6V +Fj7wWo5cI0Yawzb9OO/zSSM1hdP5rypQcCn7K2ybZGYNOKqanY43IDyWQj+JUysF +S+fFSuyv2FGifgSsxpGjuXoX6+oO1PDNhCQYnQjwoenQ6SHmBIqAhKv+VRCQUCsj +IP3ZD/kCgYEA6wYIExk1khOiBvXqlap9Bt+T43k5bp0J1x0RPJHDvXzEiVvsv0mN +Ft/EyvWoJvwxr6mtX9uVHdblQJao/ShCipdLeDGtMbHL8NH4PefBe+mkrHr/4/Yk +3cgofkp/OKrA6Cwrkis7ishZ/5QwjE9lcwRQuK5TPx16zomghd0VVCMCgYEAmHn/ +pWoHnp4vDqweByme3aELQ2w8TSc8smQMuuVx+x+4TuGfgQC7DhPb9Yqdy0d0AFec +IiVVYXG4JTCAovgLAtgwQd/M33pxv6r8ji8EXa+N/5YNmQ5+bxWth2TaKKHw1nwm +IJ3gFHyJe2WSDSBU19Ybkcw7D9xd/SkRZHfgZR0CgYEAuibj3GS6RsKgMo0zymno +b7pFBAavk8p00dqnHWeDN6IMdZPG+FhElVqWH//luUNGA5IMzgE5ohHlMXxjy2jJ +E8b0MvZ97P+bvlpBGp9nZENSeH9QEXqUBsqUMDvHetXcx8i8liECH1HD3yi8L1Zv +z2MaoL0LGNG7xL3D1GOhkisCgYBfDL42yZASax1+kgDuCh4Ent28m/5DQlBuDDx7 +TYjuOOnWEoQyENiKgArAWDbhf5tqkzK7fnZpFlDqrf+il+mVTltW1UKLlXLPPrHN +mLWqCUQFre6wGP7sFKFmI5Jzfe/6ZM4HyyLi4nd5uul+0UbSfaAWFTBEROU6aZ1z ++d6iaQKBgQClbxwJXed8LIH7sdcpUDz+sLZtcU2CBeaFvgWXN7V/pYATi2TvfN9p +8xMNUWsdTU7+2i9skzljlJ4vjEoY3cJ3Wki6cop7k6XqMGKV5oToZjwKjWGt2sp1 +8N72VtkU2o4gvK4HMIlbDIgrmNmXqLMRa6JJcRa2FPH4Xsi6NFrQPQ== +-----END RSA PRIVATE KEY----- diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/ca-ts.jks b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/ca-ts.jks new file mode 100644 index 0000000000..3fe89fa0b5 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/ca-ts.jks differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/ca-ts.p12 b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/ca-ts.p12 new file mode 100644 index 0000000000..b5fc9e70b5 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/ca-ts.p12 differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-cert.pem b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-cert.pem new file mode 100644 index 0000000000..67d2960a03 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-cert.pem @@ -0,0 +1,51 @@ +Bag Attributes + friendlyName: proxy-key + localKeyID: 54 69 6D 65 20 31 35 39 39 36 37 38 39 33 36 31 30 34 +subject=/OU=nifi/CN=proxy +issuer=/OU=nifi/CN=Test CA (Do Not Trust) +-----BEGIN CERTIFICATE----- +MIIDbjCCAlagAwIBAgIKAXR0SvpRAAAAADANBgkqhkiG9w0BAQsFADAwMQ0wCwYD +VQQLDARuaWZpMR8wHQYDVQQDDBZUZXN0IENBIChEbyBOb3QgVHJ1c3QpMB4XDTIw +MDkwOTE5MTUwNFoXDTQ4MDEyNTE5MTUwNFowHzENMAsGA1UECwwEbmlmaTEOMAwG +A1UEAwwFcHJveHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2CeF8 +tYG/58pjW0nE5Y/JnZzSxQpjs9sFzvJx9V8SEviAPxCSv0tP4PhR09hMLdKA2bTB +TM1S189fkN09fLLQtFucAJtsqseNFlZh/iJhmX1I1n5MIaLvffFNpXEaMbWOKSX7 +NfTT4mtS0jNJSpkR6bQEVrz8iiMrrMFGz9AiwEba/pg0hUTP+VP4pt7aKFwb8ZyL +6Wxo+Ny6nMe4M7KHcQi4+Cwmgc5YChMAGfCID+xGyND4vR2WJxNYe1joT+NXtGzY +zgYDU3NiYJN7lB3NKTZpXz9/Aga+NSo6pFzEFXiFNmFU50O7wkdhlhQfRlBSgjCY +0vL3X6N1CXSOKCbrAgMBAAGjgZowgZcwHQYDVR0OBBYEFCFvl95yYwhk81eaG4mH +dd94iKeXMB8GA1UdIwQYMBaAFNukt0jKduJKyg8F+c/3j0w2AcnHMA4GA1UdDwEB +/wQEAwID+DAJBgNVHRMEAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD +ATAbBgNVHREEFDASggVwcm94eYIJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IB +AQA1rAxZX2ebm40XOFhecxUQhTYJW+VYJUXRoxtjaEL9NwDvTcI8CmmaZ5LNF73E +doT75HOo+33bQz/Xnp9zFRWmiF4KH3u60cZ0obNpTCtWfY41UbN8La6FPokmaV+L +7POigubeVZf/6d7Hf8PcBQeXE+CNKJMQ73RoKbMWcdEhEdio1sXdMNPBo4m5SeDe +T/nbIbLJiFPo20lir3Q4OrzGOUnqwyT+7L64myjVkHgyjOzHC5PH4D5XGMNfQ6tQ +O2/flOso0ALGTB9VBo20sITS+BnPlMsWGdjQya/d1Oc4CEifRiWFx6H9daCb29/o +9ReApmPoZV801EkEAhPwqyZd +-----END CERTIFICATE----- +Bag Attributes + friendlyName: CN=Test CA (Do Not Trust),OU=nifi +subject=/OU=nifi/CN=Test CA (Do Not Trust) +issuer=/OU=nifi/CN=Test CA (Do Not Trust) +-----BEGIN CERTIFICATE----- +MIIDYzCCAkugAwIBAgIKAWfClyDGAAAAADANBgkqhkiG9w0BAQsFADAwMQ0wCwYD +VQQLDARuaWZpMR8wHQYDVQQDDBZUZXN0IENBIChEbyBOb3QgVHJ1c3QpMB4XDTE4 +MTIxODE4MzIyM1oXDTQ2MDUwNDE4MzIyM1owMDENMAsGA1UECwwEbmlmaTEfMB0G +A1UEAwwWVGVzdCBDQSAoRG8gTm90IFRydXN0KTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAIv7lVgGRHGaYmKkeTJpFzAp6QA7Anik/u1a+1ngGFWf9e6l +RkSX6US+nPbDRLJpSkO0c+/v8BwAKBiHUFaGF9XV7YvX92x/Gb3/FidSu+HAW/w/ +keIZ8PHvXbMtTvEur+nY1hSDvssdw1nAYAB9DG26HdRSg5c1DYgHLk9WCDWuIspU +n31YCb0lStWWbHM53i8xLfeV3IdOw9P3+d8bopzUUjk2quSxxekvzLCC1e14csJG +GIKLplRUq+zWRgkGYF8Fkx+kYGL62sehAdVcblxjwnXnmlPHvlxeaclsAVn4LCQj +gQzstzAv+s7sNSCxHba4vAusszWxOFiM1Vk8VvcCAwEAAaN/MH0wDgYDVR0PAQH/ +BAQDAgH+MAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFNukt0jKduJKyg8F+c/3j0w2 +AcnHMB8GA1UdIwQYMBaAFNukt0jKduJKyg8F+c/3j0w2AcnHMB0GA1UdJQQWMBQG +CCsGAQUFBwMCBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAQEAMvNsYLooq3zh +ts0fPU8dNcfe/NXFK6Uwg0RQPtq/l1ChGnZgXicx+RHMR5Q08pR62e+3gztk+LRE +iR9PpXqKFLM8slhR1z4sZ+Ja38ZHcOjsDPJeMKjUTrK8MNQN3YPKzoPE0AnLmsZI +Kf1eUIXXA3uXiXkIIVuxPPK96Q5Rla0xnbOpgejzGJ0BIMFP3odLlSahtT2Gl6wC +bdyImBkFntRJMoUx1fwUSKvIN5GUpaG6+E3mwgjckTUGZ15WrAllWqzhI06T73Yv +qR4FsQizqrqLimrIgvCBH6SWbOcsjCH/I58KqMRtG+kmfa/iwMfy0MMzuzx1Kwbr +qOi08D8F0w== +-----END CERTIFICATE----- diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-key.pem b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-key.pem new file mode 100644 index 0000000000..1a5417d009 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-key.pem @@ -0,0 +1,85 @@ +Bag Attributes + friendlyName: proxy-key + localKeyID: 54 69 6D 65 20 31 35 39 39 36 37 38 39 33 36 31 30 34 +Key Attributes: +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIkocYj9mKFyMCAggA +MBQGCCqGSIb3DQMHBAibNQqgCb2bzQSCBMiTWItLGuguyT/g4eZXZMZvZW2cnZ5+ +heTqXLAWChAt8IwiGYC1K40PcVQvNBPFoLqbdH2EsvmK8l7tNv1tq/kjpkuhVuwL +06E9m9Y8ucS0Q60WlREaac2kth5rWdx586kwKsM7h79fKSS86F3tCbamIgdst7xQ +WQ1z7Dcx6Et6VY4F8i6kEgp9b5zIHmbBxB/b9dfHDJSQ5WyzHjc1raQ6XJ/c7DFs +URkafQbAQ5urHVS9+aWfCPXT9Wjl/4kYqcRWkYua/ulhzcIKyBr1BP+t4Xsoj3da +4AZzR+ZSQiKj6UkBSssEJbPtOfonhQrFsoTm3sR1k6pXa4uySef42WGoex77ekco +ReXiAI7Z1tPp3QI6ossYK5R7WY876/fblMNL96hGtCQOM0P9x5L58sVZJ0j2xbJx +Tvu91yKA3vVn7cI0z99dz3ULuM2bdmKoZbwMlhgk1X7VRg43GHLVd9NlpUido7/Y +b/KV0aGKrxBr3rdXu+rVZ/7ElhmQ3q7nkU6hxo3Lj8eF9dkJzhxPtRvUAqsRhaN0 +c/iNSNJsDX1g+LOzuGI9xxS8lg61C73jz4Dhfv+CPNZzVXiJctY74hMzTw0gkkSW +4cN/0UoGdjVpeUScy9+IqF94Hp6w7xDXTJ6egoT60ZtT7Ls8zc6TTNl+qo/IGEnw +QA+7Z6OY38Z0gMQivtDeOrLFFcTy98ZBibvJlw/LsW2KadXzGooXuJX2AgTfs3U5 +oIBjmubincvVhpcQDAdzifzURZuAnvEb1FvQIAC/jp8U009f2JByycobmviluNSp +2EirUpA0XsW8B99EUlUfzbCOAb9YS8vesyzevJP43XjKlNNsptG4pe8jiMaxUFGE +1fdseWJwiRfSXpwQBafovOVXItxZWkrfD+i7h6LfG79hDFNHTcDknoYjJzcAYOYF +ju9vUhE9dgyZogppOQ32NURkyh8HkHZFGI9MlaoqNXl4z9TRMUVxJET/ZCJ0zsE+ +WV5KL6oiPU5VuGbIReouDtPd6lOtqsaE3PoKfriGhIKSGOHbxOZa3WLs/++BJO8A +5eNKWuqwUtL4uPw4gnxJFIyK04lzMp5q5BT7fkrlenRvsOn8IGZp6e5Wvsb3dCx1 +LKR8ktyjnxSpSw9dACDeGGxTeCOUxM5ElafI7RKsJ1kCtUpxMyfTru1SmP3Hcepc +GteHjm/vrv9TW0ZBhGFsuxo5AuZFnsjSAy6JVHRYQaNzdTs8qJTZvYhGjq/XSI34 +Jafugsu1uKMKu+RfvbSM30/70kXu6R0DT55w1j8AMbdchyJDBQT4Ua6zK/N2CHW2 +++jxwM7h7C4imFgsILjo41TT7Ve2vqG3PkLaSIgotZDs9zCYJxN/A9QGHTfzjQvy +zp2yNCC5ebyGfCgBafdCasyyPuJGVagSvNky7fRlhu0HI+kPIarL6xASFDxDVBEF +ko5kHDbKZHFp4skdi+Bu/DKtIoGUEJabRkkyOO0rOrq4ldEm3oAnn3PIUaVk0NNb +pcSx9Hb/M2wWRMHdTpRrBYJ++GlcCn2oaIX4kkxk5ERvm17guHxWATq4spI3GM38 +kCcXwUCEjV1ILpn43m/+1RPTzFKpa7S79HjMWbQEx7VTgtVwxqFq3bGUTV7MtnSQ +rbg= +-----END ENCRYPTED PRIVATE KEY----- +Bag Attributes + friendlyName: proxy-key + localKeyID: 54 69 6D 65 20 31 35 39 39 36 37 38 39 33 36 31 30 34 +subject=/OU=nifi/CN=proxy +issuer=/OU=nifi/CN=Test CA (Do Not Trust) +-----BEGIN CERTIFICATE----- +MIIDbjCCAlagAwIBAgIKAXR0SvpRAAAAADANBgkqhkiG9w0BAQsFADAwMQ0wCwYD +VQQLDARuaWZpMR8wHQYDVQQDDBZUZXN0IENBIChEbyBOb3QgVHJ1c3QpMB4XDTIw +MDkwOTE5MTUwNFoXDTQ4MDEyNTE5MTUwNFowHzENMAsGA1UECwwEbmlmaTEOMAwG +A1UEAwwFcHJveHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2CeF8 +tYG/58pjW0nE5Y/JnZzSxQpjs9sFzvJx9V8SEviAPxCSv0tP4PhR09hMLdKA2bTB +TM1S189fkN09fLLQtFucAJtsqseNFlZh/iJhmX1I1n5MIaLvffFNpXEaMbWOKSX7 +NfTT4mtS0jNJSpkR6bQEVrz8iiMrrMFGz9AiwEba/pg0hUTP+VP4pt7aKFwb8ZyL +6Wxo+Ny6nMe4M7KHcQi4+Cwmgc5YChMAGfCID+xGyND4vR2WJxNYe1joT+NXtGzY +zgYDU3NiYJN7lB3NKTZpXz9/Aga+NSo6pFzEFXiFNmFU50O7wkdhlhQfRlBSgjCY +0vL3X6N1CXSOKCbrAgMBAAGjgZowgZcwHQYDVR0OBBYEFCFvl95yYwhk81eaG4mH +dd94iKeXMB8GA1UdIwQYMBaAFNukt0jKduJKyg8F+c/3j0w2AcnHMA4GA1UdDwEB +/wQEAwID+DAJBgNVHRMEAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD +ATAbBgNVHREEFDASggVwcm94eYIJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IB +AQA1rAxZX2ebm40XOFhecxUQhTYJW+VYJUXRoxtjaEL9NwDvTcI8CmmaZ5LNF73E +doT75HOo+33bQz/Xnp9zFRWmiF4KH3u60cZ0obNpTCtWfY41UbN8La6FPokmaV+L +7POigubeVZf/6d7Hf8PcBQeXE+CNKJMQ73RoKbMWcdEhEdio1sXdMNPBo4m5SeDe +T/nbIbLJiFPo20lir3Q4OrzGOUnqwyT+7L64myjVkHgyjOzHC5PH4D5XGMNfQ6tQ +O2/flOso0ALGTB9VBo20sITS+BnPlMsWGdjQya/d1Oc4CEifRiWFx6H9daCb29/o +9ReApmPoZV801EkEAhPwqyZd +-----END CERTIFICATE----- +Bag Attributes + friendlyName: CN=Test CA (Do Not Trust),OU=nifi +subject=/OU=nifi/CN=Test CA (Do Not Trust) +issuer=/OU=nifi/CN=Test CA (Do Not Trust) +-----BEGIN CERTIFICATE----- +MIIDYzCCAkugAwIBAgIKAWfClyDGAAAAADANBgkqhkiG9w0BAQsFADAwMQ0wCwYD +VQQLDARuaWZpMR8wHQYDVQQDDBZUZXN0IENBIChEbyBOb3QgVHJ1c3QpMB4XDTE4 +MTIxODE4MzIyM1oXDTQ2MDUwNDE4MzIyM1owMDENMAsGA1UECwwEbmlmaTEfMB0G +A1UEAwwWVGVzdCBDQSAoRG8gTm90IFRydXN0KTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAIv7lVgGRHGaYmKkeTJpFzAp6QA7Anik/u1a+1ngGFWf9e6l +RkSX6US+nPbDRLJpSkO0c+/v8BwAKBiHUFaGF9XV7YvX92x/Gb3/FidSu+HAW/w/ +keIZ8PHvXbMtTvEur+nY1hSDvssdw1nAYAB9DG26HdRSg5c1DYgHLk9WCDWuIspU +n31YCb0lStWWbHM53i8xLfeV3IdOw9P3+d8bopzUUjk2quSxxekvzLCC1e14csJG +GIKLplRUq+zWRgkGYF8Fkx+kYGL62sehAdVcblxjwnXnmlPHvlxeaclsAVn4LCQj +gQzstzAv+s7sNSCxHba4vAusszWxOFiM1Vk8VvcCAwEAAaN/MH0wDgYDVR0PAQH/ +BAQDAgH+MAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFNukt0jKduJKyg8F+c/3j0w2 +AcnHMB8GA1UdIwQYMBaAFNukt0jKduJKyg8F+c/3j0w2AcnHMB0GA1UdJQQWMBQG +CCsGAQUFBwMCBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAQEAMvNsYLooq3zh +ts0fPU8dNcfe/NXFK6Uwg0RQPtq/l1ChGnZgXicx+RHMR5Q08pR62e+3gztk+LRE +iR9PpXqKFLM8slhR1z4sZ+Ja38ZHcOjsDPJeMKjUTrK8MNQN3YPKzoPE0AnLmsZI +Kf1eUIXXA3uXiXkIIVuxPPK96Q5Rla0xnbOpgejzGJ0BIMFP3odLlSahtT2Gl6wC +bdyImBkFntRJMoUx1fwUSKvIN5GUpaG6+E3mwgjckTUGZ15WrAllWqzhI06T73Yv +qR4FsQizqrqLimrIgvCBH6SWbOcsjCH/I58KqMRtG+kmfa/iwMfy0MMzuzx1Kwbr +qOi08D8F0w== +-----END CERTIFICATE----- diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-ks.jks b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-ks.jks new file mode 100644 index 0000000000..444b43dbda Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-ks.jks differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-ks.p12 b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-ks.p12 new file mode 100644 index 0000000000..9eca9d8b29 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/proxy-ks.p12 differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/registry-cert.pem b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/registry-cert.pem new file mode 100644 index 0000000000..026e3eb956 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/registry-cert.pem @@ -0,0 +1,51 @@ +Bag Attributes + friendlyName: registry-key + localKeyID: 54 69 6D 65 20 31 35 34 35 31 35 37 39 34 36 32 31 34 +subject=/OU=nifi/CN=registry +issuer=/OU=nifi/CN=Test CA (Do Not Trust) +-----BEGIN CERTIFICATE----- +MIIDdDCCAlygAwIBAgIKAWfClyHCAAAAADANBgkqhkiG9w0BAQsFADAwMQ0wCwYD +VQQLDARuaWZpMR8wHQYDVQQDDBZUZXN0IENBIChEbyBOb3QgVHJ1c3QpMB4XDTE4 +MTIxODE4MzIyM1oXDTQ2MDUwNDE4MzIyM1owIjENMAsGA1UECwwEbmlmaTERMA8G +A1UEAwwIcmVnaXN0cnkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4 +OYy3dRjERT7HcighqGW3eb4I0DfweWJ42/uf94/hMFSIQPo/lJetomhFSGiswRCN +Y9ybWMaB0jPL7ksJQJLzUTKOQvH08Ml/MaiwsRHlGlD+8LqTma0jT+vdhdcypZ+B +m5x6ozC5mZB1QqDYwXN21YR7IGYDMAdZ0OQuMreC+u7+fZ4LZLTFeKajWJSZ3fnv +UKtYN05OUr8HZXk6Cc0vys7YZZeIQ5vKdbDjNSEt1yixfvp468KzFSNOCYtLStcL +bDq4MBhxplQXTVJie1ofYnc8pYAG8BP1IPusfJ/NxCpk4pzSjFKC/GI0QfqcTVWA +REUmTFpRZGykWsx0aBPFAgMBAAGjgZ0wgZowHQYDVR0OBBYEFB2WLWWv5ubjziz6 +2IfJcs3Spy2kMB8GA1UdIwQYMBaAFNukt0jKduJKyg8F+c/3j0w2AcnHMA4GA1Ud +DwEB/wQEAwID+DAJBgNVHRMEAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEF +BQcDATAeBgNVHREEFzAVgghyZWdpc3RyeYIJbG9jYWxob3N0MA0GCSqGSIb3DQEB +CwUAA4IBAQAe5BReiXYEzM8ef7Wcl7DBLSh0Q5tWAl8Z4xCrOlNHjHM6GEZirJmh +ww6W1wDth/kftZnptjxAP21SbdjzmgDRcRu2wqZSHeWP6lL53BfegE/AFaBwTlOE +2nESoGIDl1vROMFFOnzR2ZJWmSoUDk/4oVFLZYAabUZKjfUZTjz99O7Pk4GiySrg +c7/xKnYx8x91+jqFjpIgR/pkvrTPaPkAtgs68a5YxGT8yHH0wA1Ve+3zmqkQbg9g +fDimZ4fgaorS0JzuqiZbIyzxu7Q6G506Lu34l5NA2qZQzdTm8g98ksli7DMazc8n +8o68bZD9szMrvGiVCx/ujtiu2GG1y187 +-----END CERTIFICATE----- +Bag Attributes + friendlyName: CN=Test CA (Do Not Trust),OU=nifi +subject=/OU=nifi/CN=Test CA (Do Not Trust) +issuer=/OU=nifi/CN=Test CA (Do Not Trust) +-----BEGIN CERTIFICATE----- +MIIDYzCCAkugAwIBAgIKAWfClyDGAAAAADANBgkqhkiG9w0BAQsFADAwMQ0wCwYD +VQQLDARuaWZpMR8wHQYDVQQDDBZUZXN0IENBIChEbyBOb3QgVHJ1c3QpMB4XDTE4 +MTIxODE4MzIyM1oXDTQ2MDUwNDE4MzIyM1owMDENMAsGA1UECwwEbmlmaTEfMB0G +A1UEAwwWVGVzdCBDQSAoRG8gTm90IFRydXN0KTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAIv7lVgGRHGaYmKkeTJpFzAp6QA7Anik/u1a+1ngGFWf9e6l +RkSX6US+nPbDRLJpSkO0c+/v8BwAKBiHUFaGF9XV7YvX92x/Gb3/FidSu+HAW/w/ +keIZ8PHvXbMtTvEur+nY1hSDvssdw1nAYAB9DG26HdRSg5c1DYgHLk9WCDWuIspU +n31YCb0lStWWbHM53i8xLfeV3IdOw9P3+d8bopzUUjk2quSxxekvzLCC1e14csJG +GIKLplRUq+zWRgkGYF8Fkx+kYGL62sehAdVcblxjwnXnmlPHvlxeaclsAVn4LCQj +gQzstzAv+s7sNSCxHba4vAusszWxOFiM1Vk8VvcCAwEAAaN/MH0wDgYDVR0PAQH/ +BAQDAgH+MAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFNukt0jKduJKyg8F+c/3j0w2 +AcnHMB8GA1UdIwQYMBaAFNukt0jKduJKyg8F+c/3j0w2AcnHMB0GA1UdJQQWMBQG +CCsGAQUFBwMCBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAQEAMvNsYLooq3zh +ts0fPU8dNcfe/NXFK6Uwg0RQPtq/l1ChGnZgXicx+RHMR5Q08pR62e+3gztk+LRE +iR9PpXqKFLM8slhR1z4sZ+Ja38ZHcOjsDPJeMKjUTrK8MNQN3YPKzoPE0AnLmsZI +Kf1eUIXXA3uXiXkIIVuxPPK96Q5Rla0xnbOpgejzGJ0BIMFP3odLlSahtT2Gl6wC +bdyImBkFntRJMoUx1fwUSKvIN5GUpaG6+E3mwgjckTUGZ15WrAllWqzhI06T73Yv +qR4FsQizqrqLimrIgvCBH6SWbOcsjCH/I58KqMRtG+kmfa/iwMfy0MMzuzx1Kwbr +qOi08D8F0w== +-----END CERTIFICATE----- diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/registry-key.pem b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/registry-key.pem new file mode 100644 index 0000000000..e2e48e723b --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/registry-key.pem @@ -0,0 +1,85 @@ +Bag Attributes + friendlyName: registry-key + localKeyID: 54 69 6D 65 20 31 35 34 35 31 35 37 39 34 36 32 31 34 +Key Attributes: +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIq3tPUSgTjYoCAggA +MBQGCCqGSIb3DQMHBAjQRc2FRduPPwSCBMh1unFOz4EL+BfzTMsHc//QDhof+/ey +4voz/DqHsPnxSXGCxDJc51OnyINIRZsIVaJVDxcKbwMtoLEVasGw54oYeTRSkJ7l +56dTEvHHBx9/ksdcKGshCfA2hBI5YMiWXjk477+iKI7AhoHXt5azk65NUG4+CU/X +FuqzAcXfwtktfl/nyl5U7j+Kh7cVpV0XEINUyjBiqKjTkAMn/+GMOUN0Tb/hYtZo +P8bz8cqQiLIdoQzqVJmVkS4cXhUcD0a2wJ9ptPz+Mss4VOl7812R30kBB1figNIy +V7hzC7vJpECQoCkqvDDfJBmyjSA5JDaVQMyXyrpP1nrRDdKOqufYbA93ojm1AA+h +eCXWtqWcLVgUHEQz6hzrxcigyv+iR29xA58wB+/GBs2GIgrpfA5kKK9QiI1WsAT0 +B7mLEhzACtfctuWkcHsY7GEHKdweVBGkRzMFjdNpO4mYDyp9x7Xc2AVxENyfdZIA +e3JNRMK3yQHOr4oAk1YnU7h4Dzw4dsbLetixSsPtn+5PpoulUKqd7VLqQb62Fzlt +FquphwZesdmLZLm6igMXfAb6JzPIXg8HQGubX8MHWdbtHlkPOnZ6YQ1MOZvX8PGB +2TY76c7O5lBU2Az/jcSSh0Y4vuf55mTLEdnvsIuwrlITitZFTM4fOpFTbP+g4MEy +L4KtUP8TnezsNCitxQSOnzBHkZ52Ik6aL+macjS0UgPiku9P4aC0kpszAXZUuumQ +PdgcEIH8dJ6/wvs7hBHHlR4xLbj6nOQ5rz6Cxx0Ozb+F8oi5WcwoeSUNz9z51Z52 +ENj3BQ8G672KtKRJ8h2csGt6SAH0sjjjzThiKk9e4o1/la6/6D5mQsRUxgGbZ/pw +ewtPkFIPa9s+XRvlIu9QBKaqvIZ3aRARvtdUOiap2XOGkqPLmeLBQ8yCvqDczkwV +f5dg8zcE817bfe5NcAUQyOrT8A+ANpjLZNwKK7B1tg5Q7S3G+u9MeSSR6xMyw+ce +04eVo3+rtFia7gMnL1covgA46jBufl6to1AwSPHkbooVKdPUODPG8FKKpJSjHOUz +WGNx8+QXXsvZyWw2betA6eT2lJuSfKct5QSpwImLFjH0xjwpSu0dvQK3N6IvpLkR +lei0ATBjZ5ekRRpGowDJJlFE3LKU2BkVMikl6/c3Ap89/EKdIeN3odVtA7GPDHyy +Vxh0sywOdatL46HI2EvMlMbIFX0BvJVZQ68BZ8UxmucrzC6G+hoVzMk18vSDm3s/ +Ydj5FiUuBSL36x9sQmzQXk2XvQscHCYnMX7H4F5TVaarfp1xQS36H3P6rUXcDRhp ++EM6P/YkQkelGIc+t2AcHwcYeISzqVIXBzMQfAaiR2YsQc/27mM/mkI+wT14WfxN +cf47ver5OBc2xfKScp4dJpWMVe/gZEAiyVlSw5gZXGP/CbunBNVhldSl8lh/nCpc +WJrmnRe5wyx9+OKoyEI8w9fE5c1Pwct33oeMnaKdpI8g/eDBARZkz1Tousso9a+K +5YmqHpOhQkEMxCrhxF+yY+XmoLCIPg0B+EX5IWZfHGL5Etm5FhcijzFw86z2QHZ+ +JUINfPR1WnN8lNoUFA4w/MTv0wREeWD6cF/bzYQQDrnzdl5tznYSFF5xAEb5KrlH +aGA= +-----END ENCRYPTED PRIVATE KEY----- +Bag Attributes + friendlyName: registry-key + localKeyID: 54 69 6D 65 20 31 35 34 35 31 35 37 39 34 36 32 31 34 +subject=/OU=nifi/CN=registry +issuer=/OU=nifi/CN=Test CA (Do Not Trust) +-----BEGIN CERTIFICATE----- +MIIDdDCCAlygAwIBAgIKAWfClyHCAAAAADANBgkqhkiG9w0BAQsFADAwMQ0wCwYD +VQQLDARuaWZpMR8wHQYDVQQDDBZUZXN0IENBIChEbyBOb3QgVHJ1c3QpMB4XDTE4 +MTIxODE4MzIyM1oXDTQ2MDUwNDE4MzIyM1owIjENMAsGA1UECwwEbmlmaTERMA8G +A1UEAwwIcmVnaXN0cnkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4 +OYy3dRjERT7HcighqGW3eb4I0DfweWJ42/uf94/hMFSIQPo/lJetomhFSGiswRCN +Y9ybWMaB0jPL7ksJQJLzUTKOQvH08Ml/MaiwsRHlGlD+8LqTma0jT+vdhdcypZ+B +m5x6ozC5mZB1QqDYwXN21YR7IGYDMAdZ0OQuMreC+u7+fZ4LZLTFeKajWJSZ3fnv +UKtYN05OUr8HZXk6Cc0vys7YZZeIQ5vKdbDjNSEt1yixfvp468KzFSNOCYtLStcL +bDq4MBhxplQXTVJie1ofYnc8pYAG8BP1IPusfJ/NxCpk4pzSjFKC/GI0QfqcTVWA +REUmTFpRZGykWsx0aBPFAgMBAAGjgZ0wgZowHQYDVR0OBBYEFB2WLWWv5ubjziz6 +2IfJcs3Spy2kMB8GA1UdIwQYMBaAFNukt0jKduJKyg8F+c/3j0w2AcnHMA4GA1Ud +DwEB/wQEAwID+DAJBgNVHRMEAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEF +BQcDATAeBgNVHREEFzAVgghyZWdpc3RyeYIJbG9jYWxob3N0MA0GCSqGSIb3DQEB +CwUAA4IBAQAe5BReiXYEzM8ef7Wcl7DBLSh0Q5tWAl8Z4xCrOlNHjHM6GEZirJmh +ww6W1wDth/kftZnptjxAP21SbdjzmgDRcRu2wqZSHeWP6lL53BfegE/AFaBwTlOE +2nESoGIDl1vROMFFOnzR2ZJWmSoUDk/4oVFLZYAabUZKjfUZTjz99O7Pk4GiySrg +c7/xKnYx8x91+jqFjpIgR/pkvrTPaPkAtgs68a5YxGT8yHH0wA1Ve+3zmqkQbg9g +fDimZ4fgaorS0JzuqiZbIyzxu7Q6G506Lu34l5NA2qZQzdTm8g98ksli7DMazc8n +8o68bZD9szMrvGiVCx/ujtiu2GG1y187 +-----END CERTIFICATE----- +Bag Attributes + friendlyName: CN=Test CA (Do Not Trust),OU=nifi +subject=/OU=nifi/CN=Test CA (Do Not Trust) +issuer=/OU=nifi/CN=Test CA (Do Not Trust) +-----BEGIN CERTIFICATE----- +MIIDYzCCAkugAwIBAgIKAWfClyDGAAAAADANBgkqhkiG9w0BAQsFADAwMQ0wCwYD +VQQLDARuaWZpMR8wHQYDVQQDDBZUZXN0IENBIChEbyBOb3QgVHJ1c3QpMB4XDTE4 +MTIxODE4MzIyM1oXDTQ2MDUwNDE4MzIyM1owMDENMAsGA1UECwwEbmlmaTEfMB0G +A1UEAwwWVGVzdCBDQSAoRG8gTm90IFRydXN0KTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAIv7lVgGRHGaYmKkeTJpFzAp6QA7Anik/u1a+1ngGFWf9e6l +RkSX6US+nPbDRLJpSkO0c+/v8BwAKBiHUFaGF9XV7YvX92x/Gb3/FidSu+HAW/w/ +keIZ8PHvXbMtTvEur+nY1hSDvssdw1nAYAB9DG26HdRSg5c1DYgHLk9WCDWuIspU +n31YCb0lStWWbHM53i8xLfeV3IdOw9P3+d8bopzUUjk2quSxxekvzLCC1e14csJG +GIKLplRUq+zWRgkGYF8Fkx+kYGL62sehAdVcblxjwnXnmlPHvlxeaclsAVn4LCQj +gQzstzAv+s7sNSCxHba4vAusszWxOFiM1Vk8VvcCAwEAAaN/MH0wDgYDVR0PAQH/ +BAQDAgH+MAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFNukt0jKduJKyg8F+c/3j0w2 +AcnHMB8GA1UdIwQYMBaAFNukt0jKduJKyg8F+c/3j0w2AcnHMB0GA1UdJQQWMBQG +CCsGAQUFBwMCBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAQEAMvNsYLooq3zh +ts0fPU8dNcfe/NXFK6Uwg0RQPtq/l1ChGnZgXicx+RHMR5Q08pR62e+3gztk+LRE +iR9PpXqKFLM8slhR1z4sZ+Ja38ZHcOjsDPJeMKjUTrK8MNQN3YPKzoPE0AnLmsZI +Kf1eUIXXA3uXiXkIIVuxPPK96Q5Rla0xnbOpgejzGJ0BIMFP3odLlSahtT2Gl6wC +bdyImBkFntRJMoUx1fwUSKvIN5GUpaG6+E3mwgjckTUGZ15WrAllWqzhI06T73Yv +qR4FsQizqrqLimrIgvCBH6SWbOcsjCH/I58KqMRtG+kmfa/iwMfy0MMzuzx1Kwbr +qOi08D8F0w== +-----END CERTIFICATE----- diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/registry-ks.jks b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/registry-ks.jks new file mode 100644 index 0000000000..0bc06d7ba8 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/registry-ks.jks differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/registry-ks.p12 b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/registry-ks.p12 new file mode 100644 index 0000000000..0f10f89de1 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/registry-ks.p12 differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/user1-cert.pem b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/user1-cert.pem new file mode 100644 index 0000000000..dfecaec9f2 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/user1-cert.pem @@ -0,0 +1,50 @@ +Bag Attributes + friendlyName: user1-key + localKeyID: 54 69 6D 65 20 31 35 34 35 31 35 37 39 34 38 38 33 36 +subject=/OU=nifi/CN=user1 +issuer=/OU=nifi/CN=Test CA (Do Not Trust) +-----BEGIN CERTIFICATE----- +MIIDTzCCAjegAwIBAgIKAWfClyTXAAAAADANBgkqhkiG9w0BAQsFADAwMQ0wCwYD +VQQLDARuaWZpMR8wHQYDVQQDDBZUZXN0IENBIChEbyBOb3QgVHJ1c3QpMB4XDTE4 +MTIxODE4MzIyNFoXDTQ2MDUwNDE4MzIyNFowHzENMAsGA1UECwwEbmlmaTEOMAwG +A1UEAwwFdXNlcjEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCKSsvi +WSrOIDjF+drgVnrB1QYk1dNeQp64nE7ffzlPoEOWbNQzho+JTp1D9XI9+IBUmSTW +f4JeYDs0gTpbfwzVmsLI30u/a0wUgERM3+8fl9XwrSJ4blkG4A8az4i3CFjObOYA +6znDclHLGcmWthwjxQ50n/BjTUGHJFpK/1/uLLgfeJreP73RdTm8fPMRj9H1JhpV +/UsP4w0EJX3sr2acKW34w+edkQgbGxxg2+dAokb/ODrO6/wntbYaFJguCEe5jQL+ +bfSVMiix99RjTeflFaKU0fQI0GYbcf6wmz9JUEvX9JXiclVyu9daV6jUgKJcg6SJ +Aqjx5boVrUFH1J4zAgMBAAGjfDB6MB0GA1UdDgQWBBRjObpZQdsXe5pq7k501MOU +5ChXezAfBgNVHSMEGDAWgBTbpLdIynbiSsoPBfnP949MNgHJxzAOBgNVHQ8BAf8E +BAMCA/gwCQYDVR0TBAIwADAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEw +DQYJKoZIhvcNAQELBQADggEBADPa3w6bgNuY8oTRxzinXRCJKFFiVDM0SI3K0qEo +4Evg5np1xgIou6p4k9QHAmbb0wnVdOJQ+74PXpm1Z7kSmZAXz/wXFFKQSmVILWTH +EO23ThBPDOzvks1DXo76KjW1rTMsXHV0rTBzq/OJShfA5zGfXuKuI+60EozsO0Xh +bSeBUjmW0wU7b/dYw2WrmeBFPvz9VhpQG8ZL3cfMvjqIYtn8qRu+gw5pMdKJOpQV +4YcyAhusREGMWc4Lmq+kXGk4UlOy9imUNuKkT10e9IS7STZHVqoKXPs5Y2jJODrr +S8fvbRJUvu/WLRRi0AXsC7MD5U1p+emBeLF3g95MneCbjEg= +-----END CERTIFICATE----- +Bag Attributes + friendlyName: CN=Test CA (Do Not Trust),OU=nifi +subject=/OU=nifi/CN=Test CA (Do Not Trust) +issuer=/OU=nifi/CN=Test CA (Do Not Trust) +-----BEGIN CERTIFICATE----- +MIIDYzCCAkugAwIBAgIKAWfClyDGAAAAADANBgkqhkiG9w0BAQsFADAwMQ0wCwYD +VQQLDARuaWZpMR8wHQYDVQQDDBZUZXN0IENBIChEbyBOb3QgVHJ1c3QpMB4XDTE4 +MTIxODE4MzIyM1oXDTQ2MDUwNDE4MzIyM1owMDENMAsGA1UECwwEbmlmaTEfMB0G +A1UEAwwWVGVzdCBDQSAoRG8gTm90IFRydXN0KTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAIv7lVgGRHGaYmKkeTJpFzAp6QA7Anik/u1a+1ngGFWf9e6l +RkSX6US+nPbDRLJpSkO0c+/v8BwAKBiHUFaGF9XV7YvX92x/Gb3/FidSu+HAW/w/ +keIZ8PHvXbMtTvEur+nY1hSDvssdw1nAYAB9DG26HdRSg5c1DYgHLk9WCDWuIspU +n31YCb0lStWWbHM53i8xLfeV3IdOw9P3+d8bopzUUjk2quSxxekvzLCC1e14csJG +GIKLplRUq+zWRgkGYF8Fkx+kYGL62sehAdVcblxjwnXnmlPHvlxeaclsAVn4LCQj +gQzstzAv+s7sNSCxHba4vAusszWxOFiM1Vk8VvcCAwEAAaN/MH0wDgYDVR0PAQH/ +BAQDAgH+MAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFNukt0jKduJKyg8F+c/3j0w2 +AcnHMB8GA1UdIwQYMBaAFNukt0jKduJKyg8F+c/3j0w2AcnHMB0GA1UdJQQWMBQG +CCsGAQUFBwMCBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAQEAMvNsYLooq3zh +ts0fPU8dNcfe/NXFK6Uwg0RQPtq/l1ChGnZgXicx+RHMR5Q08pR62e+3gztk+LRE +iR9PpXqKFLM8slhR1z4sZ+Ja38ZHcOjsDPJeMKjUTrK8MNQN3YPKzoPE0AnLmsZI +Kf1eUIXXA3uXiXkIIVuxPPK96Q5Rla0xnbOpgejzGJ0BIMFP3odLlSahtT2Gl6wC +bdyImBkFntRJMoUx1fwUSKvIN5GUpaG6+E3mwgjckTUGZ15WrAllWqzhI06T73Yv +qR4FsQizqrqLimrIgvCBH6SWbOcsjCH/I58KqMRtG+kmfa/iwMfy0MMzuzx1Kwbr +qOi08D8F0w== +-----END CERTIFICATE----- diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/user1-key.pem b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/user1-key.pem new file mode 100644 index 0000000000..f288cda20a --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/user1-key.pem @@ -0,0 +1,84 @@ +Bag Attributes + friendlyName: user1-key + localKeyID: 54 69 6D 65 20 31 35 34 35 31 35 37 39 34 38 38 33 36 +Key Attributes: +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIb383JvWdLvECAggA +MBQGCCqGSIb3DQMHBAhzr4pzpi7sRASCBMhm5cb7aAuvV28Oaj9pGKPtekdWNEwr +zKVzoy9xu0QTef/m0JxCuRW0qnfyGKzYHFjjgfxvA3deAnQYpnxDmWPJwHbw/mzx +RM/YRTiFa45w5HulOc9t+C5N4pess7QVNYgaWp3EcbjtuwrPry0HvJFqM7JyNoK+ +g3pyZ6pZq1CZFSua4oNEu30frbiTYXKeMMa/YTNmN2jLtS3VkY1BSXYm/jtIktFw +YCAsGz/9u4jIWt2SqWR1ieV4d3PGrvFm6yvBq1dZx/jIpHfZU0ozMEUqwZx4Hevs +VoPmwHMI/0SDWtXgso6zGgKKkXu0BdUhR+m4C1xg8iBAFWQxrNASQb+q6RcCymFs +j0nDe9PdS6w764KgkcRPv5xo2kEqULy1D/gXxAbSWq9luvG1ljJ/6gvdg8StFIx0 +ugAgywm9/++TAiM/OU4fxkFF0D0QhHbuwBVynbUHdOFSclrEZ4vzyW+1VUf+Z8KT +u4YicaSAXY6reEYp2Qcux93OKEqQ62uM7cMI3QItKqbSX+WokPev1iKgjqMaqAUU +jtypACu+LwANQmTkWUaLGdAHanKhuXiYmt1wMLbQ0U2kOEZ83si8sBerKI1qUOh7 +0ZFqOrIhRxZH0OErPBLyBER41OMHtlTdKEFHDJAZ5emwIFNV23dHM9I401PfFeHb +F1xLl30GThKC/xyUWmkbTUKQ/4GJZPAhLKvOQCbWiMmTpBeCdyADIQO4V4RrGuzT +UwAdhfMkMPbyi25iLtVxhm7PUN9ywhbj/KzPMkc/1/M8yUw3gzo7GGBPEiOxNdc/ +sqeG69tn/P2qocvuH646zSO7IgcOcadrGpzZhana0nt1SEOqAFg379NXNXZPmhzW +BOFj5W2PlWe9cfBrQxbFkF0F+RIrTnwYUyJCpqTgJkIPtvmwDPODFAwvPvMPal1a +AjsqucMEvDY5lHMYyPA3/heneO9oCavrJEt1wKS0MCdNVN4a8hEQsmCz71MaYEiI +G1/t1jVIFc7N8zGApfuzPHnaquRnkdlMaEArJDkXLi0m4OC2rVXoJrlqtJAzkT/8 +qzG030F21G/STOpjYjKjqU+woEmT+ZmLGNCme5oBAo3OQbIe2Jf5uU5jdlh+3PnO +QBES7sMPH9TlMauGKd6s6+MHB43YOQj4PpkLYWdnK8GTiPqSrx4/gZipyL8ZvY7V +2wPJqE3nxQleWoy8Y6NVuph0LM8dhcmdGPo8sw0wm0Q2f+fG/L7j5BVuhYB66YNR +pxNrNKu4KWfmye5XuZLK6DJvPZUu5C8zNQedt8853zacsjypzfJzQRmEav3p1oL8 +2zReH8MyXK6VSBEPS+3u/K/I6oqUXyhzIsf/K4uXkIj4WcQeionPtk7ibcA5nAo0 +aEQFNze889Ns8wOPicswnTk4SffS6adM+1eJ+BmMVWAAs+LfW84hJkvlP5ik4UWZ +Fr0YLDkXLB71NmzhA7jVP+wnZtRjurcyLabT3p4u4t+eBnwLUCU9Mk5M1Y1hnR2K +viioDqvla2W1WfC9uPsdq5+5ufUYRa79fZjne+YY8Yyh/Vyf7cXKiNo//1t3U++x +TE1xsQvr4y40MZowemEZav215/R1m7ZbPWYOn2dz6Qv6qAWyiAcCuo9hqwpSsVwg +4TM= +-----END ENCRYPTED PRIVATE KEY----- +Bag Attributes + friendlyName: user1-key + localKeyID: 54 69 6D 65 20 31 35 34 35 31 35 37 39 34 38 38 33 36 +subject=/OU=nifi/CN=user1 +issuer=/OU=nifi/CN=Test CA (Do Not Trust) +-----BEGIN CERTIFICATE----- +MIIDTzCCAjegAwIBAgIKAWfClyTXAAAAADANBgkqhkiG9w0BAQsFADAwMQ0wCwYD +VQQLDARuaWZpMR8wHQYDVQQDDBZUZXN0IENBIChEbyBOb3QgVHJ1c3QpMB4XDTE4 +MTIxODE4MzIyNFoXDTQ2MDUwNDE4MzIyNFowHzENMAsGA1UECwwEbmlmaTEOMAwG +A1UEAwwFdXNlcjEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCKSsvi +WSrOIDjF+drgVnrB1QYk1dNeQp64nE7ffzlPoEOWbNQzho+JTp1D9XI9+IBUmSTW +f4JeYDs0gTpbfwzVmsLI30u/a0wUgERM3+8fl9XwrSJ4blkG4A8az4i3CFjObOYA +6znDclHLGcmWthwjxQ50n/BjTUGHJFpK/1/uLLgfeJreP73RdTm8fPMRj9H1JhpV +/UsP4w0EJX3sr2acKW34w+edkQgbGxxg2+dAokb/ODrO6/wntbYaFJguCEe5jQL+ +bfSVMiix99RjTeflFaKU0fQI0GYbcf6wmz9JUEvX9JXiclVyu9daV6jUgKJcg6SJ +Aqjx5boVrUFH1J4zAgMBAAGjfDB6MB0GA1UdDgQWBBRjObpZQdsXe5pq7k501MOU +5ChXezAfBgNVHSMEGDAWgBTbpLdIynbiSsoPBfnP949MNgHJxzAOBgNVHQ8BAf8E +BAMCA/gwCQYDVR0TBAIwADAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEw +DQYJKoZIhvcNAQELBQADggEBADPa3w6bgNuY8oTRxzinXRCJKFFiVDM0SI3K0qEo +4Evg5np1xgIou6p4k9QHAmbb0wnVdOJQ+74PXpm1Z7kSmZAXz/wXFFKQSmVILWTH +EO23ThBPDOzvks1DXo76KjW1rTMsXHV0rTBzq/OJShfA5zGfXuKuI+60EozsO0Xh +bSeBUjmW0wU7b/dYw2WrmeBFPvz9VhpQG8ZL3cfMvjqIYtn8qRu+gw5pMdKJOpQV +4YcyAhusREGMWc4Lmq+kXGk4UlOy9imUNuKkT10e9IS7STZHVqoKXPs5Y2jJODrr +S8fvbRJUvu/WLRRi0AXsC7MD5U1p+emBeLF3g95MneCbjEg= +-----END CERTIFICATE----- +Bag Attributes + friendlyName: CN=Test CA (Do Not Trust),OU=nifi +subject=/OU=nifi/CN=Test CA (Do Not Trust) +issuer=/OU=nifi/CN=Test CA (Do Not Trust) +-----BEGIN CERTIFICATE----- +MIIDYzCCAkugAwIBAgIKAWfClyDGAAAAADANBgkqhkiG9w0BAQsFADAwMQ0wCwYD +VQQLDARuaWZpMR8wHQYDVQQDDBZUZXN0IENBIChEbyBOb3QgVHJ1c3QpMB4XDTE4 +MTIxODE4MzIyM1oXDTQ2MDUwNDE4MzIyM1owMDENMAsGA1UECwwEbmlmaTEfMB0G +A1UEAwwWVGVzdCBDQSAoRG8gTm90IFRydXN0KTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAIv7lVgGRHGaYmKkeTJpFzAp6QA7Anik/u1a+1ngGFWf9e6l +RkSX6US+nPbDRLJpSkO0c+/v8BwAKBiHUFaGF9XV7YvX92x/Gb3/FidSu+HAW/w/ +keIZ8PHvXbMtTvEur+nY1hSDvssdw1nAYAB9DG26HdRSg5c1DYgHLk9WCDWuIspU +n31YCb0lStWWbHM53i8xLfeV3IdOw9P3+d8bopzUUjk2quSxxekvzLCC1e14csJG +GIKLplRUq+zWRgkGYF8Fkx+kYGL62sehAdVcblxjwnXnmlPHvlxeaclsAVn4LCQj +gQzstzAv+s7sNSCxHba4vAusszWxOFiM1Vk8VvcCAwEAAaN/MH0wDgYDVR0PAQH/ +BAQDAgH+MAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFNukt0jKduJKyg8F+c/3j0w2 +AcnHMB8GA1UdIwQYMBaAFNukt0jKduJKyg8F+c/3j0w2AcnHMB0GA1UdJQQWMBQG +CCsGAQUFBwMCBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAQEAMvNsYLooq3zh +ts0fPU8dNcfe/NXFK6Uwg0RQPtq/l1ChGnZgXicx+RHMR5Q08pR62e+3gztk+LRE +iR9PpXqKFLM8slhR1z4sZ+Ja38ZHcOjsDPJeMKjUTrK8MNQN3YPKzoPE0AnLmsZI +Kf1eUIXXA3uXiXkIIVuxPPK96Q5Rla0xnbOpgejzGJ0BIMFP3odLlSahtT2Gl6wC +bdyImBkFntRJMoUx1fwUSKvIN5GUpaG6+E3mwgjckTUGZ15WrAllWqzhI06T73Yv +qR4FsQizqrqLimrIgvCBH6SWbOcsjCH/I58KqMRtG+kmfa/iwMfy0MMzuzx1Kwbr +qOi08D8F0w== +-----END CERTIFICATE----- diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/user1-ks.jks b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/user1-ks.jks new file mode 100644 index 0000000000..94ddf05902 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/user1-ks.jks differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/user1-ks.p12 b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/user1-ks.p12 new file mode 100644 index 0000000000..47e27731d0 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/user1-ks.p12 differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-docs/pom.xml b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/pom.xml new file mode 100644 index 0000000000..8c4342031d --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + + org.apache.nifi.registry + nifi-registry-core + 1.14.0-SNAPSHOT + + nifi-registry-web-docs + war + + + true + true + + + + + + org.apache.rat + apache-rat-plugin + + + src/main/webapp/js/jquery.min.js + + + + + + + + + org.apache.nifi.registry + nifi-registry-utils + 1.14.0-SNAPSHOT + provided + + + org.apache.commons + commons-lang3 + provided + + + javax.servlet + javax.servlet-api + + + + javax.servlet.jsp.jstl + jstl + 1.2 + + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/java/org/apache/nifi/registry/web/docs/DocumentationController.java b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/java/org/apache/nifi/registry/web/docs/DocumentationController.java new file mode 100644 index 0000000000..3283c737e4 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/java/org/apache/nifi/registry/web/docs/DocumentationController.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.nifi.registry.web.docs; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * + */ +public class DocumentationController extends HttpServlet { + + private static final int GENERAL_LINK_COUNT = 4; + private static final int DEVELOPER_LINK_COUNT = 2; + + // context for accessing the extension mapping + private ServletContext servletContext; + + @Override + public void init(final ServletConfig config) throws ServletException { + super.init(config); + servletContext = config.getServletContext(); + } + + /** + * + * @param request servlet request + * @param response servlet response + * @throws ServletException if a servlet-specific error occurs + * @throws IOException if an I/O error occurs + */ + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + // forward appropriately + request.getRequestDispatcher("/WEB-INF/jsp/documentation.jsp").forward(request, response); + } + +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/resources/META-INF/LICENSE b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/resources/META-INF/LICENSE new file mode 100644 index 0000000000..5f683bc114 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/resources/META-INF/LICENSE @@ -0,0 +1,223 @@ + + Apache License + Version 2.0, January 2004 + https://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: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) 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 + + (d) 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 + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + +This product bundles 'JQuery' which is available under and MIT style license. + (c) 2005, 2014 jQuery Foundation, Inc. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/resources/META-INF/NOTICE b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/resources/META-INF/NOTICE new file mode 100644 index 0000000000..54829e631c --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/resources/META-INF/NOTICE @@ -0,0 +1,14 @@ +nifi-web-docs +Copyright 2014-2017 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (https://www.apache.org/). + +************************ +Common Development and Distribution License 1.1 +************************ + +The following binary components are provided under the Common Development and Distribution License 1.1. See project link for details. + + (CDDL 1.1) (GPL2 w/ CPE) Java Servlet API (javax.servlet:javax.servlet-api:jar:3.1.0 - https://servlet-spec.java.net) + (CDDL 1.1) (GPL2 w/ CPE) JavaServer Pages Standard Tag Library (javax.servlet.jsp.jstl:jstl:jar:1.2 - https://javaee.github.io/jstl-api/) \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/WEB-INF/jsp/documentation.jsp b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/WEB-INF/jsp/documentation.jsp new file mode 100644 index 0000000000..0db7c41c11 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/WEB-INF/jsp/documentation.jsp @@ -0,0 +1,84 @@ +<%-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You 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. +--%> +<%@ page contentType="text/html" pageEncoding="UTF-8" session="false" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + + NiFi Registry Documentation + + + + + + + + + + + +
+ +
+
NiFi Registry Documentation
+
+
+
+
+
+
+
+
+
General
+ +
+
+
Developer
+ +
+
+
+
+ +
+
+ + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/WEB-INF/jsp/no-documentation-found.jsp b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/WEB-INF/jsp/no-documentation-found.jsp new file mode 100644 index 0000000000..ce34b1be92 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/WEB-INF/jsp/no-documentation-found.jsp @@ -0,0 +1,31 @@ +<%-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You 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. +--%> +<%@ page contentType="text/html" pageEncoding="UTF-8" session="false" %> + + + + NiFi + + + + + + +

Yikes!

+

Unable to locate the documentation for the selected item.

+ + \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/WEB-INF/web.xml b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000..cf74ba63fd --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,33 @@ + + + + nifi-registry-docs + + 404 + /WEB-INF/jsp/no-documentation-found.jsp + + + documentation-controller + org.apache.nifi.registry.web.docs.DocumentationController + + + documentation-controller + /documentation + + + documentation + + diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/css/component-usage.css b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/css/component-usage.css new file mode 100644 index 0000000000..65f62ca5a4 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/css/component-usage.css @@ -0,0 +1,192 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +@import "https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic|Noto+Serif:400,400italic,700,700italic|Droid+Sans+Mono:400"; + +html, html a { + -webkit-font-smoothing: antialiased; + text-shadow: 1px 1px 1px rgba(0,0,0,0.004); +} + +body { + margin: 0 auto; + display: block; + font-family: "Open Sans","DejaVu Sans",sans-serif; + padding-left: 20px; +} + +.title { + font-weight: bold; + color: #7a2518; + font-size: 18px; +} + +.hidden { + display: none; +} + +/* tables */ + +table { + color:#666; + font-size:14px; + background:#eaebec; + border:#ccc 1px solid; + -webkit-border-radius:3px; + border-radius:3px; + width: 100%; + word-wrap: break-word; +} + +table th { + padding:11px 15px 12px 15px; + border-top:1px solid #fafafa; + border-bottom:1px solid #e0e0e0; + + background: #ededed; +} + +table th:first-child { + text-align: left; + padding-left:10px; +} + +table th:last-child { + text-align: left; + padding-left:10px; +} + +table tr:first-child th:first-child { + border-top-left-radius:3px; +} + +table tr:first-child th:last-child { + border-top-right-radius:3px; +} + +table tr { + text-align: center; + padding-left:10px; +} + +table td:first-child { + text-align: left; + padding-left:10px; + border-left: 0; +} + +table td:last-child { + text-align: left; + padding-left:10px; + border-left: 0; + vertical-align: top; + +} + +table td { + padding:12px; + background: #fafafa; +} + +table tr:last-child td { + border-bottom:0; +} + +table tr:last-child td:first-child { + border-bottom-left-radius:3px; +} + +table tr:last-child td:last-child { + border-bottom-right-radius:3px; +} + +td#default-value, td#name, td#value { + max-width: 200px; +} + +td#allowable-values { + max-width: 300px; +} + +td#description { + vertical-align: middle; +} + +td#bundle-info { + max-width: 50px; +} + +/* links */ + +a, a:link, a:visited { + cursor: pointer; + color: #2156a5; + text-decoration: none; + border: none; +} + +a:hover, a:active { + color: #2156a5; + text-decoration: none; + border: none; +} + +.clear { + clear: both; +} + +/* p */ + +p { + font-family: 'Noto Serif', 'DejaVu Serif', serif; + font-size: 16px; +} + +p strong { + font-weight: bold; +} + +/* ul li */ +td ul { + margin: 0px 0px 0px 0px; + padding-left: 20px; +} +ul li { + text-align: left; + display: list-item; +} + +ul li strong { + font-weight: bold; +} + +h2 { + font-weight: normal; + color: #ba3925; +} + +/* pre */ + +pre { + font-size: 14px; + background-color: #fefefe; + border: 1px solid #ccc; + border-left: 6px solid #ccc; + color: #555; + margin-bottom: 10px; + padding: 5px 8px; +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/css/main.css b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/css/main.css new file mode 100644 index 0000000000..8b50064dc7 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/css/main.css @@ -0,0 +1,217 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ +* { + margin: 0; + padding: 0; +} + +#documentation-body { + width: 100%; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + +/* banners */ + +div.main-banner-header { + display: none; + font-weight: bold; + font-size: 1em; + text-align: center; + line-height: 15px; + color: #7e7e7e; + margin: 0px auto; + width: 100%; + height: 1em; + background-color: #fff; + background-image: url(../images/bgHeader.png); + background-position: center; + background-repeat: no-repeat; +} + +div.main-banner-footer { + display: none; + color: #fff; + text-align: center; + font-weight: bold; + font-size: 1em; + overflow: visible; + background-color: #9eb9c7; + background-image: url(../images/bgBannerFoot.png); + background-repeat: repeat-x; + background-position: left top; +} + +/* documentation */ + +div.documentation-header { + border-bottom: 1px solid #d1dee5; + color: #365c6a; + font-size: 13px; + display: flex; +} + +#component-list-toggle-link { + padding: 4px; + font-size: 14px; + font-weight: bold; + color: #264c58; + cursor: pointer; + width: 12px; + text-align: center; + align-self: flex-end; +} + +#header-contents { + display: flex; + flex-wrap: wrap; +} + +#nf-title { + font-size: 20px; + margin: 5px 5px 0px 5px; +} + +#nf-version { + font-size: 14px; + margin: 11px 5px 0px 5px; + flex-grow: 1; +} + +.version { + font-style: italic; + color: #aaa; +} + +#selected-component { + font-size: 20px; + margin: 5px 5px 0px 5px; +} + +/* content flex-box containers */ + +#component-root-container { + display: flex; + flex-wrap: wrap; + align-items: stretch; + width: 100%; +} + +#component-listing-container { + flex-grow: 1; + min-width: 312px; + max-width: 350px; + padding: 0px 4px 0px 4px; +} + +#component-usage-container { + flex-grow: 4; + min-width: 300px; + padding: 0px 4px 0px 4px; +} + +/* component listing */ + +div.component-listing { + overflow: auto; + font-size: 16px; +} + +div.component-listing div.section { + margin-bottom: 15px; +} + +div.component-listing div.header { + font-weight: bold; + color: #264c58; +} + +div.component-links ul { + list-style: none; +} + +li.component-item { + padding: 2px; + padding-left: 4px; + border-left: 8px solid transparent; + font-family: "Open Sans","DejaVu Sans",sans-serif; + font-size: 15px; +} + +li.component-item a { + color: #1e373f; +} + +li.component-item:hover { + border-left: 8px solid #d1dee5; +} + +li.component-item:hover a { + color: #264c58; +} + +li.component-item.selected { + border-left: 8px solid #7098ad; +} + +div.component-links span.no-components { + font-style: italic; + color: #777; +} + +/* component filter control */ + +#component-filter-controls { +} + +#component-filter-container { + margin-left: 2px; +} + +#component-filter { + font-size: 12px; + height: 18px; + line-height: 20px; + width: 98%; + float: left; +} + +input.component-filter-list { + color: #888; + font-style: italic; +} + +#component-filter-stats { + font-size: 9px; + font-weight: bold; + color: #9f6000; + clear: left; + line-height: normal; + margin-left: 7px; +} + +/* component usage */ + +#component-usage { + overflow: auto; + width: 100%; + height: 100%; + position: absolute; +} \ No newline at end of file diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/images/bgBannerFoot.png b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/images/bgBannerFoot.png new file mode 100755 index 0000000000..16a17fea11 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/images/bgBannerFoot.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/images/bgHeader.png b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/images/bgHeader.png new file mode 100644 index 0000000000..3cf88c54e3 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/images/bgHeader.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/images/bgTableHeader.png b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/images/bgTableHeader.png new file mode 100755 index 0000000000..8f5e058fea Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/images/bgTableHeader.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/images/iconInfo.png b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/images/iconInfo.png new file mode 100644 index 0000000000..8c53b4ca0c Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/images/iconInfo.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/images/registry-favicon.png b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/images/registry-favicon.png new file mode 100644 index 0000000000..87dc8c96b1 Binary files /dev/null and b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/images/registry-favicon.png differ diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/js/application.js b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/js/application.js new file mode 100644 index 0000000000..e0b455887f --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/js/application.js @@ -0,0 +1,400 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + +/* global top */ + +$(document).ready(function () { + + var isUndefined = function (obj) { + return typeof obj === 'undefined'; + }; + + var isNull = function (obj) { + return obj === null; + }; + + var isDefinedAndNotNull = function (obj) { + return !isUndefined(obj) && !isNull(obj); + }; + + /** + * Get the filter text. + * + * @returns {unresolved} + */ + var getFilterText = function () { + var filter = ''; + var ruleFilter = $('#component-filter'); + if (!ruleFilter.hasClass('component-filter-list')) { + filter = ruleFilter.val(); + } + return filter; + }; + + var applyComponentFilter = function (componentContainer) { + var matchingComponents = 0; + var componentLinks = $(componentContainer).find('a.component-link, a.document-link'); + + if (componentLinks.length === 0) { + return matchingComponents; + } + + // get the filter text + var filter = getFilterText(); + if (filter !== '') { + var filterExp = new RegExp(filter, 'i'); + + // update the displayed rule count + $.each(componentLinks, function (_, componentLink) { + var a = $(componentLink); + var li = a.closest('li.component-item'); + + // get the rule text for matching + var componentName = a.text(); + + // see if any of the text from this rule matches + var componentMatches = componentName.search(filterExp) >= 0; + + // handle whether the rule matches + if (componentMatches === true) { + li.show(); + matchingComponents++; + } else { + // hide the rule + li.hide(); + } + }); + } else { + // ensure every rule is visible + componentLinks.closest('li.component-item').show(); + + // set the number of displayed rules + matchingComponents = componentLinks.length; + } + + // show whether there are status if appropriate + var noMatching = componentContainer.find('span.no-matching'); + if (matchingComponents === 0) { + noMatching.show(); + } else { + noMatching.hide(); + } + + return matchingComponents; + }; + + var applyFilter = function () { + var matchingGeneral = applyComponentFilter($('#general-links')); + var matchingProcessors = applyComponentFilter($('#processor-links')); + var matchingControllerServices = applyComponentFilter($('#controller-service-links')); + var matchingReportingTasks = applyComponentFilter($('#reporting-task-links')); + var matchingDeveloper = applyComponentFilter($('#developer-links')); + + // update the rule count + $('#displayed-components').text(matchingGeneral + matchingProcessors + matchingControllerServices + matchingReportingTasks + matchingDeveloper); + }; + + var selectComponent = function (selectedExtension, selectedBundleGroup, selectedBundleArtifact, selectedArtifactVersion) { + var componentLinks = $('a.component-link'); + + // consider each link + $.each(componentLinks, function () { + var componentLink = $(this); + var item = componentLink.closest('li.component-item'); + var extension = item.find('span.extension-class').text(); + var group = item.find('span.bundle-group').text(); + var artifact = item.find('span.bundle-artifact').text(); + var version = item.find('span.bundle-version').text(); + + if (extension === selectedExtension && group === selectedBundleGroup + && artifact === selectedBundleArtifact && version === selectedArtifactVersion) { + + // remove all selected styles + $('li.component-item').removeClass('selected'); + + // select this links item + item.addClass('selected'); + + // set the header + $('#selected-component').text(componentLink.text()); + + // stop iteration + return false; + } + }); + }; + + var selectDocument = function (documentName) { + var documentLinks = $('a.document-link'); + + // consider each link + $.each(documentLinks, function () { + var documentLink = $(this); + if (documentName === $.trim(documentLink.text())) { + // remove all selected styles + $('li.component-item').removeClass('selected'); + + // select this links item + documentLink.closest('li.component-item').addClass('selected'); + + // set the header + $('#selected-component').text(documentLink.text()); + + // stop iteration + return false; + } + }); + }; + + // get the banners if we're not in the shell + var bannerHeaderHeight = 0; + var bannerFooterHeight = 0; + var banners = $.Deferred(function (deferred) { + if (top === window) { + $.ajax({ + type: 'GET', + url: '../nifi-api/flow/banners', + dataType: 'json' + }).then(function (response) { + // ensure the banners response is specified + if (isDefinedAndNotNull(response.banners)) { + if (isDefinedAndNotNull(response.banners.headerText) && response.banners.headerText !== '') { + // update the header text + var bannerHeader = $('#banner-header').text(response.banners.headerText).show(); + bannerHeaderHeight = bannerHeader.height(); + } + + if (isDefinedAndNotNull(response.banners.footerText) && response.banners.footerText !== '') { + // update the footer text and show it + var bannerFooter = $('#banner-footer').text(response.banners.footerText).show(); + bannerFooterHeight = bannerFooter.height(); + } + } + + deferred.resolve(); + }, function () { + deferred.reject(); + }); + } else { + deferred.resolve(); + } + }).promise(); + + // get the about details + var about = $.ajax({ + type: 'GET', + url: '../nifi-api/flow/about', + dataType: 'json' + }).done(function (response) { + var aboutDetails = response.about; + + // set the document title and the about title + $('#nf-version').text(aboutDetails.version); + }); + + // once the banners have loaded, function with remainder of the page + $.when(banners, about).always(function () { + // define the function for filtering the list + $('#component-filter').keyup(function () { + applyFilter(); + }).focus(function () { + if ($(this).hasClass('component-filter-list')) { + $(this).removeClass('component-filter-list').val(''); + } + }).blur(function () { + if ($(this).val() === '') { + $(this).addClass('component-filter-list').val('Filter'); + } + }).addClass('component-filter-list').val('Filter'); + + // get the component containers to install the window listener + var documentationHeader = $('#documentation-header'); + var componentRootContainer = $('#component-root-container'); + var componentListingContainer = $('#component-listing-container', componentRootContainer); + var componentListing = $('#component-listing', componentListingContainer); + var componentFilterControls = $('#component-filter-controls', componentRootContainer); + var componentUsageContainer = $('#component-usage-container', componentUsageContainer); + var componentUsage = $('#component-usage', componentUsageContainer); + + var componentListingContainerPaddingX = 0; + componentListingContainerPaddingX += parseInt(componentListingContainer.css("padding-right"), 10); + componentListingContainerPaddingX += parseInt(componentListingContainer.css("padding-left"), 10); + + var componentListingContainerPaddingY = 0; + componentListingContainerPaddingY += parseInt(componentListingContainer.css("padding-top"), 10); + componentListingContainerPaddingY += parseInt(componentListingContainer.css("padding-bottom"), 10); + + var componentUsageContainerPaddingX = 0; + componentUsageContainerPaddingX += parseInt(componentUsageContainer.css("padding-right"), 10); + componentUsageContainerPaddingX += parseInt(componentUsageContainer.css("padding-left"), 10); + + var componentUsageContainerPaddingY = 0; + componentUsageContainerPaddingY += parseInt(componentUsageContainer.css("padding-top"), 10); + componentUsageContainerPaddingY += parseInt(componentUsageContainer.css("padding-bottom"), 10); + + var componentListingContainerMinWidth = parseInt(componentListingContainer.css("min-width"), 10) + componentListingContainerPaddingX; + var componentUsageContainerMinWidth = parseInt(componentUsageContainer.css("min-width"), 10) + componentUsageContainerPaddingX; + var smallDisplayBoundary = componentListingContainerMinWidth + componentUsageContainerMinWidth; + + var cssComponentListingNormal = { backgroundColor: "#ffffff" }; + var cssComponentListingSmall = { backgroundColor: "#fbfbfb" }; + + // add a window resize listener + $(window).resize(function () { + // This -1 is the border-top of #component-usage-container + var baseHeight = window.innerHeight - 1; + baseHeight -= bannerHeaderHeight; + baseHeight -= bannerFooterHeight; + baseHeight -= documentationHeader.height(); + + // resize component list accordingly + if (smallDisplayBoundary > window.innerWidth) { + // screen is not wide enough to display content usage + // within the same row. + componentListingContainer.css(cssComponentListingSmall); + componentListingContainer.css({ + borderBottom: "1px solid #ddddd8" + }); + componentListing.css({ + height: "200px" + }); + // resize the iframe accordingly + var componentUsageHeight = baseHeight; + if (componentListingContainer.is(":visible")) { + componentUsageHeight -= componentListingContainer.height(); + componentUsageHeight -= 1; // border-bottom + } + componentUsageHeight -= componentListingContainerPaddingY; + componentUsageHeight -= componentUsageContainerPaddingY; + componentUsage.css({ + width: componentUsageContainer.width(), + height: componentUsageHeight + }); + componentUsageContainer.css({ + height: componentUsage.height() + }); + } else { + componentListingContainer.css(cssComponentListingNormal); + + var componentListingHeight = baseHeight; + componentListingHeight -= componentFilterControls.height(); + componentListingHeight -= componentListingContainerPaddingY; + componentListing.css({ + height: componentListingHeight + }); + + // resize the iframe accordingly + componentUsage.css({ + width: componentUsageContainer.width(), + height: baseHeight - componentUsageContainerPaddingY + }); + componentUsageContainer.css({ + height: componentUsage.height() + }); + componentListingContainer.css({ + borderBottom: "0px" + }); + } + }); + + + var toggleComponentListing = $('#component-list-toggle-link'); + toggleComponentListing.click(function(){ + componentListingContainer.toggle(0, function(){ + toggleComponentListing.text($(this).is(":visible") ? "-" : "+"); + $(window).resize(); + }); + }); + + // listen for loading of the iframe to update the title + $('#component-usage').on('load', function () { + + // resize window accordingly. + $(window).resize(); + + var bundleAndComponent = ''; + var href = $(this).contents().get(0).location.href; + + // see if the href ends in index.htm[l] + var indexOfIndexHtml = href.indexOf('index.htm'); + if (indexOfIndexHtml >= 0) { + href = href.substring(0, indexOfIndexHtml); + } + + // remove the trailing separator + if (href.length > 0) { + var indexOfSeparator = href.lastIndexOf('/'); + if (indexOfSeparator === href.length - 1) { + href = href.substring(0, indexOfSeparator); + } + } + + // remove the beginning bits + if (href.length > 0) { + var path = 'nifi-docs/components'; + var indexOfPath = href.indexOf(path); + if (indexOfPath >= 0) { + var indexOfBundle = indexOfPath + path.length + 1; + if (indexOfBundle < href.length) { + bundleAndComponent = href.substr(indexOfBundle); + } + } + } + + // if we could extract the bundle coordinates + if (bundleAndComponent !== '') { + var bundleTokens = bundleAndComponent.split('/'); + if (bundleTokens.length === 4) { + selectComponent(bundleTokens[3], bundleTokens[0], bundleTokens[1], bundleTokens[2]); + } + } + }); + + // listen for on the rest api and user guide and developer guide and admin guide and overview + $('a.document-link').on('click', function() { + selectDocument($(this).text()); + }); + + // get the initial selection + var initialLink = $('a.document-link:first'); + var initialSelectionType = $.trim($('#initial-selection-type').text()); + + if (initialSelectionType !== '') { + var initialSelectionBundleGroup = $.trim($('#initial-selection-bundle-group').text()); + var initialSelectionBundleArtifact = $.trim($('#initial-selection-bundle-artifact').text()); + var initialSelectionBundleVersion = $.trim($('#initial-selection-bundle-version').text()); + + $('a.component-link').each(function () { + var componentLink = $(this); + var item = componentLink.closest('li.component-item'); + var extension = item.find('span.extension-class').text(); + var group = item.find('span.bundle-group').text(); + var artifact = item.find('span.bundle-artifact').text(); + var version = item.find('span.bundle-version').text(); + + if (extension === initialSelectionType && group === initialSelectionBundleGroup + && artifact === initialSelectionBundleArtifact && version === initialSelectionBundleVersion) { + initialLink = componentLink; + return false; + } + }); + } + + // click the first link + initialLink[0].click(); + }); +}); diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/js/jquery.min.js b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/js/jquery.min.js new file mode 100644 index 0000000000..4c5be4c0fb --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/js/jquery.min.js @@ -0,0 +1,4 @@ +/*! jQuery v3.1.1 | (c) jQuery Foundation | jquery.org/license */ +!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.1.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext,B=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,C=/^.[^:#\[\.,]*$/;function D(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):C.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(D(this,a||[],!1))},not:function(a){return this.pushStack(D(this,a||[],!0))},is:function(a){return!!D(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var E,F=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,G=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||E,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:F.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),B.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};G.prototype=r.fn,E=r(d);var H=/^(?:parents|prev(?:Until|All))/,I={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function J(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return J(a,"nextSibling")},prev:function(a){return J(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return a.contentDocument||r.merge([],a.childNodes)}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(I[a]||r.uniqueSort(e),H.test(a)&&e.reverse()),this.pushStack(e)}});var K=/[^\x20\t\r\n\f]+/g;function L(a){var b={};return r.each(a.match(K)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?L(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function M(a){return a}function N(a){throw a}function O(a,b,c){var d;try{a&&r.isFunction(d=a.promise)?d.call(a).done(b).fail(c):a&&r.isFunction(d=a.then)?d.call(a,b,c):b.call(void 0,a)}catch(a){c.call(void 0,a)}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==N&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:M,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:M)),c[2][3].add(g(0,a,r.isFunction(d)?d:N))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(O(a,g.done(h(c)).resolve,g.reject),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)O(e[c],h(c),g.reject);return g.promise()}});var P=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&P.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var Q=r.Deferred();r.fn.ready=function(a){return Q.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,holdReady:function(a){a?r.readyWait++:r.ready(!0)},ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||Q.resolveWith(d,[r]))}}),r.ready.then=Q.then;function R(){d.removeEventListener("DOMContentLoaded",R), +a.removeEventListener("load",R),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",R),a.addEventListener("load",R));var S=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)S(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){W.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=V.get(a,b),c&&(!d||r.isArray(c)?d=V.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return V.get(a,c)||V.access(a,c,{empty:r.Callbacks("once memory").add(function(){V.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,ka=/^$|\/(?:java|ecma)script/i,la={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};la.optgroup=la.option,la.tbody=la.tfoot=la.colgroup=la.caption=la.thead,la.th=la.td;function ma(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&r.nodeName(a,b)?r.merge([a],c):c}function na(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=ma(l.appendChild(f),"script"),j&&na(g),c){k=0;while(f=g[k++])ka.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var qa=d.documentElement,ra=/^key/,sa=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ta=/^([^.]*)(?:\.(.+)|)/;function ua(){return!0}function va(){return!1}function wa(){try{return d.activeElement}catch(a){}}function xa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)xa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=va;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(qa,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(K)||[""],j=b.length;while(j--)h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.hasData(a)&&V.get(a);if(q&&(i=q.events)){b=(b||"").match(K)||[""],j=b.length;while(j--)if(h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&V.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(V.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,za=/\s*$/g;function Da(a,b){return r.nodeName(a,"table")&&r.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a:a}function Ea(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Fa(a){var b=Ba.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ga(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(V.hasData(a)&&(f=V.access(a),g=V.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&Aa.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ia(f,b,c,d)});if(m&&(e=pa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(ma(e,"script"),Ea),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=ma(h),f=ma(a),d=0,e=f.length;d0&&na(g,!i&&ma(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(T(c)){if(b=c[V.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[V.expando]=void 0}c[W.expando]&&(c[W.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ja(this,a,!0)},remove:function(a){return Ja(this,a)},text:function(a){return S(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.appendChild(a)}})},prepend:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(ma(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return S(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!za.test(a)&&!la[(ja.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function Ya(a,b,c,d,e){return new Ya.prototype.init(a,b,c,d,e)}r.Tween=Ya,Ya.prototype={constructor:Ya,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=Ya.propHooks[this.prop];return a&&a.get?a.get(this):Ya.propHooks._default.get(this)},run:function(a){var b,c=Ya.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Ya.propHooks._default.set(this),this}},Ya.prototype.init.prototype=Ya.prototype,Ya.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},Ya.propHooks.scrollTop=Ya.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=Ya.prototype.init,r.fx.step={};var Za,$a,_a=/^(?:toggle|show|hide)$/,ab=/queueHooks$/;function bb(){$a&&(a.requestAnimationFrame(bb),r.fx.tick())}function cb(){return a.setTimeout(function(){Za=void 0}),Za=r.now()}function db(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ba[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function eb(a,b,c){for(var d,e=(hb.tweeners[b]||[]).concat(hb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?ib:void 0)), +void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b),null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&r.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(K);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),ib={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=jb[b]||r.find.attr;jb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=jb[g],jb[g]=e,e=null!=c(a,b,d)?g:null,jb[g]=f),e}});var kb=/^(?:input|select|textarea|button)$/i,lb=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return S(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):kb.test(a.nodeName)||lb.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function mb(a){var b=a.match(K)||[];return b.join(" ")}function nb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,nb(this)))});if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,nb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,nb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(K)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=nb(this),b&&V.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":V.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+mb(nb(c))+" ").indexOf(b)>-1)return!0;return!1}});var ob=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":r.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(ob,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:mb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(r.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var pb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!pb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,pb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(V.get(h,"events")||{})[b.type]&&V.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&T(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!T(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=V.access(d,b);e||d.addEventListener(a,c,!0),V.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=V.access(d,b)-1;e?V.access(d,b,e):(d.removeEventListener(a,c,!0),V.remove(d,b))}}});var qb=a.location,rb=r.now(),sb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var tb=/\[\]$/,ub=/\r?\n/g,vb=/^(?:submit|button|image|reset|file)$/i,wb=/^(?:input|select|textarea|keygen)/i;function xb(a,b,c,d){var e;if(r.isArray(b))r.each(b,function(b,e){c||tb.test(a)?d(a,e):xb(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)xb(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(r.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)xb(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&wb.test(this.nodeName)&&!vb.test(a)&&(this.checked||!ia.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:r.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(ub,"\r\n")}}):{name:b.name,value:c.replace(ub,"\r\n")}}).get()}});var yb=/%20/g,zb=/#.*$/,Ab=/([?&])_=[^&]*/,Bb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Cb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Db=/^(?:GET|HEAD)$/,Eb=/^\/\//,Fb={},Gb={},Hb="*/".concat("*"),Ib=d.createElement("a");Ib.href=qb.href;function Jb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(K)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Kb(a,b,c,d){var e={},f=a===Gb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Lb(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Mb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Nb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:qb.href,type:"GET",isLocal:Cb.test(qb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Hb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Lb(Lb(a,r.ajaxSettings),b):Lb(r.ajaxSettings,a)},ajaxPrefilter:Jb(Fb),ajaxTransport:Jb(Gb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Bb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||qb.href)+"").replace(Eb,qb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(K)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Ib.protocol+"//"+Ib.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Kb(Fb,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Db.test(o.type),f=o.url.replace(zb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(yb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(sb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Ab,"$1"),n=(sb.test(f)?"&":"?")+"_="+rb++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Hb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Kb(Gb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Mb(o,y,d)),v=Nb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Ob={0:200,1223:204},Pb=r.ajaxSettings.xhr();o.cors=!!Pb&&"withCredentials"in Pb,o.ajax=Pb=!!Pb,r.ajaxTransport(function(b){var c,d;if(o.cors||Pb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Ob[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r("