[ https://issues.apache.org/jira/browse/NIFI-1614?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=15191395#comment-15191395 ]
ASF GitHub Bot commented on NIFI-1614: -------------------------------------- Github user jvwing commented on a diff in the pull request: https://github.com/apache/nifi/pull/267#discussion_r55872279 --- Diff: nifi-nar-bundles/nifi-iaa-providers-bundle/nifi-file-identity-provider/src/main/java/org/apache/nifi/authentication/file/FileIdentityProvider.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.authentication.file; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +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.bind.ValidationEvent; +import javax.xml.bind.ValidationEventHandler; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; + +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.exception.IdentityAccessException; +import org.apache.nifi.authentication.exception.InvalidLoginCredentialsException; +import org.apache.nifi.authorization.exception.ProviderCreationException; +import org.apache.nifi.authorization.exception.ProviderDestructionException; +import org.apache.nifi.authentication.file.generated.UserCredentials; +import org.apache.nifi.authentication.file.generated.UserCredentialsList; +import org.apache.nifi.util.FormatUtils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + + +/** + * Identity provider for simple username/password authentication backed by a local credentials file. The credentials + * file contains usernames and password hashes in bcrypt format. Any compatible bcrypt "2a" implementation may be used + * to populate the credentials file. + * <p> + * The XML format of the credentials file is as follows: + * <pre> + * {@code + * <?xml version='1.0' encoding='utf-8'?> + * <credentials> + * <user name="user1" passwordHash="$2a$10$ztplXcwIaUNu8JXkrS.9ge4WjorJzdUrpBh2.02Y6VXvgxkLKAtvG" /> + * <user name="user2" passwordHash="$2a$10$24wB0UAUsRbOXz4KRZ5KlenzcEddnhIyXMyPkpTnS/29Tt12jfJJW" /> + * <user name="user3" passwordHash="$2a$10$dM0d7CBH3ifNZAPKV3EDNOcljMB80y97on6I8wixH4irMw18DYEi6" /> + * </credentials> + * } + * </pre> + */ +public class FileIdentityProvider implements LoginIdentityProvider { + + static final String PROPERTY_CREDENTIALS_FILE = "Credentials File"; + static final String PROPERTY_EXPIRATION_PERIOD = "Authentication Expiration"; + + private static final Logger logger = LoggerFactory.getLogger(FileIdentityProvider.class); + private static final String CREDENTIALS_XSD = "/credentials.xsd"; + private static final String JAXB_GENERATED_PATH = "org.apache.nifi.authentication.file.generated"; + private static final JAXBContext JAXB_CONTEXT = initializeJaxbContext(); + + private String issuer; + private long expirationPeriodMilliseconds; + private String credentialsFilePath; + private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + private String identifier; + + private static JAXBContext initializeJaxbContext() { + try { + return JAXBContext.newInstance(JAXB_GENERATED_PATH, FileIdentityProvider.class.getClassLoader()); + } catch (JAXBException e) { + throw new RuntimeException("Failed creating JAXBContext for " + FileIdentityProvider.class.getCanonicalName()); + } + } + + private static ValidationEventHandler defaultValidationEventHandler = new ValidationEventHandler() { + @Override + public boolean handleEvent(ValidationEvent event) { + return false; + } + }; + + static UserCredentialsList loadCredentialsList(String filePath) throws Exception { + return loadCredentialsList(filePath, defaultValidationEventHandler); + } + + static UserCredentialsList loadCredentialsList(String filePath, ValidationEventHandler validationEventHandler) throws Exception { + final File userDetailsFile = new File(filePath); + + if (userDetailsFile.exists()) { + final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + final Schema schema = schemaFactory.newSchema(UserCredentialsList.class.getResource(CREDENTIALS_XSD)); + + final Unmarshaller unmarshaller = JAXB_CONTEXT.createUnmarshaller(); + unmarshaller.setSchema(schema); + unmarshaller.setEventHandler(validationEventHandler); + final JAXBElement<UserCredentialsList> element = unmarshaller.unmarshal(new StreamSource(userDetailsFile), + UserCredentialsList.class); + UserCredentialsList credentialsList = element.getValue(); + return credentialsList; + } else { + final String notFoundMessage = "The credentials configuration file was not found at: " + + userDetailsFile.getAbsolutePath(); + throw new FileNotFoundException(notFoundMessage); + } + } + + + @Override + public final void initialize(final LoginIdentityProviderInitializationContext initializationContext) throws ProviderCreationException { + this.identifier = initializationContext.getIdentifier(); + this.issuer = getClass().getSimpleName(); + } + + @Override + public final void onConfigured(final LoginIdentityProviderConfigurationContext configurationContext) throws ProviderCreationException { + final Map<String, String> configProperties = configurationContext.getProperties(); + for (String propertyKey : configProperties.keySet()) { + String propValue = configProperties.get(propertyKey); + logger.debug("found property '{}': '{}'", propertyKey, propValue); + } + + credentialsFilePath = configProperties.get(PROPERTY_CREDENTIALS_FILE); + if (credentialsFilePath == null) { + final String message = String.format("Identity Provider '%s' requires a credentials file path in property '%s'", + identifier, PROPERTY_CREDENTIALS_FILE); + throw new ProviderCreationException(message); + } + + final String rawExpirationPeriod = configProperties.get(PROPERTY_EXPIRATION_PERIOD); + if (rawExpirationPeriod == null || rawExpirationPeriod.isEmpty()) { + final String message = String.format("Identity Provider '%s' requires a credential expiration in property '%s'", + identifier, PROPERTY_EXPIRATION_PERIOD); + throw new ProviderCreationException(message); + } else { + try { + expirationPeriodMilliseconds = FormatUtils.getTimeDuration(rawExpirationPeriod, TimeUnit.MILLISECONDS); + } catch (IllegalArgumentException iae) { + final String message = String.format("Identity Provider '%s' property '%s' value of '%s', is not a valid time period", + identifier, PROPERTY_EXPIRATION_PERIOD, rawExpirationPeriod); + throw new ProviderCreationException(message); + } + } + + logger.debug("Identity Provider '{}' configured to use file '{}' and expiration period of '{}'={} milliseconds", + identifier, credentialsFilePath, rawExpirationPeriod, expirationPeriodMilliseconds); + } + + String getCredentialsFilePath() { + return credentialsFilePath; + } + + long getExpirationPeriod() { + return expirationPeriodMilliseconds; + } + + @Override + public final AuthenticationResponse authenticate(final LoginCredentials credentials) throws InvalidLoginCredentialsException, IdentityAccessException { + final String loginUsername = credentials.getUsername(); + final String loginPassword = credentials.getPassword(); + AuthenticationResponse authResponse = null; + + try { + UserCredentialsList credentialsList = loadCredentialsList(credentialsFilePath); + List<UserCredentials> userCredentials = credentialsList.getUser(); + for (UserCredentials userCreds : userCredentials) { + final String storedUsername = userCreds.getName(); + if (storedUsername.equals(loginUsername)) { --- End diff -- Case-sensitivity on Usernames - Thank you, I'm afraid I had not thought that through properly. I believe this should be consistent with the FileAuthorizationProvider (at least), which appears to be case-insensitive. I'll research more and update appropriately. > Simple Username/Password Authentication > --------------------------------------- > > Key: NIFI-1614 > URL: https://issues.apache.org/jira/browse/NIFI-1614 > Project: Apache NiFi > Issue Type: Improvement > Components: Extensions > Reporter: James Wing > Priority: Minor > > NiFi should include a simple option for username/password authentication > backed by a local file store. NiFi's existing certificate and LDAP > authentication schemes are very secure. However, the configuration and setup > is complex, making them more suitable for long-lived corporate and government > installations, but less accessible for casual or short-term use. Simple > username/password authentication would help more users secure more NiFi > installations beyond anonymous admin access. -- This message was sent by Atlassian JIRA (v6.3.4#6332)