Return-Path: X-Original-To: apmail-nifi-commits-archive@minotaur.apache.org Delivered-To: apmail-nifi-commits-archive@minotaur.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id 6ABCC10BE5 for ; Fri, 11 Mar 2016 18:51:39 +0000 (UTC) Received: (qmail 11637 invoked by uid 500); 11 Mar 2016 18:51:39 -0000 Delivered-To: apmail-nifi-commits-archive@nifi.apache.org Received: (qmail 11610 invoked by uid 500); 11 Mar 2016 18:51:39 -0000 Mailing-List: contact commits-help@nifi.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@nifi.apache.org Delivered-To: mailing list commits@nifi.apache.org Received: (qmail 11600 invoked by uid 99); 11 Mar 2016 18:51:39 -0000 Received: from arcas.apache.org (HELO arcas) (140.211.11.28) by apache.org (qpsmtpd/0.29) with ESMTP; Fri, 11 Mar 2016 18:51:39 +0000 Received: from arcas.apache.org (localhost [127.0.0.1]) by arcas (Postfix) with ESMTP id 123312C1F69 for ; Fri, 11 Mar 2016 18:51:39 +0000 (UTC) Date: Fri, 11 Mar 2016 18:51:39 +0000 (UTC) From: "ASF GitHub Bot (JIRA)" To: commits@nifi.apache.org Message-ID: In-Reply-To: References: Subject: [jira] [Commented] (NIFI-1614) Simple Username/Password Authentication MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 7bit X-JIRA-FingerPrint: 30527f35849b9dde25b450d4833f0394 [ https://issues.apache.org/jira/browse/NIFI-1614?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=15191375#comment-15191375 ] 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_r55870940 --- 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. + *

+ * The XML format of the credentials file is as follows: + *

    + * {@code
    + * 
    + * 
    + *     
    + *     
    + *     
    + * 
    + * }
    + * 
+ */ +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 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 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); --- End diff -- I did consider this, and I decided on the repetitive read, no-restart-required design. My assumption depends on there being relatively few users and a relatively generous token expiration period. With a 12 hour - 24 hour token expiration period, it did not seem to matter if we read the file 100 times a day to log in 100 users. And how many NiFi installations have 100 users? And of those 100+ user installations, how many are not already down the LDAP or X.509 path? I decided administrative flexibility without restarting would be a nice and inexpensive benefit, fitting with the theme of an 'easier' identity scheme. Do you have a strong opinion? What working assumptions about numbers of users, expiration periods, and login frequency would you follow? > 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)