From 052c60dc53385828eb0015b165249ddece40c00d Mon Sep 17 00:00:00 2001 From: exceptionfactory Date: Mon, 12 Apr 2021 07:48:20 -0500 Subject: [PATCH] NIFI-8363 Added Single User Login Identity Provider and Authorizer (#4968) * NIFI-8363 Added Single User Login Identity Provider and Authorizer - Reads and writes username and hashed password in login-identity-providers.xml - Generates random username using java.util.UUID.randomUUID() - Generates random password using java.security.SecureRandom with Base64 encoding - Writes generated password hash using bcrypt * NIFI-8363 Updated SingleUserAuthorizer to require SingleUserLoginIdentityProvider * NIFI-8363 Added handling of null login identity provider property --- .../pom.xml | 35 +++ .../src/main/resources/META-INF/LICENSE | 232 ++++++++++++++++++ .../src/main/resources/META-INF/NOTICE | 21 ++ .../nifi-single-user-iaa-providers/pom.xml | 50 ++++ .../user/SingleUserLoginIdentityProvider.java | 168 +++++++++++++ .../user/encoder/BCryptPasswordEncoder.java | 56 +++++ .../single/user/encoder/PasswordEncoder.java | 39 +++ .../user/writer/LoginCredentialsWriter.java | 31 +++ .../StandardLoginCredentialsWriter.java | 210 ++++++++++++++++ .../single/user/SingleUserAuthorizer.java | 144 +++++++++++ ....nifi.authentication.LoginIdentityProvider | 15 ++ .../org.apache.nifi.authorization.Authorizer | 15 ++ .../SingleUserLoginIdentityProviderTest.java | 173 +++++++++++++ .../encoder/BCryptPasswordEncoderTest.java | 45 ++++ .../StandardLoginCredentialsWriterTest.java | 68 +++++ .../single/user/SingleUserAuthorizerTest.java | 77 ++++++ .../conf/login-identity-providers.xml | 24 ++ .../populated-login-identity-providers.xml | 24 ++ .../unsupported-login-identity-providers.xml | 21 ++ .../pom.xml | 38 +++ nifi-nar-bundles/pom.xml | 1 + 21 files changed, 1487 insertions(+) create mode 100644 nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers-nar/pom.xml create mode 100644 nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers-nar/src/main/resources/META-INF/LICENSE create mode 100644 nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers-nar/src/main/resources/META-INF/NOTICE create mode 100644 nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/pom.xml create mode 100644 nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/java/org/apache/nifi/authentication/single/user/SingleUserLoginIdentityProvider.java create mode 100644 nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/java/org/apache/nifi/authentication/single/user/encoder/BCryptPasswordEncoder.java create mode 100644 nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/java/org/apache/nifi/authentication/single/user/encoder/PasswordEncoder.java create mode 100644 nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/java/org/apache/nifi/authentication/single/user/writer/LoginCredentialsWriter.java create mode 100644 nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/java/org/apache/nifi/authentication/single/user/writer/StandardLoginCredentialsWriter.java create mode 100644 nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/java/org/apache/nifi/authorization/single/user/SingleUserAuthorizer.java create mode 100644 nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/resources/META-INF/services/org.apache.nifi.authentication.LoginIdentityProvider create mode 100644 nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/resources/META-INF/services/org.apache.nifi.authorization.Authorizer create mode 100644 nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/test/java/org/apache/nifi/authentication/single/user/SingleUserLoginIdentityProviderTest.java create mode 100644 nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/test/java/org/apache/nifi/authentication/single/user/encoder/BCryptPasswordEncoderTest.java create mode 100644 nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/test/java/org/apache/nifi/authentication/single/user/writer/StandardLoginCredentialsWriterTest.java create mode 100644 nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/test/java/org/apache/nifi/authorization/single/user/SingleUserAuthorizerTest.java create mode 100644 nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/test/resources/conf/login-identity-providers.xml create mode 100644 nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/test/resources/conf/populated-login-identity-providers.xml create mode 100644 nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/test/resources/conf/unsupported-login-identity-providers.xml create mode 100644 nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/pom.xml diff --git a/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers-nar/pom.xml b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers-nar/pom.xml new file mode 100644 index 0000000000..a265d7b59c --- /dev/null +++ b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers-nar/pom.xml @@ -0,0 +1,35 @@ + + + + 4.0.0 + + org.apache.nifi + nifi-single-user-iaa-providers-bundle + 1.14.0-SNAPSHOT + + nifi-single-user-iaa-providers-nar + nar + + true + true + + + + org.apache.nifi + nifi-single-user-iaa-providers + + + \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers-nar/src/main/resources/META-INF/LICENSE b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers-nar/src/main/resources/META-INF/LICENSE new file mode 100644 index 0000000000..3de44face9 --- /dev/null +++ b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers-nar/src/main/resources/META-INF/LICENSE @@ -0,0 +1,232 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (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. + +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. + + 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. (http://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. \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers-nar/src/main/resources/META-INF/NOTICE b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers-nar/src/main/resources/META-INF/NOTICE new file mode 100644 index 0000000000..4e57934f44 --- /dev/null +++ b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers-nar/src/main/resources/META-INF/NOTICE @@ -0,0 +1,21 @@ +nifi-single-user-iaa-providers-nar +Copyright 2015-2020 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +****************** +Apache Software License v2 +****************** + +The following binary components are provided under the Apache Software License v2 + + (ASLv2) BCrypt Password Hashing Function (at.favre.lib:bcrypt:jar:0.9.0 - https://github.com/patrickfav/bcrypt) + The following NOTICE information applies: + BCrypt Password Hashing Function 0.9.0 + Copyright 2018 Patrick Favre-Bulle + + (ASLv2) Bytes Utility Library (at.favre.lib:bytes:jar:1.3.0 - https://github.com/patrickfav/bytes-java) + The following NOTICE information applies: + Bytes Utility Library 1.3.0 + Copyright 2017 Patrick Favre-Bulle diff --git a/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/pom.xml b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/pom.xml new file mode 100644 index 0000000000..7dc81a0a14 --- /dev/null +++ b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/pom.xml @@ -0,0 +1,50 @@ + + + + 4.0.0 + + org.apache.nifi + nifi-single-user-iaa-providers-bundle + 1.14.0-SNAPSHOT + + nifi-single-user-iaa-providers + jar + + + org.apache.nifi + nifi-api + + + org.apache.nifi + nifi-framework-api + + + org.apache.nifi + nifi-utils + 1.14.0-SNAPSHOT + + + org.apache.nifi + nifi-properties + 1.14.0-SNAPSHOT + + + at.favre.lib + bcrypt + 0.9.0 + + + \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/java/org/apache/nifi/authentication/single/user/SingleUserLoginIdentityProvider.java b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/java/org/apache/nifi/authentication/single/user/SingleUserLoginIdentityProvider.java new file mode 100644 index 0000000000..e3570303bb --- /dev/null +++ b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/java/org/apache/nifi/authentication/single/user/SingleUserLoginIdentityProvider.java @@ -0,0 +1,168 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.authentication.single.user; + +import org.apache.nifi.authentication.AuthenticationResponse; +import org.apache.nifi.authentication.LoginCredentials; +import org.apache.nifi.authentication.LoginIdentityProvider; +import org.apache.nifi.authentication.LoginIdentityProviderConfigurationContext; +import org.apache.nifi.authentication.LoginIdentityProviderInitializationContext; +import org.apache.nifi.authentication.annotation.LoginIdentityProviderContext; +import org.apache.nifi.authentication.exception.InvalidLoginCredentialsException; +import org.apache.nifi.authentication.exception.ProviderCreationException; +import org.apache.nifi.authentication.single.user.encoder.BCryptPasswordEncoder; +import org.apache.nifi.authentication.single.user.encoder.PasswordEncoder; +import org.apache.nifi.authentication.single.user.writer.LoginCredentialsWriter; +import org.apache.nifi.authentication.single.user.writer.StandardLoginCredentialsWriter; +import org.apache.nifi.util.NiFiProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.security.SecureRandom; +import java.util.Base64; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * Single User Login Identity Provider using bcrypt password encoder + */ +public class SingleUserLoginIdentityProvider implements LoginIdentityProvider { + protected static final String USERNAME_PROPERTY = "Username"; + + protected static final String PASSWORD_PROPERTY = "Password"; + + private static final Logger LOGGER = LoggerFactory.getLogger(SingleUserLoginIdentityProvider.class); + + private static final Base64.Encoder RANDOM_BYTE_ENCODER = Base64.getEncoder().withoutPadding(); + + private static final int RANDOM_BYTE_LENGTH = 24; + + private static final long EXPIRATION = TimeUnit.HOURS.toMillis(1); + + protected PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + + private File loginIdentityProviderConfigurationFile; + + private LoginCredentials configuredCredentials; + + /** + * Set NiFi Properties using method injection + * + * @param niFiProperties NiFi Properties + */ + @LoginIdentityProviderContext + public void setProperties(final NiFiProperties niFiProperties) { + loginIdentityProviderConfigurationFile = niFiProperties.getLoginIdentityProviderConfigurationFile(); + } + + /** + * Authenticate using Credentials comparing password and then username for verification + * + * @param credentials Login Credentials + * @return Authentication Response + * @throws InvalidLoginCredentialsException Thrown on unverified username or password + */ + @Override + public AuthenticationResponse authenticate(final LoginCredentials credentials) throws InvalidLoginCredentialsException { + final String password = credentials.getPassword(); + if (isPasswordVerified(password)) { + final String username = credentials.getUsername(); + if (isUsernameVerified(username)) { + return new AuthenticationResponse(username, username, EXPIRATION, getClass().getSimpleName()); + } else { + throw new InvalidLoginCredentialsException("Username verification failed"); + } + } else { + throw new InvalidLoginCredentialsException("Password verification failed"); + } + } + + /** + * Initialize Provider + * + * @param initializationContext Initialization Context + */ + @Override + public void initialize(final LoginIdentityProviderInitializationContext initializationContext) { + LOGGER.debug("Initializing Provider"); + } + + /** + * Configure Provider using Username and Password properties with support for generating random values + * + * @param configurationContext Configuration Context containing properties + * @throws ProviderCreationException Thrown when unable to write Login Credentials + */ + @Override + public void onConfigured(final LoginIdentityProviderConfigurationContext configurationContext) throws ProviderCreationException { + LOGGER.debug("Configuring Provider"); + final String password = configurationContext.getProperty(PASSWORD_PROPERTY); + if (password == null || password.length() == 0) { + try { + configuredCredentials = generateLoginCredentials(); + LOGGER.info("Updating Login Identity Providers Configuration [{}]", loginIdentityProviderConfigurationFile); + final LoginCredentialsWriter loginCredentialsWriter = getLoginCredentialsWriter(); + loginCredentialsWriter.writeLoginCredentials(configuredCredentials); + } catch (final IOException e) { + throw new ProviderCreationException("Generating Login Credentials Failed", e); + } + } else { + final String username = configurationContext.getProperty(USERNAME_PROPERTY); + configuredCredentials = new LoginCredentials(username, password); + } + } + + /** + * Destroy Provider + */ + @Override + public void preDestruction() { + LOGGER.debug("Destroying Provider"); + } + + protected String generatePassword() { + final SecureRandom secureRandom = new SecureRandom(); + final byte[] bytes = new byte[RANDOM_BYTE_LENGTH]; + secureRandom.nextBytes(bytes); + return RANDOM_BYTE_ENCODER.encodeToString(bytes); + } + + private LoginCredentialsWriter getLoginCredentialsWriter() { + return new StandardLoginCredentialsWriter(loginIdentityProviderConfigurationFile); + } + + private LoginCredentials generateLoginCredentials() throws IOException { + final String username = UUID.randomUUID().toString(); + final String password = generatePassword(); + + final String separator = System.lineSeparator(); + LOGGER.info("{}{}Generated Username [{}]{}Generated Password [{}]{}", separator, separator, username, separator, password, separator); + + final String hashedPassword = passwordEncoder.encode(password.toCharArray()); + return new LoginCredentials(username, hashedPassword); + } + + private boolean isPasswordVerified(final String password) { + return passwordEncoder.matches(password.toCharArray(), configuredCredentials.getPassword()); + } + + private boolean isUsernameVerified(final String username) { + return configuredCredentials.getUsername().equals(username); + } +} diff --git a/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/java/org/apache/nifi/authentication/single/user/encoder/BCryptPasswordEncoder.java b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/java/org/apache/nifi/authentication/single/user/encoder/BCryptPasswordEncoder.java new file mode 100644 index 0000000000..58e303f636 --- /dev/null +++ b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/java/org/apache/nifi/authentication/single/user/encoder/BCryptPasswordEncoder.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.authentication.single.user.encoder; + +import at.favre.lib.crypto.bcrypt.BCrypt; + +/** + * Password Encoder implementation using bcrypt hashing + */ +public class BCryptPasswordEncoder implements PasswordEncoder { + private static final int DEFAULT_COST = 12; + + private static final BCrypt.Version BCRYPT_VERSION = BCrypt.Version.VERSION_2B; + + private static final BCrypt.Hasher HASHER = BCrypt.with(BCRYPT_VERSION); + + private static final BCrypt.Verifyer VERIFYER = BCrypt.verifyer(BCRYPT_VERSION); + + /** + * Encode Password and return bcrypt hashed password + * + * @param password Password + * @return bcrypt hashed password + */ + @Override + public String encode(final char[] password) { + return HASHER.hashToString(DEFAULT_COST, password); + } + + /** + * Match Password against bcrypt hashed password + * + * @param password Password to be matched + * @param encodedPassword bcrypt hashed password + * @return Matched status + */ + @Override + public boolean matches(final char[] password, final String encodedPassword) { + final BCrypt.Result result = VERIFYER.verifyStrict(password, encodedPassword.toCharArray()); + return result.verified; + } +} diff --git a/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/java/org/apache/nifi/authentication/single/user/encoder/PasswordEncoder.java b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/java/org/apache/nifi/authentication/single/user/encoder/PasswordEncoder.java new file mode 100644 index 0000000000..935914d816 --- /dev/null +++ b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/java/org/apache/nifi/authentication/single/user/encoder/PasswordEncoder.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.authentication.single.user.encoder; + +/** + * Password Encoder for encoding and matching passwords modeled after Spring Security PasswordEncoder + */ +public interface PasswordEncoder { + /** + * Encode Password and return hashed password + * + * @param password Password + * @return Encoded Password + */ + String encode(char[] password); + + /** + * Match Password against encoded password + * + * @param password Password to be matched + * @param encodedPassword Encoded representation of password for matching + * @return Matched status + */ + boolean matches(char[] password, String encodedPassword); +} diff --git a/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/java/org/apache/nifi/authentication/single/user/writer/LoginCredentialsWriter.java b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/java/org/apache/nifi/authentication/single/user/writer/LoginCredentialsWriter.java new file mode 100644 index 0000000000..e26405b9a3 --- /dev/null +++ b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/java/org/apache/nifi/authentication/single/user/writer/LoginCredentialsWriter.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.authentication.single.user.writer; + +import org.apache.nifi.authentication.LoginCredentials; + +/** + * Writer for Login Identity Providers Configuration + */ +public interface LoginCredentialsWriter { + /** + * Write Login Credentials + * + * @param loginCredentials Login Credentials + */ + void writeLoginCredentials(LoginCredentials loginCredentials); +} diff --git a/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/java/org/apache/nifi/authentication/single/user/writer/StandardLoginCredentialsWriter.java b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/java/org/apache/nifi/authentication/single/user/writer/StandardLoginCredentialsWriter.java new file mode 100644 index 0000000000..693893ee09 --- /dev/null +++ b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/java/org/apache/nifi/authentication/single/user/writer/StandardLoginCredentialsWriter.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.authentication.single.user.writer; + +import org.apache.nifi.authentication.LoginCredentials; +import org.apache.nifi.authentication.single.user.SingleUserLoginIdentityProvider; + +import javax.xml.stream.XMLEventFactory; +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLEventWriter; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.events.Attribute; +import javax.xml.stream.events.Characters; +import javax.xml.stream.events.EndElement; +import javax.xml.stream.events.StartElement; +import javax.xml.stream.events.XMLEvent; +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.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.Iterator; + +/** + * Standard Login Credentials Writer updates Login Identity Providers Single User definition with Login Credentials + */ +public class StandardLoginCredentialsWriter implements LoginCredentialsWriter { + + private static final String PROVIDERS_PREFIX = "login-identity-providers-"; + + private static final String PROVIDERS_SUFFIX = ".xml"; + + private static final String CLASS_TAG = "class"; + + private static final String PROVIDER_TAG = "provider"; + + private static final String PROPERTY_TAG = "property"; + + private static final String NAME_ATTRIBUTE = "name"; + + private static final String USERNAME_PROPERTY = "Username"; + + private static final String PASSWORD_PROPERTY = "Password"; + + private final XMLEventFactory eventFactory = XMLEventFactory.newFactory(); + + private final File providersFile; + + public StandardLoginCredentialsWriter(final File providersFile) { + this.providersFile = providersFile; + } + + @Override + public void writeLoginCredentials(final LoginCredentials loginCredentials) { + try { + final File updatedProvidersFile = File.createTempFile(PROVIDERS_PREFIX, PROVIDERS_SUFFIX); + writeLoginCredentials(loginCredentials, updatedProvidersFile); + Files.move(updatedProvidersFile.toPath(), providersFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (final IOException e) { + throw new UncheckedIOException("Writing Login Identity Providers Failed", e); + } catch (final XMLStreamException e) { + throw new RuntimeException("Processing Login Identity Providers Failed", e); + } + } + + private void writeLoginCredentials(final LoginCredentials loginCredentials, final File updatedProvidersFile) throws IOException, XMLStreamException { + try (final OutputStream outputStream = new FileOutputStream(updatedProvidersFile)) { + final XMLEventWriter providersWriter = getProvidersWriter(outputStream); + try (final InputStream inputStream = new FileInputStream(providersFile)) { + final XMLEventReader providersReader = getProvidersReader(inputStream); + updateLoginIdentityProviders(loginCredentials, providersReader, providersWriter); + providersReader.close(); + } + providersWriter.close(); + } + } + + private void updateLoginIdentityProviders(final LoginCredentials credentials, + final XMLEventReader providersReader, + final XMLEventWriter providersWriter) throws XMLStreamException { + boolean processingSingleUserProvider = false; + + while (providersReader.hasNext()) { + final XMLEvent event = providersReader.nextEvent(); + providersWriter.add(event); + + if (isStartClass(event)) { + final XMLEvent nextEvent = providersReader.nextEvent(); + providersWriter.add(nextEvent); + if (nextEvent.isCharacters()) { + final String providerClass = nextEvent.asCharacters().getData(); + if (SingleUserLoginIdentityProvider.class.getName().equals(providerClass)) { + processingSingleUserProvider = true; + } + } + } else if (isEndProvider(event)) { + processingSingleUserProvider = false; + } + + if (processingSingleUserProvider) { + if (isStartProperty(event, USERNAME_PROPERTY)) { + processProperty(providersReader, providersWriter, credentials.getUsername()); + } else if (isStartProperty(event, PASSWORD_PROPERTY)) { + processProperty(providersReader, providersWriter, credentials.getPassword()); + } + } + } + } + + /** + * Process Property Value and replace existing Characters when found + * + * @param providersReader Providers Reader + * @param providersWriter Providers Writer + * @param propertyValue Property Value to be added + * @throws XMLStreamException Thrown on XMLEventReader.nextEvent() + */ + private void processProperty(final XMLEventReader providersReader, final XMLEventWriter providersWriter, final String propertyValue) throws XMLStreamException { + final XMLEvent nextEvent = providersReader.nextEvent(); + + final Characters propertyCharacters = eventFactory.createCharacters(propertyValue); + providersWriter.add(propertyCharacters); + + if (nextEvent.isEndElement()) { + providersWriter.add(nextEvent); + } + } + + private boolean isStartClass(final XMLEvent event) { + boolean found = false; + + if (event.isStartElement()) { + final StartElement startElement = event.asStartElement(); + found = CLASS_TAG.equals(startElement.getName().getLocalPart()); + } + + return found; + } + + private boolean isStartProperty(final XMLEvent event, final String propertyName) { + boolean found = false; + + if (event.isStartElement()) { + final StartElement startElement = event.asStartElement(); + found = PROPERTY_TAG.equals(startElement.getName().getLocalPart()) && isProperty(startElement, propertyName); + } + + return found; + } + + private boolean isProperty(final StartElement startElement, final String propertyName) { + boolean found = false; + + final Iterator attributes = startElement.getAttributes(); + while (attributes.hasNext()) { + final Attribute attribute = attributes.next(); + if (NAME_ATTRIBUTE.equals(attribute.getName().getLocalPart())) { + if (propertyName.equals(attribute.getValue())) { + found = true; + break; + } + } + } + + return found; + } + + private boolean isEndProvider(final XMLEvent event) { + boolean found = false; + + if (event.isEndElement()) { + final EndElement endElement = event.asEndElement(); + found = PROVIDER_TAG.equals(endElement.getName().getLocalPart()); + } + + return found; + } + + private XMLEventWriter getProvidersWriter(final OutputStream outputStream) throws XMLStreamException { + final XMLOutputFactory outputFactory = XMLOutputFactory.newInstance(); + return outputFactory.createXMLEventWriter(outputStream); + } + + private XMLEventReader getProvidersReader(final InputStream inputStream) throws XMLStreamException { + final XMLInputFactory inputFactory = XMLInputFactory.newFactory(); + inputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false); + inputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false); + return inputFactory.createXMLEventReader(inputStream); + } +} diff --git a/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/java/org/apache/nifi/authorization/single/user/SingleUserAuthorizer.java b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/java/org/apache/nifi/authorization/single/user/SingleUserAuthorizer.java new file mode 100644 index 0000000000..1f78a41b0d --- /dev/null +++ b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/java/org/apache/nifi/authorization/single/user/SingleUserAuthorizer.java @@ -0,0 +1,144 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.nifi.authorization.single.user; + +import org.apache.nifi.authentication.single.user.SingleUserLoginIdentityProvider; +import org.apache.nifi.authorization.AuthorizationRequest; +import org.apache.nifi.authorization.AuthorizationResult; +import org.apache.nifi.authorization.Authorizer; +import org.apache.nifi.authorization.AuthorizerConfigurationContext; +import org.apache.nifi.authorization.AuthorizerInitializationContext; +import org.apache.nifi.authorization.annotation.AuthorizerContext; +import org.apache.nifi.authorization.exception.AuthorizationAccessException; +import org.apache.nifi.authorization.exception.AuthorizerCreationException; +import org.apache.nifi.util.NiFiProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.events.StartElement; +import javax.xml.stream.events.XMLEvent; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Single User Authorizer requires Single User Login Identity Provider to be configured and authorizes all requests + */ +public class SingleUserAuthorizer implements Authorizer { + private static final Logger LOGGER = LoggerFactory.getLogger(SingleUserAuthorizer.class); + + private static final String REQUIRED_PROVIDER = SingleUserLoginIdentityProvider.class.getName(); + + private static final String IDENTIFIER_TAG = "identifier"; + + private static final String CLASS_TAG = "class"; + + private static final String BLANK_PROVIDER = "provider"; + + /** + * Set NiFi Properties using method injection + * + * @param niFiProperties NiFi Properties + */ + @AuthorizerContext + public void setProperties(final NiFiProperties niFiProperties) { + final File configuration = niFiProperties.getLoginIdentityProviderConfigurationFile(); + final String identifier = niFiProperties.getProperty(NiFiProperties.SECURITY_USER_LOGIN_IDENTITY_PROVIDER, BLANK_PROVIDER); + if (isSingleUserLoginIdentityProviderConfigured(identifier, configuration)) { + LOGGER.debug("Required Login Identity Provider Configured [{}]", REQUIRED_PROVIDER); + } else { + final String message = String.format("%s requires %s to be configured", getClass().getSimpleName(), REQUIRED_PROVIDER); + throw new AuthorizerCreationException(message); + } + } + + @Override + public AuthorizationResult authorize(final AuthorizationRequest request) throws AuthorizationAccessException { + return AuthorizationResult.approved(); + } + + @Override + public void initialize(final AuthorizerInitializationContext initializationContext) { + LOGGER.info("Initializing Authorizer"); + } + + @Override + public void onConfigured(final AuthorizerConfigurationContext configurationContext) { + LOGGER.info("Configuring Authorizer"); + } + + @Override + public void preDestruction() { + LOGGER.info("Destroying Authorizer"); + } + + private boolean isSingleUserLoginIdentityProviderConfigured(final String configuredIdentifier, final File configuration) { + try (final InputStream inputStream = new FileInputStream(configuration)) { + final XMLEventReader reader = getProvidersReader(inputStream); + final boolean configured = isSingleUserLoginIdentityProviderConfigured(configuredIdentifier, reader); + reader.close(); + return configured; + } catch (final XMLStreamException | IOException e) { + throw new AuthorizerCreationException("Failed to read Login Identity Providers Configuration", e); + } + } + + private boolean isSingleUserLoginIdentityProviderConfigured(final String configuredIdentifier, final XMLEventReader reader) throws XMLStreamException { + boolean providerConfigured = false; + + boolean identifierFound = false; + while (reader.hasNext()) { + final XMLEvent event = reader.nextEvent(); + if (isStartElement(event, IDENTIFIER_TAG)) { + final String providerIdentifier = reader.getElementText().trim(); + identifierFound = configuredIdentifier.equals(providerIdentifier); + } + + if (identifierFound) { + // Compare class after finding configured provider identifier + if (isStartElement(event, CLASS_TAG)) { + final String providerClass = reader.getElementText().trim(); + providerConfigured = REQUIRED_PROVIDER.equals(providerClass); + } + } + } + + return providerConfigured; + } + + private boolean isStartElement(final XMLEvent event, final String localElementName) { + boolean found = false; + + if (event.isStartElement()) { + final StartElement startElement = event.asStartElement(); + found = localElementName.equals(startElement.getName().getLocalPart()); + } + + return found; + } + + private XMLEventReader getProvidersReader(final InputStream inputStream) throws XMLStreamException { + final XMLInputFactory inputFactory = XMLInputFactory.newFactory(); + inputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false); + inputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false); + return inputFactory.createXMLEventReader(inputStream); + } +} diff --git a/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/resources/META-INF/services/org.apache.nifi.authentication.LoginIdentityProvider b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/resources/META-INF/services/org.apache.nifi.authentication.LoginIdentityProvider new file mode 100644 index 0000000000..813f239dbe --- /dev/null +++ b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/resources/META-INF/services/org.apache.nifi.authentication.LoginIdentityProvider @@ -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.authentication.single.user.SingleUserLoginIdentityProvider \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/resources/META-INF/services/org.apache.nifi.authorization.Authorizer b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/resources/META-INF/services/org.apache.nifi.authorization.Authorizer new file mode 100644 index 0000000000..73ec1d6e09 --- /dev/null +++ b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/main/resources/META-INF/services/org.apache.nifi.authorization.Authorizer @@ -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.authorization.single.user.SingleUserAuthorizer diff --git a/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/test/java/org/apache/nifi/authentication/single/user/SingleUserLoginIdentityProviderTest.java b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/test/java/org/apache/nifi/authentication/single/user/SingleUserLoginIdentityProviderTest.java new file mode 100644 index 0000000000..21f7b0fc9c --- /dev/null +++ b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/test/java/org/apache/nifi/authentication/single/user/SingleUserLoginIdentityProviderTest.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.authentication.single.user; + +import org.apache.nifi.authentication.AuthenticationResponse; +import org.apache.nifi.authentication.LoginCredentials; +import org.apache.nifi.authentication.LoginIdentityProviderConfigurationContext; +import org.apache.nifi.authentication.exception.InvalidLoginCredentialsException; +import org.apache.nifi.authentication.single.user.encoder.PasswordEncoder; +import org.apache.nifi.util.NiFiProperties; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.eq; + +@RunWith(MockitoJUnitRunner.class) +public class SingleUserLoginIdentityProviderTest { + private static final String BLANK_PROVIDERS = "/conf/login-identity-providers.xml"; + + private static final String XML_SUFFIX = ".xml"; + + private static final String EMPTY_PROPERTIES_PATH = ""; + + private static final Pattern USERNAME_PATTERN = Pattern.compile("Username\">([^<]+)<"); + + private static final Pattern PASSWORD_PATTERN = Pattern.compile("Password\">([^<]+)<"); + + private static final int FIRST_GROUP = 1; + + @Mock + private LoginIdentityProviderConfigurationContext configurationContext; + + private StringPasswordEncoder encoder; + + private SingleUserLoginIdentityProvider provider; + + @Before + public void setProvider() { + provider = new SingleUserLoginIdentityProvider(); + encoder = new StringPasswordEncoder(); + provider.passwordEncoder = encoder; + } + + @Test + public void testOnConfiguredGeneratePasswordAuthenticateSuccess() throws IOException, URISyntaxException { + final Path configuredProvidersPath = Files.createTempFile(getClass().getSimpleName(), XML_SUFFIX); + configuredProvidersPath.toFile().deleteOnExit(); + + final Path blankProvidersPath = Paths.get(getClass().getResource(BLANK_PROVIDERS).toURI()); + Files.copy(blankProvidersPath, configuredProvidersPath, StandardCopyOption.REPLACE_EXISTING); + + final Properties properties = new Properties(); + properties.put(NiFiProperties.LOGIN_IDENTITY_PROVIDER_CONFIGURATION_FILE, configuredProvidersPath.toString()); + + final NiFiProperties niFiProperties = NiFiProperties.createBasicNiFiProperties(EMPTY_PROPERTIES_PATH, properties); + provider.setProperties(niFiProperties); + provider.onConfigured(configurationContext); + + final String providersConfiguration = new String(Files.readAllBytes(configuredProvidersPath)); + + final Matcher usernameMatcher = USERNAME_PATTERN.matcher(providersConfiguration); + assertTrue("Username not found", usernameMatcher.find()); + final String username = usernameMatcher.group(FIRST_GROUP); + + final Matcher passwordMatcher = PASSWORD_PATTERN.matcher(providersConfiguration); + assertTrue("Password not found", passwordMatcher.find()); + + final LoginCredentials loginCredentials = new LoginCredentials(username, encoder.encoded); + final AuthenticationResponse response = provider.authenticate(loginCredentials); + assertEquals(username, response.getUsername()); + } + + @Test + public void testOnConfiguredAuthenticateInvalidPassword() throws URISyntaxException { + final Path blankProvidersPath = Paths.get(getClass().getResource(BLANK_PROVIDERS).toURI()); + final Properties properties = new Properties(); + properties.put(NiFiProperties.LOGIN_IDENTITY_PROVIDER_CONFIGURATION_FILE, blankProvidersPath.toString()); + final NiFiProperties niFiProperties = NiFiProperties.createBasicNiFiProperties(EMPTY_PROPERTIES_PATH, properties); + provider.setProperties(niFiProperties); + + final String username = String.class.getSimpleName(); + when(configurationContext.getProperty(eq(SingleUserLoginIdentityProvider.USERNAME_PROPERTY))).thenReturn(username); + when(configurationContext.getProperty(eq(SingleUserLoginIdentityProvider.PASSWORD_PROPERTY))).thenReturn(String.class.getName()); + provider.onConfigured(configurationContext); + + final LoginCredentials loginCredentials = new LoginCredentials(username, LoginCredentials.class.getName()); + assertThrows(InvalidLoginCredentialsException.class, () -> provider.authenticate(loginCredentials)); + } + + @Test + public void testOnConfiguredAuthenticateInvalidUsername() throws URISyntaxException { + final Path blankProvidersPath = Paths.get(getClass().getResource(BLANK_PROVIDERS).toURI()); + final Properties properties = new Properties(); + properties.put(NiFiProperties.LOGIN_IDENTITY_PROVIDER_CONFIGURATION_FILE, blankProvidersPath.toString()); + final NiFiProperties niFiProperties = NiFiProperties.createBasicNiFiProperties(EMPTY_PROPERTIES_PATH, properties); + provider.setProperties(niFiProperties); + + final String username = String.class.getSimpleName(); + final String password = String.class.getName(); + when(configurationContext.getProperty(eq(SingleUserLoginIdentityProvider.USERNAME_PROPERTY))).thenReturn(username); + when(configurationContext.getProperty(eq(SingleUserLoginIdentityProvider.PASSWORD_PROPERTY))).thenReturn(password); + provider.onConfigured(configurationContext); + + final LoginCredentials loginCredentials = new LoginCredentials(LoginCredentials.class.getName(), password); + assertThrows(InvalidLoginCredentialsException.class, () -> provider.authenticate(loginCredentials)); + + } + + @Test + public void testOnConfiguredAuthenticateSuccess() throws URISyntaxException { + final Path blankProvidersPath = Paths.get(getClass().getResource(BLANK_PROVIDERS).toURI()); + final Properties properties = new Properties(); + properties.put(NiFiProperties.LOGIN_IDENTITY_PROVIDER_CONFIGURATION_FILE, blankProvidersPath.toString()); + final NiFiProperties niFiProperties = NiFiProperties.createBasicNiFiProperties(EMPTY_PROPERTIES_PATH, properties); + provider.setProperties(niFiProperties); + + final String username = String.class.getSimpleName(); + final String password = String.class.getName(); + when(configurationContext.getProperty(eq(SingleUserLoginIdentityProvider.USERNAME_PROPERTY))).thenReturn(username); + when(configurationContext.getProperty(eq(SingleUserLoginIdentityProvider.PASSWORD_PROPERTY))).thenReturn(password); + provider.onConfigured(configurationContext); + + final LoginCredentials loginCredentials = new LoginCredentials(username, password); + final AuthenticationResponse response = provider.authenticate(loginCredentials); + assertEquals(username, response.getUsername()); + } + + private static class StringPasswordEncoder implements PasswordEncoder { + private String encoded; + + @Override + public String encode(final char[] password) { + encoded = new String(password); + return encoded; + } + + @Override + public boolean matches(final char[] password, final String encodedPassword) { + return encodedPassword.equals(new String(password)); + } + } +} diff --git a/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/test/java/org/apache/nifi/authentication/single/user/encoder/BCryptPasswordEncoderTest.java b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/test/java/org/apache/nifi/authentication/single/user/encoder/BCryptPasswordEncoderTest.java new file mode 100644 index 0000000000..36b12c89aa --- /dev/null +++ b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/test/java/org/apache/nifi/authentication/single/user/encoder/BCryptPasswordEncoderTest.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.authentication.single.user.encoder; + +import org.junit.Test; + +import java.util.regex.Pattern; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class BCryptPasswordEncoderTest { + + private static final Pattern BCRYPT_PATTERN = Pattern.compile("^\\$2b\\$12\\$.+$"); + + @Test + public void testEncode() { + final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); + final String encoded = encoder.encode(String.class.getSimpleName().toCharArray()); + assertNotNull("Encoded Password not found", encoded); + assertTrue("Encoded Password bcrypt hash not found", BCRYPT_PATTERN.matcher(encoded).matches()); + } + + @Test + public void testEncodeMatches() { + final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); + final char[] password = String.class.getSimpleName().toCharArray(); + final String encoded = encoder.encode(password); + assertTrue("Encoded Password not matched", encoder.matches(password, encoded)); + } +} diff --git a/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/test/java/org/apache/nifi/authentication/single/user/writer/StandardLoginCredentialsWriterTest.java b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/test/java/org/apache/nifi/authentication/single/user/writer/StandardLoginCredentialsWriterTest.java new file mode 100644 index 0000000000..6659bf241f --- /dev/null +++ b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/test/java/org/apache/nifi/authentication/single/user/writer/StandardLoginCredentialsWriterTest.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.authentication.single.user.writer; + +import org.apache.nifi.authentication.LoginCredentials; +import org.junit.Test; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.UUID; + +import static org.junit.Assert.assertTrue; + +public class StandardLoginCredentialsWriterTest { + private static final String BLANK_PROVIDERS = "/conf/login-identity-providers.xml"; + + private static final String POPULATED_PROVIDERS = "/conf/populated-login-identity-providers.xml"; + + private static final String XML_SUFFIX = ".xml"; + + @Test + public void testWriteLoginCredentialsBlankProviders() throws IOException, URISyntaxException { + final Path sourceProvidersPath = Paths.get(getClass().getResource(BLANK_PROVIDERS).toURI()); + assertCredentialsFound(sourceProvidersPath); + } + + @Test + public void testWriteLoginCredentialsPopulatedProviders() throws IOException, URISyntaxException { + final Path sourceProvidersPath = Paths.get(getClass().getResource(POPULATED_PROVIDERS).toURI()); + assertCredentialsFound(sourceProvidersPath); + } + + private void assertCredentialsFound(final Path sourceProvidersPath) throws IOException { + final Path configuredProvidersPath = Files.createTempFile(getClass().getSimpleName(), XML_SUFFIX); + configuredProvidersPath.toFile().deleteOnExit(); + + Files.copy(sourceProvidersPath, configuredProvidersPath, StandardCopyOption.REPLACE_EXISTING); + + final StandardLoginCredentialsWriter writer = new StandardLoginCredentialsWriter(configuredProvidersPath.toFile()); + + final String username = UUID.randomUUID().toString(); + final String password = UUID.randomUUID().toString(); + final LoginCredentials credentials = new LoginCredentials(username, password); + writer.writeLoginCredentials(credentials); + + final String configuration = new String(Files.readAllBytes(configuredProvidersPath)); + assertTrue("Username not found", configuration.contains(username)); + assertTrue("Password not found", configuration.contains(password)); + } +} diff --git a/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/test/java/org/apache/nifi/authorization/single/user/SingleUserAuthorizerTest.java b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/test/java/org/apache/nifi/authorization/single/user/SingleUserAuthorizerTest.java new file mode 100644 index 0000000000..db0fbb818b --- /dev/null +++ b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/test/java/org/apache/nifi/authorization/single/user/SingleUserAuthorizerTest.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.authorization.single.user; + +import org.apache.nifi.authorization.exception.AuthorizerCreationException; +import org.apache.nifi.util.NiFiProperties; +import org.junit.Before; +import org.junit.Test; + +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Properties; + +import static org.junit.Assert.assertThrows; + +public class SingleUserAuthorizerTest { + private static final String BLANK_PROVIDERS = "/conf/login-identity-providers.xml"; + + private static final String UNSUPPORTED_PROVIDERS = "/conf/unsupported-login-identity-providers.xml"; + + private static final String PROVIDER_IDENTIFIER = "single-user-provider"; + + private static final String UNSUPPORTED_PROVIDER_IDENTIFIER = "unsupported-provider"; + + private static final String EMPTY_PROPERTIES_PATH = ""; + + private SingleUserAuthorizer authorizer; + + @Before + public void setAuthorizer() { + authorizer = new SingleUserAuthorizer(); + } + + @Test + public void testSetPropertiesSingleUserIdentityProviderConfigured() throws URISyntaxException { + final Path providersPath = Paths.get(getClass().getResource(BLANK_PROVIDERS).toURI()); + final Properties properties = new Properties(); + properties.put(NiFiProperties.LOGIN_IDENTITY_PROVIDER_CONFIGURATION_FILE, providersPath.toString()); + properties.put(NiFiProperties.SECURITY_USER_LOGIN_IDENTITY_PROVIDER, PROVIDER_IDENTIFIER); + final NiFiProperties niFiProperties = NiFiProperties.createBasicNiFiProperties(EMPTY_PROPERTIES_PATH, properties); + authorizer.setProperties(niFiProperties); + } + + @Test + public void testSetPropertiesSingleUserIdentityProviderNotSpecified() throws URISyntaxException { + final Path providersPath = Paths.get(getClass().getResource(BLANK_PROVIDERS).toURI()); + final Properties properties = new Properties(); + properties.put(NiFiProperties.LOGIN_IDENTITY_PROVIDER_CONFIGURATION_FILE, providersPath.toString()); + final NiFiProperties niFiProperties = NiFiProperties.createBasicNiFiProperties(EMPTY_PROPERTIES_PATH, properties); + assertThrows(AuthorizerCreationException.class, () -> authorizer.setProperties(niFiProperties)); + } + + @Test + public void testSetPropertiesAuthorizerCreationException() throws URISyntaxException { + final Path providersPath = Paths.get(getClass().getResource(UNSUPPORTED_PROVIDERS).toURI()); + final Properties properties = new Properties(); + properties.put(NiFiProperties.LOGIN_IDENTITY_PROVIDER_CONFIGURATION_FILE, providersPath.toString()); + properties.put(NiFiProperties.SECURITY_USER_LOGIN_IDENTITY_PROVIDER, UNSUPPORTED_PROVIDER_IDENTIFIER); + final NiFiProperties niFiProperties = NiFiProperties.createBasicNiFiProperties(EMPTY_PROPERTIES_PATH, properties); + assertThrows(AuthorizerCreationException.class, () -> authorizer.setProperties(niFiProperties)); + } +} diff --git a/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/test/resources/conf/login-identity-providers.xml b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/test/resources/conf/login-identity-providers.xml new file mode 100644 index 0000000000..ee52d1c6f2 --- /dev/null +++ b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/test/resources/conf/login-identity-providers.xml @@ -0,0 +1,24 @@ + + + + + + single-user-provider + org.apache.nifi.authentication.single.user.SingleUserLoginIdentityProvider + + + + \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/test/resources/conf/populated-login-identity-providers.xml b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/test/resources/conf/populated-login-identity-providers.xml new file mode 100644 index 0000000000..bc21ed5ac0 --- /dev/null +++ b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/test/resources/conf/populated-login-identity-providers.xml @@ -0,0 +1,24 @@ + + + + + + single-user-provider + org.apache.nifi.authentication.single.user.SingleUserLoginIdentityProvider + USERNAME + PASSWORD + + \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/test/resources/conf/unsupported-login-identity-providers.xml b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/test/resources/conf/unsupported-login-identity-providers.xml new file mode 100644 index 0000000000..0ec30e558b --- /dev/null +++ b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/nifi-single-user-iaa-providers/src/test/resources/conf/unsupported-login-identity-providers.xml @@ -0,0 +1,21 @@ + + + + + unsupported-provider + org.apache.nifi.authentication.LoginIdentityProvider + + \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/pom.xml b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/pom.xml new file mode 100644 index 0000000000..224fa2d328 --- /dev/null +++ b/nifi-nar-bundles/nifi-single-user-iaa-providers-bundle/pom.xml @@ -0,0 +1,38 @@ + + + + 4.0.0 + + org.apache.nifi + nifi-nar-bundles + 1.14.0-SNAPSHOT + + nifi-single-user-iaa-providers-bundle + pom + + nifi-single-user-iaa-providers + nifi-single-user-iaa-providers-nar + + + + + org.apache.nifi + nifi-single-user-iaa-providers + 1.14.0-SNAPSHOT + + + + \ No newline at end of file diff --git a/nifi-nar-bundles/pom.xml b/nifi-nar-bundles/pom.xml index 6a9ea07ad4..67cce43190 100755 --- a/nifi-nar-bundles/pom.xml +++ b/nifi-nar-bundles/pom.xml @@ -53,6 +53,7 @@ nifi-azure-bundle nifi-ldap-iaa-providers-bundle nifi-kerberos-iaa-providers-bundle + nifi-single-user-iaa-providers-bundle nifi-riemann-bundle nifi-html-bundle nifi-scripting-bundle