| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.nifi.toolkit.encryptconfig |
| |
| import groovy.cli.commons.CliBuilder |
| import groovy.cli.commons.OptionAccessor |
| import org.apache.commons.cli.HelpFormatter |
| import org.apache.commons.cli.Options |
| import org.apache.nifi.properties.BootstrapProperties |
| import org.apache.nifi.properties.ConfigEncryptionTool |
| import org.apache.nifi.properties.PropertyProtectionScheme |
| import org.apache.nifi.properties.ProtectedPropertyContext |
| import org.apache.nifi.properties.SensitivePropertyProtectionException |
| import org.apache.nifi.properties.SensitivePropertyProvider |
| import org.apache.nifi.properties.SensitivePropertyProviderFactory |
| import org.apache.nifi.properties.StandardSensitivePropertyProviderFactory |
| import org.apache.nifi.registry.properties.util.NiFiRegistryBootstrapUtils |
| import org.apache.nifi.toolkit.encryptconfig.util.BootstrapUtil |
| import org.apache.nifi.toolkit.encryptconfig.util.NiFiRegistryAuthorizersXmlEncryptor |
| import org.apache.nifi.toolkit.encryptconfig.util.NiFiRegistryIdentityProvidersXmlEncryptor |
| import org.apache.nifi.toolkit.encryptconfig.util.NiFiRegistryPropertiesEncryptor |
| import org.apache.nifi.toolkit.encryptconfig.util.ToolUtilities |
| import org.apache.nifi.util.console.TextDevices |
| import org.slf4j.Logger |
| import org.slf4j.LoggerFactory |
| |
| import java.util.function.Supplier |
| |
| class NiFiRegistryMode implements ToolMode { |
| |
| private static final Logger logger = LoggerFactory.getLogger(NiFiRegistryMode.class) |
| |
| CliBuilder cli |
| boolean verboseEnabled |
| |
| NiFiRegistryMode() { |
| cli = cliBuilder() |
| verboseEnabled = false |
| } |
| |
| static Supplier<BootstrapProperties> getBootstrapSupplier(final String bootstrapConfPath) { |
| new Supplier<BootstrapProperties>() { |
| @Override |
| BootstrapProperties get() { |
| try { |
| NiFiRegistryBootstrapUtils.loadBootstrapProperties(bootstrapConfPath) |
| } catch (final IOException e) { |
| throw new SensitivePropertyProtectionException(e.getCause(), e) |
| } |
| } |
| } |
| } |
| |
| @Override |
| void run(String[] args) { |
| try { |
| |
| def options = cli.parse(args) |
| |
| if (!options || options.h) { |
| EncryptConfigMain.printUsageAndExit("", EncryptConfigMain.EXIT_STATUS_OTHER) |
| } |
| |
| if (options.v) { |
| verboseEnabled = true |
| } |
| EncryptConfigLogger.configureLogger(verboseEnabled) |
| |
| NiFiRegistryConfiguration config = new NiFiRegistryConfiguration(options) |
| run(config) |
| |
| } catch (Exception e) { |
| if (verboseEnabled) { |
| logger.error("Encountered an error: ${e.getMessage()}") |
| } |
| EncryptConfigMain.printUsageAndExit(e.getMessage(), EncryptConfigMain.EXIT_STATUS_FAILURE) |
| } |
| } |
| |
| void run(NiFiRegistryConfiguration config) throws Exception { |
| |
| if (config.usingPassword) { |
| logger.info("Using encryption key derived from password.") |
| } else if (config.usingRawKeyHex) { |
| logger.info("Using encryption key provided.") |
| } else if (config.usingBootstrapKey) { |
| logger.info("Using encryption key from input bootstrap.conf.") |
| } |
| |
| logger.debug("(src) bootstrap.conf: ${config.inputBootstrapPath}") |
| logger.debug("(dest) bootstrap.conf: ${config.outputBootstrapPath}") |
| logger.debug("(src) nifi-registry.properties: ${config.inputNiFiRegistryPropertiesPath}") |
| logger.debug("(dest) nifi-registry.properties: ${config.outputNiFiRegistryPropertiesPath}") |
| logger.debug("(src) identity-providers.xml: ${config.inputIdentityProvidersPath}") |
| logger.debug("(dest) identity-providers.xml: ${config.outputIdentityProvidersPath}") |
| logger.debug("(src) authorizers.xml: ${config.inputAuthorizersPath}") |
| logger.debug("(dest) authorizers.xml: ${config.outputAuthorizersPath}") |
| |
| Properties niFiRegistryProperties = null |
| if (config.handlingNiFiRegistryProperties) { |
| try { |
| logger.debug("Encrypting NiFi Registry Properties") |
| niFiRegistryProperties = config.propertiesEncryptor.loadFile(config.inputNiFiRegistryPropertiesPath) |
| // if properties are not protected, then the call to decrypt is a no-op |
| niFiRegistryProperties = config.propertiesEncryptor.decrypt(niFiRegistryProperties) |
| niFiRegistryProperties = config.propertiesEncryptor.encrypt(niFiRegistryProperties) |
| } catch (Exception e) { |
| throw new RuntimeException("Encountered error trying to load and encrypt NiFi Registry Properties in ${config.inputNiFiRegistryPropertiesPath}: ${e.getMessage()}", e) |
| } |
| } |
| |
| String identityProvidersXml = null |
| if (config.handlingIdentityProviders) { |
| try { |
| logger.debug("Encrypting Identity Providers XML") |
| identityProvidersXml = config.identityProvidersXmlEncryptor.loadXmlFile(config.inputIdentityProvidersPath) |
| // if xml is not protected, then the call to decrypt is a no-op |
| identityProvidersXml = config.identityProvidersXmlEncryptor.decrypt(identityProvidersXml) |
| identityProvidersXml = config.identityProvidersXmlEncryptor.encrypt(identityProvidersXml) |
| } catch (Exception e) { |
| throw new RuntimeException("Encountered error trying to load and encrypt Identity Providers XML in ${config.inputIdentityProvidersPath}: ${e.getMessage()}", e) |
| } |
| } |
| |
| String authorizersXml = null |
| if (config.handlingAuthorizers) { |
| try { |
| logger.debug("Encrypting Authorizers XML") |
| authorizersXml = config.authorizersXmlEncryptor.loadXmlFile(config.inputAuthorizersPath) |
| // if xml is not protected, then the call to decrypt is a no-op |
| authorizersXml = config.authorizersXmlEncryptor.decrypt(authorizersXml) |
| authorizersXml = config.authorizersXmlEncryptor.encrypt(authorizersXml) |
| } catch (Exception e) { |
| throw new RuntimeException("Encountered error trying to load and encrypt Authorizers XML in ${config.inputAuthorizersPath}: ${e.getMessage()}", e) |
| } |
| } |
| |
| try { |
| // Do this as part of a transaction? |
| synchronized (this) { |
| |
| if (config.writingKeyToBootstrap) { |
| BootstrapUtil.writeKeyToBootstrapFile(config.encryptionKey, BootstrapUtil.REGISTRY_BOOTSTRAP_KEY_PROPERTY, config.outputBootstrapPath, config.inputBootstrapPath) |
| logger.info("Updated bootstrap config file with root key: ${config.outputBootstrapPath}") |
| } |
| |
| if (config.handlingNiFiRegistryProperties) { |
| config.propertiesEncryptor.write(niFiRegistryProperties, config.outputNiFiRegistryPropertiesPath, config.inputNiFiRegistryPropertiesPath) |
| logger.info("Updated NiFi Registry Properties file with protected values: ${config.outputNiFiRegistryPropertiesPath}") |
| } |
| if (config.handlingIdentityProviders) { |
| config.identityProvidersXmlEncryptor.writeXmlFile(identityProvidersXml, config.outputIdentityProvidersPath, config.inputIdentityProvidersPath) |
| logger.info("Updated Identity Providers XML file with protected values: ${config.outputIdentityProvidersPath}") |
| } |
| if (config.handlingAuthorizers) { |
| config.authorizersXmlEncryptor.writeXmlFile(authorizersXml, config.outputAuthorizersPath, config.inputAuthorizersPath) |
| logger.info("Updated Authorizers XML file with protected values: ${config.outputAuthorizersPath}") |
| } |
| } |
| } catch (Exception e) { |
| throw new RuntimeException("Encountered error while writing the output files: ${e.getMessage()}", e) |
| } |
| } |
| |
| static Options getCliOptions() { |
| return cliBuilder().options |
| } |
| |
| static CliBuilder cliBuilder() { |
| |
| String usage = "${NiFiRegistryMode.class.getCanonicalName()} [options]" |
| |
| int formatWidth = EncryptConfigMain.HELP_FORMAT_WIDTH |
| HelpFormatter formatter = new HelpFormatter() |
| formatter.setWidth(formatWidth) |
| formatter.setOptionComparator(null) // preserve order of options below in help text |
| |
| CliBuilder cli = new CliBuilder( |
| usage: usage, |
| width: formatWidth, |
| formatter: formatter) |
| |
| cli.h(longOpt: 'help', 'Show usage information (this message)') |
| cli.v(longOpt: 'verbose', 'Sets verbose mode (default false)') |
| |
| // Options for the new password or key |
| cli.p(longOpt: 'password', |
| args: 1, |
| argName: 'password', |
| optionalArg: true, |
| 'Protect the files using a password-derived key. If an argument is not provided to this flag, interactive mode will be triggered to prompt the user to enter the password.') |
| cli.k(longOpt: 'key', |
| args: 1, |
| argName: 'keyhex', |
| optionalArg: true, |
| 'Protect the files using a raw hexadecimal key. If an argument is not provided to this flag, interactive mode will be triggered to prompt the user to enter the key.') |
| cli.S(longOpt: 'protectionScheme', |
| args: 1, |
| argName: 'protectionScheme', |
| "Selects the protection scheme for encrypted properties. Valid values are: [${PropertyProtectionScheme.values().join(", ")}] (default is ${ConfigEncryptionTool.DEFAULT_PROTECTION_SCHEME.name()})") |
| |
| // Options for the old password or key, if running the tool to migrate keys |
| cli._(longOpt: 'oldPassword', |
| args: 1, |
| argName: 'password', |
| 'If the input files are already protected using a password-derived key, this specifies the old password so that the files can be unprotected before re-protecting.') |
| cli._(longOpt: 'oldKey', |
| args: 1, |
| argName: 'keyhex', |
| 'If the input files are already protected using a key, this specifies the raw hexadecimal key so that the files can be unprotected before re-protecting.') |
| cli.H(longOpt: 'oldProtectionScheme', |
| args: 1, |
| argName: 'protectionScheme', |
| "The old protection scheme to use during encryption migration (see --protectionScheme for possible values). Default is ${ConfigEncryptionTool.DEFAULT_PROTECTION_SCHEME.name()}.") |
| |
| // Options for output bootstrap.conf file |
| cli.b(longOpt: 'bootstrapConf', |
| args: 1, |
| argName: 'file', |
| 'The bootstrap.conf file containing no root key or an existing root key, and any other protection scheme configuration properties. If a new password or key is specified (using -p or -k) and no output bootstrap.conf file is specified, then this file will be overwritten to persist the new master key.') |
| cli.B(longOpt: 'outputBootstrapConf', |
| args: 1, |
| argName: 'file', |
| 'The destination bootstrap.conf file to persist root key. If specified, the input bootstrap.conf will not be modified.') |
| |
| // Options for input/output nifi-registry.properties files |
| cli.r(longOpt: 'nifiRegistryProperties', |
| args: 1, |
| argName: 'file', |
| 'The nifi-registry.properties file containing unprotected config values, overwritten if no output file specified.') |
| cli.R(longOpt: 'outputNifiRegistryProperties', |
| args: 1, |
| argName: 'file', |
| 'The destination nifi-registry.properties file containing protected config values.') |
| |
| // Options for input/output authorizers.xml files |
| cli.a(longOpt: 'authorizersXml', |
| args: 1, |
| argName: 'file', |
| 'The authorizers.xml file containing unprotected config values, overwritten if no output file specified.') |
| cli.A(longOpt: 'outputAuthorizersXml', |
| args: 1, |
| argName: 'file', |
| 'The destination authorizers.xml file containing protected config values.') |
| |
| // Options for input/output identity-providers.xml files |
| cli.i(longOpt: 'identityProvidersXml', |
| args: 1, |
| argName: 'file', |
| 'The identity-providers.xml file containing unprotected config values, overwritten if no output file specified.') |
| cli.I(longOpt: 'outputIdentityProvidersXml', |
| args: 1, |
| argName: 'file', |
| 'The destination identity-providers.xml file containing protected config values.') |
| |
| return cli |
| |
| } |
| |
| static class NiFiRegistryConfiguration implements Configuration { |
| |
| OptionAccessor rawOptions |
| |
| boolean usingRawKeyHex |
| boolean usingPassword |
| boolean usingBootstrapKey |
| |
| PropertyProtectionScheme protectionScheme |
| String encryptionKey |
| PropertyProtectionScheme oldProtectionScheme |
| String decryptionKey |
| |
| SensitivePropertyProvider encryptionProvider |
| SensitivePropertyProvider decryptionProvider |
| SensitivePropertyProviderFactory providerFactory |
| |
| boolean writingKeyToBootstrap = false |
| String inputBootstrapPath |
| String outputBootstrapPath |
| |
| boolean handlingNiFiRegistryProperties = false |
| String inputNiFiRegistryPropertiesPath |
| String outputNiFiRegistryPropertiesPath |
| NiFiRegistryPropertiesEncryptor propertiesEncryptor |
| |
| boolean handlingIdentityProviders = false |
| String inputIdentityProvidersPath |
| String outputIdentityProvidersPath |
| NiFiRegistryIdentityProvidersXmlEncryptor identityProvidersXmlEncryptor |
| |
| boolean handlingAuthorizers = false |
| String inputAuthorizersPath |
| String outputAuthorizersPath |
| NiFiRegistryAuthorizersXmlEncryptor authorizersXmlEncryptor |
| |
| NiFiRegistryConfiguration() { |
| } |
| |
| NiFiRegistryConfiguration(OptionAccessor options) { |
| this.rawOptions = options |
| |
| validateOptions() |
| |
| // Set input bootstrap.conf path |
| inputBootstrapPath = rawOptions.b |
| |
| determineOldProtectionScheme() |
| // Determine key for decryption (if migrating) |
| determineDecryptionKey() |
| if (!decryptionKey) { |
| logger.debug("No decryption key specified via options, so if any input files require decryption prior to re-encryption (i.e., migration), this tool will fail.") |
| } |
| |
| handlingNiFiRegistryProperties = rawOptions.r |
| if (handlingNiFiRegistryProperties) { |
| inputNiFiRegistryPropertiesPath = rawOptions.r |
| outputNiFiRegistryPropertiesPath = rawOptions.R ?: inputNiFiRegistryPropertiesPath |
| } |
| |
| determineProtectionScheme() |
| |
| // Determine key for encryption (required) |
| determineEncryptionKey() |
| if (!encryptionKey) { |
| throw new RuntimeException("Failed to configure tool, could not determine encryption key. Must provide -p, -k, or -b. If using -b, bootstrap.conf argument must already contain root key.") |
| } |
| providerFactory = StandardSensitivePropertyProviderFactory |
| .withKeyAndBootstrapSupplier(encryptionKey, getBootstrapSupplier(inputBootstrapPath)) |
| encryptionProvider = providerFactory.getProvider(protectionScheme) |
| |
| decryptionProvider = decryptionKey ? providerFactory.getProvider(oldProtectionScheme) : null |
| |
| if (handlingNiFiRegistryProperties) { |
| propertiesEncryptor = new NiFiRegistryPropertiesEncryptor(encryptionProvider, decryptionProvider) |
| } |
| |
| writingKeyToBootstrap = (usingPassword || usingRawKeyHex || rawOptions.B) |
| if (writingKeyToBootstrap) { |
| outputBootstrapPath = rawOptions.B ?: inputBootstrapPath |
| } |
| |
| handlingIdentityProviders = rawOptions.i |
| if (handlingIdentityProviders) { |
| inputIdentityProvidersPath = rawOptions.i |
| outputIdentityProvidersPath = rawOptions.I ?: inputIdentityProvidersPath |
| identityProvidersXmlEncryptor = new NiFiRegistryIdentityProvidersXmlEncryptor(encryptionProvider, decryptionProvider, providerFactory) |
| } |
| |
| handlingAuthorizers = rawOptions.a |
| if (handlingAuthorizers) { |
| inputAuthorizersPath = rawOptions.a |
| outputAuthorizersPath = rawOptions.A ?: inputAuthorizersPath |
| authorizersXmlEncryptor = new NiFiRegistryAuthorizersXmlEncryptor(encryptionProvider, decryptionProvider, providerFactory) |
| } |
| |
| } |
| |
| private void determineProtectionScheme() { |
| |
| if (rawOptions.S) { |
| protectionScheme = PropertyProtectionScheme.valueOf(rawOptions.S) |
| } else { |
| protectionScheme = ConfigEncryptionTool.DEFAULT_PROTECTION_SCHEME |
| } |
| } |
| private void determineOldProtectionScheme() { |
| |
| if (rawOptions.H) { |
| oldProtectionScheme = PropertyProtectionScheme.valueOf(rawOptions.H) |
| } else { |
| oldProtectionScheme = ConfigEncryptionTool.DEFAULT_PROTECTION_SCHEME |
| } |
| } |
| |
| private void validateOptions() { |
| |
| String validationFailedMessage = null |
| |
| if (!rawOptions.b) { |
| validationFailedMessage = "-b flag for bootstrap.conf is required." |
| if (rawOptions.B) { |
| validationFailedMessage += " Input bootsrap.conf will be used as template for output bootstrap.conf" |
| } else if (rawOptions.p || rawOptions.k) { |
| validationFailedMessage = " Encryption key will be persisted to bootstrap.conf" |
| } |
| } |
| |
| if (validationFailedMessage) { |
| throw new RuntimeException("Invalid options: " + validationFailedMessage) |
| } |
| |
| } |
| |
| private void determineEncryptionKey() { |
| if (rawOptions.p || rawOptions.k) { |
| String password = null |
| String keyHex = null |
| if (rawOptions.p) { |
| logger.debug("Attempting to generate key from password.") |
| usingPassword = true |
| password = rawOptions.getOptionValue("p") |
| } else { |
| usingRawKeyHex = true |
| keyHex = rawOptions.getOptionValue("k") |
| } |
| encryptionKey = ToolUtilities.determineKey(TextDevices.defaultTextDevice(), keyHex, password, usingPassword) |
| } else if (rawOptions.b) { |
| logger.debug("Attempting to read root key from input bootstrap.conf file.") |
| usingBootstrapKey = true |
| encryptionKey = BootstrapUtil.extractKeyFromBootstrapFile(inputBootstrapPath, BootstrapUtil.REGISTRY_BOOTSTRAP_KEY_PROPERTY) |
| if (!encryptionKey) { |
| logger.warn("-b specified without -p or -k, but the input bootstrap.conf file did not contain a root key.") |
| } |
| } |
| } |
| |
| private String determineDecryptionKey() { |
| if (rawOptions.oldPassword) { |
| logger.debug("Attempting to generate decryption key (for migration) from old password.") |
| decryptionKey = ToolUtilities.determineKey(TextDevices.defaultTextDevice(), null, rawOptions.oldPassword, true) |
| } else if (rawOptions.oldKey) { |
| decryptionKey = rawOptions.oldKey |
| } |
| } |
| |
| } |
| |
| } |