| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.nifi.properties |
| |
| import groovy.io.GroovyPrintWriter |
| import groovy.util.slurpersupport.GPathResult |
| import groovy.xml.XmlUtil |
| import org.apache.commons.cli.CommandLine |
| import org.apache.commons.cli.CommandLineParser |
| import org.apache.commons.cli.DefaultParser |
| import org.apache.commons.cli.HelpFormatter |
| import org.apache.commons.cli.Option |
| import org.apache.commons.cli.Options |
| import org.apache.commons.cli.ParseException |
| import org.apache.commons.codec.binary.Hex |
| import org.apache.nifi.encrypt.PropertyEncryptor |
| import org.apache.nifi.encrypt.PropertyEncryptorFactory |
| import org.apache.nifi.flow.encryptor.FlowEncryptor |
| import org.apache.nifi.flow.encryptor.StandardFlowEncryptor |
| import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException |
| import org.apache.nifi.toolkit.tls.commandLine.ExitCode |
| import org.apache.nifi.util.NiFiBootstrapUtils |
| import org.apache.nifi.util.NiFiProperties |
| import org.apache.nifi.util.console.TextDevice |
| import org.apache.nifi.util.console.TextDevices |
| import org.bouncycastle.crypto.generators.SCrypt |
| import org.bouncycastle.jce.provider.BouncyCastleProvider |
| import org.slf4j.Logger |
| import org.slf4j.LoggerFactory |
| import org.xml.sax.SAXException |
| |
| import javax.crypto.BadPaddingException |
| import javax.crypto.Cipher |
| import java.nio.charset.StandardCharsets |
| import java.nio.file.Files |
| import java.nio.file.Path |
| import java.nio.file.Paths |
| import java.nio.file.StandardCopyOption |
| import java.security.KeyException |
| import java.security.Security |
| import java.util.function.Supplier |
| import java.util.regex.Matcher |
| import java.util.zip.GZIPInputStream |
| import java.util.zip.GZIPOutputStream |
| import java.util.zip.ZipException |
| |
| class ConfigEncryptionTool { |
| private static final Logger logger = LoggerFactory.getLogger(ConfigEncryptionTool.class) |
| |
| public String bootstrapConfPath |
| public String niFiPropertiesPath |
| public String outputNiFiPropertiesPath |
| public String loginIdentityProvidersPath |
| public String outputLoginIdentityProvidersPath |
| public String authorizersPath |
| public String outputAuthorizersPath |
| public static flowXmlPath |
| public String outputFlowXmlPath |
| |
| static final PropertyProtectionScheme DEFAULT_PROTECTION_SCHEME = PropertyProtectionScheme.AES_GCM |
| |
| private PropertyProtectionScheme protectionScheme = DEFAULT_PROTECTION_SCHEME |
| private PropertyProtectionScheme migrationProtectionScheme = DEFAULT_PROTECTION_SCHEME |
| private String keyHex |
| private String migrationKeyHex |
| private String password |
| private String migrationPassword |
| |
| // This is the raw value used in nifi.sensitive.props.key |
| private String flowPropertiesPassword |
| private String existingFlowPropertiesPassword |
| |
| private String newFlowAlgorithm |
| private String newFlowProvider |
| |
| private NiFiProperties niFiProperties |
| private String loginIdentityProviders |
| private String authorizers |
| private InputStream flowXmlInputStream |
| |
| private boolean usingPassword = true |
| private boolean usingPasswordMigration = true |
| private boolean migration = false |
| private boolean isVerbose = false |
| private boolean handlingNiFiProperties = false |
| private boolean handlingLoginIdentityProviders = false |
| private boolean handlingAuthorizers = false |
| private boolean handlingFlowXml = false |
| private boolean ignorePropertiesFiles = false |
| private boolean translatingCli = false |
| |
| private static final String HELP_ARG = "help" |
| private static final String VERBOSE_ARG = "verbose" |
| private static final String BOOTSTRAP_CONF_ARG = "bootstrapConf" |
| private static final String NIFI_PROPERTIES_ARG = "niFiProperties" |
| private static final String OUTPUT_NIFI_PROPERTIES_ARG = "outputNiFiProperties" |
| private static final String LOGIN_IDENTITY_PROVIDERS_ARG = "loginIdentityProviders" |
| private static final String OUTPUT_LOGIN_IDENTITY_PROVIDERS_ARG = "outputLoginIdentityProviders" |
| private static final String AUTHORIZERS_ARG = "authorizers" |
| private static final String OUTPUT_AUTHORIZERS_ARG = "outputAuthorizers" |
| private static final String FLOW_XML_ARG = "flowXml" |
| private static final String OUTPUT_FLOW_XML_ARG = "outputFlowXml" |
| private static final String KEY_ARG = "key" |
| private static final String PROTECTION_SCHEME_ARG = "protectionScheme" |
| private static final String PASSWORD_ARG = "password" |
| private static final String KEY_MIGRATION_ARG = "oldKey" |
| private static final String PASSWORD_MIGRATION_ARG = "oldPassword" |
| private static final String PROTECTION_SCHEME_MIGRATION_ARG = "oldProtectionScheme" |
| private static final String USE_KEY_ARG = "useRawKey" |
| private static final String MIGRATION_ARG = "migrate" |
| private static final String PROPS_KEY_ARG = "propsKey" |
| private static final String DO_NOT_ENCRYPT_NIFI_PROPERTIES_ARG = "encryptFlowXmlOnly" |
| private static final String NEW_FLOW_ALGORITHM_ARG = "newFlowAlgorithm" |
| private static final String NEW_FLOW_PROVIDER_ARG = "newFlowProvider" |
| private static final String TRANSLATE_CLI_ARG = "translateCli" |
| |
| private static final String PROTECTION_SCHEME_DESC = String.format("Selects the protection scheme for encrypted properties. " + |
| "Valid values are: [%s] (default is %s)", PropertyProtectionScheme.values().join(", "), DEFAULT_PROTECTION_SCHEME.name()) |
| |
| // Static holder to avoid re-generating the options object multiple times in an invocation |
| private static Options staticOptions |
| |
| // Hard-coded fallback value from historical defaults |
| private static final String DEFAULT_NIFI_SENSITIVE_PROPS_KEY = "nififtw!" |
| private static final int MIN_PASSWORD_LENGTH = 12 |
| |
| // Strong parameters as of 12 Aug 2016 (for key derivation) |
| // This value can remain an int until best practice specifies a value above 2**32 |
| private static final int SCRYPT_N = 2**16 |
| private static final int SCRYPT_R = 8 |
| private static final int SCRYPT_P = 1 |
| |
| private static |
| final String BOOTSTRAP_KEY_COMMENT = "# Root key in hexadecimal format for encrypted sensitive configuration values" |
| private static final String BOOTSTRAP_KEY_PREFIX = "nifi.bootstrap.sensitive.key=" |
| private static final String JAVA_HOME = "JAVA_HOME" |
| private static final String NIFI_TOOLKIT_HOME = "NIFI_TOOLKIT_HOME" |
| private static final String SEP = System.lineSeparator() |
| |
| private static final String FOOTER = buildFooter() |
| |
| private static |
| final String DEFAULT_DESCRIPTION = "This tool reads from a nifi.properties and/or " + |
| "login-identity-providers.xml file with plain sensitive configuration values, " + |
| "prompts the user for a root key, and encrypts each value. It will replace the " + |
| "plain value with the protected value in the same file (or write to a new file if " + |
| "specified). It can also be used to migrate already-encrypted values in those " + |
| "files or in flow.xml.gz to be encrypted with a new key." |
| |
| private static final String LDAP_PROVIDER_CLASS = "org.apache.nifi.ldap.LdapProvider" |
| private static |
| final String LDAP_PROVIDER_REGEX = /(?s)<provider>(?:(?!<provider>).)*?<class>\s*org\.apache\.nifi\.ldap\.LdapProvider.*?<\/provider>/ |
| /* Explanation of LDAP_PROVIDER_REGEX: |
| * (?s) -> single-line mode (i.e., `.` in regex matches newlines) |
| * <provider> -> find occurrence of `<provider>` literally (case-sensitive) |
| * (?: ... ) -> group but do not capture submatch |
| * (?! ... ) -> negative lookahead |
| * (?:(?!<provider>).)*? -> find everything until a new `<provider>` starts. This is for not selecting multiple providers in one match |
| * <class> -> find occurrence of `<class>` literally (case-sensitive) |
| * \s* -> find any whitespace |
| * org\.apache\.nifi\.ldap\.LdapProvider |
| * -> find occurrence of `org.apache.nifi.ldap.LdapProvider` literally (case-sensitive) |
| * .*?</provider> -> find everything as needed up until and including occurrence of `</provider>` |
| */ |
| |
| private static final String LDAP_USER_GROUP_PROVIDER_CLASS = "org.apache.nifi.ldap.tenants.LdapUserGroupProvider" |
| private static final String LDAP_USER_GROUP_PROVIDER_REGEX = |
| /(?s)<userGroupProvider>(?:(?!<userGroupProvider>).)*?<class>\s*org\.apache\.nifi\.ldap\.tenants\.LdapUserGroupProvider.*?<\/userGroupProvider>/ |
| /* Explanation of LDAP_USER_GROUP_PROVIDER_REGEX: |
| * (?s) -> single-line mode (i.e., `.` in regex matches newlines) |
| * <userGroupProvider> -> find occurrence of `<userGroupProvider>` literally (case-sensitive) |
| * (?: ... ) -> group but do not capture submatch |
| * (?! ... ) -> negative lookahead |
| * (?:(?!<userGroupProvider>).)*? -> find everything until a new `<userGroupProvider>` starts. This is for not selecting multiple userGroupProviders in one match |
| * <class> -> find occurrence of `<class>` literally (case-sensitive) |
| * \s* -> find any whitespace |
| * org\.apache\.nifi\.ldap\.tenants\.LdapUserGroupProvider |
| * -> find occurrence of `org.apache.nifi.ldap.tenants.LdapUserGroupProvider` literally (case-sensitive) |
| * .*?</userGroupProvider> -> find everything as needed up until and including occurrence of '</userGroupProvider>' |
| */ |
| |
| private static final String XML_DECLARATION_REGEX = /<\?xml version="1.0" encoding="UTF-8"\?>/ |
| private static final String WRAPPED_FLOW_XML_CIPHER_TEXT_REGEX = /enc\{[a-fA-F0-9]+?\}/ |
| |
| private static final String DEFAULT_FLOW_ALGORITHM = "PBEWITHMD5AND256BITAES-CBC-OPENSSL" |
| |
| private static final Map<String, String> PROPERTY_KEY_MAP = [ |
| "nifi.security.keystore" : "keystore", |
| "nifi.security.keystoreType" : "keystoreType", |
| "nifi.security.keystorePasswd" : "keystorePasswd", |
| "nifi.security.keyPasswd" : "keyPasswd", |
| "nifi.security.truststore" : "truststore", |
| "nifi.security.truststoreType" : "truststoreType", |
| "nifi.security.truststorePasswd": "truststorePasswd", |
| ] |
| |
| private static String buildHeader(String description = DEFAULT_DESCRIPTION) { |
| "${SEP}${description}${SEP * 2}" |
| } |
| |
| private static String buildFooter() { |
| "${SEP}Java home: ${System.getenv(JAVA_HOME)}${SEP}NiFi Toolkit home: ${System.getenv(NIFI_TOOLKIT_HOME)}" |
| } |
| |
| private final Options options |
| private final String header |
| |
| |
| ConfigEncryptionTool() { |
| this(DEFAULT_DESCRIPTION) |
| } |
| |
| ConfigEncryptionTool(String description) { |
| this.header = buildHeader(description) |
| this.options = getCliOptions() |
| } |
| |
| static Options buildOptions() { |
| Options options = new Options() |
| options.addOption(Option.builder("h").longOpt(HELP_ARG).hasArg(false).desc("Show usage information (this message)").build()) |
| options.addOption(Option.builder("v").longOpt(VERBOSE_ARG).hasArg(false).desc("Sets verbose mode (default false)").build()) |
| options.addOption(Option.builder("n").longOpt(NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("The nifi.properties file containing unprotected config values (will be overwritten unless -o is specified)").build()) |
| options.addOption(Option.builder("o").longOpt(OUTPUT_NIFI_PROPERTIES_ARG).hasArg(true).argName("file").desc("The destination nifi.properties file containing protected config values (will not modify input nifi.properties)").build()) |
| options.addOption(Option.builder("l").longOpt(LOGIN_IDENTITY_PROVIDERS_ARG).hasArg(true).argName("file").desc("The login-identity-providers.xml file containing unprotected config values (will be overwritten unless -i is specified)").build()) |
| options.addOption(Option.builder("i").longOpt(OUTPUT_LOGIN_IDENTITY_PROVIDERS_ARG).hasArg(true).argName("file").desc("The destination login-identity-providers.xml file containing protected config values (will not modify input login-identity-providers.xml)").build()) |
| options.addOption(Option.builder("a").longOpt(AUTHORIZERS_ARG).hasArg(true).argName("file").desc("The authorizers.xml file containing unprotected config values (will be overwritten unless -u is specified)").build()) |
| options.addOption(Option.builder("u").longOpt(OUTPUT_AUTHORIZERS_ARG).hasArg(true).argName("file").desc("The destination authorizers.xml file containing protected config values (will not modify input authorizers.xml)").build()) |
| options.addOption(Option.builder("f").longOpt(FLOW_XML_ARG).hasArg(true).argName("file").desc("The flow.xml.gz file currently protected with old password (will be overwritten unless -g is specified)").build()) |
| options.addOption(Option.builder("g").longOpt(OUTPUT_FLOW_XML_ARG).hasArg(true).argName("file").desc("The destination flow.xml.gz file containing protected config values (will not modify input flow.xml.gz)").build()) |
| options.addOption(Option.builder("b").longOpt(BOOTSTRAP_CONF_ARG).hasArg(true).argName("file").desc("The bootstrap.conf file to persist root key and to optionally provide any configuration for the protection scheme.").build()) |
| options.addOption(Option.builder("S").longOpt(PROTECTION_SCHEME_ARG).hasArg(true).argName("protectionScheme").desc(PROTECTION_SCHEME_DESC).build()) |
| options.addOption(Option.builder("k").longOpt(KEY_ARG).hasArg(true).argName("keyhex").desc("The raw hexadecimal key to use to encrypt the sensitive properties").build()) |
| options.addOption(Option.builder("e").longOpt(KEY_MIGRATION_ARG).hasArg(true).argName("keyhex").desc("The old raw hexadecimal key to use during key migration").build()) |
| options.addOption(Option.builder("H").longOpt(PROTECTION_SCHEME_MIGRATION_ARG).hasArg(true).argName("protectionScheme").desc("The old protection scheme to use during encryption migration (see --protectionScheme for possible values). Default is " + DEFAULT_PROTECTION_SCHEME.name()).build()) |
| options.addOption(Option.builder("p").longOpt(PASSWORD_ARG).hasArg(true).argName("password").desc("The password from which to derive the key to use to encrypt the sensitive properties").build()) |
| options.addOption(Option.builder("w").longOpt(PASSWORD_MIGRATION_ARG).hasArg(true).argName("password").desc("The old password from which to derive the key during migration").build()) |
| options.addOption(Option.builder("r").longOpt(USE_KEY_ARG).hasArg(false).desc("If provided, the secure console will prompt for the raw key value in hexadecimal form").build()) |
| options.addOption(Option.builder("m").longOpt(MIGRATION_ARG).hasArg(false).desc("If provided, the nifi.properties and/or login-identity-providers.xml sensitive properties will be re-encrypted with the new scheme").build()) |
| options.addOption(Option.builder("x").longOpt(DO_NOT_ENCRYPT_NIFI_PROPERTIES_ARG).hasArg(false).desc("If provided, the properties in flow.xml.gz will be re-encrypted with a new key but the nifi.properties and/or login-identity-providers.xml files will not be modified").build()) |
| options.addOption(Option.builder("s").longOpt(PROPS_KEY_ARG).hasArg(true).argName("password|keyhex").desc("The password or key to use to encrypt the sensitive processor properties in flow.xml.gz").build()) |
| options.addOption(Option.builder("A").longOpt(NEW_FLOW_ALGORITHM_ARG).hasArg(true).argName("algorithm").desc("The algorithm to use to encrypt the sensitive processor properties in flow.xml.gz").build()) |
| options.addOption(Option.builder("P").longOpt(NEW_FLOW_PROVIDER_ARG).hasArg(true).argName("algorithm").desc("The security provider to use to encrypt the sensitive processor properties in flow.xml.gz").build()) |
| options.addOption(Option.builder("c").longOpt(TRANSLATE_CLI_ARG).hasArg(false).desc("Translates the nifi.properties file to a format suitable for the NiFi CLI tool").build()) |
| options |
| } |
| |
| static Options getCliOptions() { |
| if (!staticOptions) { |
| staticOptions = buildOptions() |
| } |
| return staticOptions |
| } |
| |
| /** |
| * Prints the usage message and available arguments for this tool (along with a specific error message if provided). |
| * |
| * @param errorMessage the optional error message |
| */ |
| void printUsage(String errorMessage) { |
| if (errorMessage) { |
| System.out.println(errorMessage) |
| System.out.println() |
| } |
| HelpFormatter helpFormatter = new HelpFormatter() |
| helpFormatter.setWidth(160) |
| helpFormatter.setOptionComparator(null) |
| // preserve manual ordering of options when printing instead of alphabetical |
| helpFormatter.printHelp(ConfigEncryptionTool.class.getCanonicalName(), header, options, FOOTER, true) |
| } |
| |
| protected void printUsageAndThrow(String errorMessage, ExitCode exitCode) throws CommandLineParseException { |
| printUsage(errorMessage) |
| throw new CommandLineParseException(errorMessage, exitCode) |
| } |
| |
| // TODO: Refactor component steps into methods |
| protected CommandLine parse(String[] args) throws CommandLineParseException { |
| CommandLineParser parser = new DefaultParser() |
| CommandLine commandLine |
| try { |
| commandLine = parser.parse(options, args) |
| if (commandLine.hasOption(HELP_ARG)) { |
| printUsageAndThrow(null, ExitCode.HELP) |
| } |
| |
| isVerbose = commandLine.hasOption(VERBOSE_ARG) |
| |
| // If this flag is present, ensure no other options are present and then fail/return |
| if (commandLine.hasOption(TRANSLATE_CLI_ARG)) { |
| translatingCli = true |
| if (commandLineHasActionFlags(commandLine, [TRANSLATE_CLI_ARG, BOOTSTRAP_CONF_ARG, NIFI_PROPERTIES_ARG])) { |
| printUsageAndThrow("When '-c'/'--${TRANSLATE_CLI_ARG}' is specified, only '-h', '-v', and '-n'/'-b' with the relevant files are allowed", ExitCode.INVALID_ARGS) |
| } |
| } |
| |
| bootstrapConfPath = commandLine.getOptionValue(BOOTSTRAP_CONF_ARG) |
| |
| // This needs to occur even if the nifi.properties won't be encrypted |
| if (commandLine.hasOption(NIFI_PROPERTIES_ARG)) { |
| boolean ignoreFlagPresent = commandLine.hasOption(DO_NOT_ENCRYPT_NIFI_PROPERTIES_ARG) |
| if (isVerbose && !ignoreFlagPresent) { |
| logger.info("Handling encryption of nifi.properties") |
| } |
| niFiPropertiesPath = commandLine.getOptionValue(NIFI_PROPERTIES_ARG) |
| outputNiFiPropertiesPath = commandLine.getOptionValue(OUTPUT_NIFI_PROPERTIES_ARG, niFiPropertiesPath) |
| handlingNiFiProperties = !ignoreFlagPresent |
| |
| if (niFiPropertiesPath == outputNiFiPropertiesPath) { |
| // TODO: Add confirmation pause and provide -y flag to offer no-interaction mode? |
| logger.warn("The source nifi.properties and destination nifi.properties are identical [${outputNiFiPropertiesPath}] so the original will be overwritten") |
| } |
| } |
| |
| if (commandLine.hasOption(PROTECTION_SCHEME_ARG)) { |
| protectionScheme = PropertyProtectionScheme.valueOf(commandLine.getOptionValue(PROTECTION_SCHEME_ARG)) |
| } |
| |
| // If translating nifi.properties to CLI format, none of the remaining parsing is necessary |
| if (translatingCli) { |
| |
| // If the nifi.properties isn't present, throw an exception |
| // If the nifi.properties is encrypted and the bootstrap.conf isn't present, we will throw an error later when the encryption is detected |
| if (!niFiPropertiesPath) { |
| printUsageAndThrow("When '-c'/'--translateCli' is specified, '-n'/'--niFiProperties' is required (and '-b'/'--bootstrapConf' is required if the properties are encrypted)", ExitCode.INVALID_ARGS) |
| } |
| |
| return commandLine |
| } |
| |
| // If this flag is provided, the nifi.properties is necessary to read/write the flow encryption key, but the encryption process will not actually be applied to nifi.properties / login-identity-providers.xml |
| if (commandLine.hasOption(DO_NOT_ENCRYPT_NIFI_PROPERTIES_ARG)) { |
| handlingNiFiProperties = false |
| handlingLoginIdentityProviders = false |
| handlingAuthorizers = false |
| ignorePropertiesFiles = true |
| } else { |
| if (commandLine.hasOption(LOGIN_IDENTITY_PROVIDERS_ARG)) { |
| if (isVerbose) { |
| logger.info("Handling encryption of login-identity-providers.xml") |
| } |
| loginIdentityProvidersPath = commandLine.getOptionValue(LOGIN_IDENTITY_PROVIDERS_ARG) |
| outputLoginIdentityProvidersPath = commandLine.getOptionValue(OUTPUT_LOGIN_IDENTITY_PROVIDERS_ARG, loginIdentityProvidersPath) |
| handlingLoginIdentityProviders = true |
| |
| if (loginIdentityProvidersPath == outputLoginIdentityProvidersPath) { |
| // TODO: Add confirmation pause and provide -y flag to offer no-interaction mode? |
| logger.warn("The source login-identity-providers.xml and destination login-identity-providers.xml are identical [${outputLoginIdentityProvidersPath}] so the original will be overwritten") |
| } |
| } |
| if (commandLine.hasOption(AUTHORIZERS_ARG)) { |
| if (isVerbose) { |
| logger.info("Handling encryption of authorizers.xml") |
| } |
| authorizersPath = commandLine.getOptionValue(AUTHORIZERS_ARG) |
| outputAuthorizersPath = commandLine.getOptionValue(OUTPUT_AUTHORIZERS_ARG, authorizersPath) |
| handlingAuthorizers = true |
| |
| if (authorizersPath == outputAuthorizersPath) { |
| // TODO: Add confirmation pause and provide -y flag to offer no-interaction mode? |
| logger.warn("The source authorizers.xml and destination authorizers.xml are identical [${outputAuthorizersPath}] so the original will be overwritten") |
| } |
| } |
| } |
| |
| if (commandLine.hasOption(FLOW_XML_ARG)) { |
| if (isVerbose) { |
| logger.info("Handling encryption of flow.xml.gz") |
| } |
| flowXmlPath = commandLine.getOptionValue(FLOW_XML_ARG) |
| outputFlowXmlPath = commandLine.getOptionValue(OUTPUT_FLOW_XML_ARG, flowXmlPath) |
| handlingFlowXml = true |
| |
| newFlowAlgorithm = commandLine.getOptionValue(NEW_FLOW_ALGORITHM_ARG) |
| newFlowProvider = commandLine.getOptionValue(NEW_FLOW_PROVIDER_ARG) |
| |
| if (flowXmlPath == outputFlowXmlPath) { |
| // TODO: Add confirmation pause and provide -y flag to offer no-interaction mode? |
| logger.warn("The source flow.xml.gz and destination flow.xml.gz are identical [${outputFlowXmlPath}] so the original will be overwritten") |
| } |
| |
| if (!commandLine.hasOption(NIFI_PROPERTIES_ARG)) { |
| printUsageAndThrow("In order to migrate a flow.xml.gz, a nifi.properties file must also be specified via '-n'/'--${NIFI_PROPERTIES_ARG}'.", ExitCode.INVALID_ARGS) |
| } |
| } |
| |
| if (isVerbose) { |
| logger.info(" bootstrap.conf: ${bootstrapConfPath}") |
| logger.info("(src) nifi.properties: ${niFiPropertiesPath}") |
| logger.info("(dest) nifi.properties: ${outputNiFiPropertiesPath}") |
| logger.info("(src) login-identity-providers.xml: ${loginIdentityProvidersPath}") |
| logger.info("(dest) login-identity-providers.xml: ${outputLoginIdentityProvidersPath}") |
| logger.info("(src) authorizers.xml: ${authorizersPath}") |
| logger.info("(dest) authorizers.xml: ${outputAuthorizersPath}") |
| logger.info("(src) flow.xml.gz: ${flowXmlPath}") |
| logger.info("(dest) flow.xml.gz: ${outputFlowXmlPath}") |
| } |
| |
| if (!commandLine.hasOption(NIFI_PROPERTIES_ARG) |
| && !commandLine.hasOption(LOGIN_IDENTITY_PROVIDERS_ARG) |
| && !commandLine.hasOption(AUTHORIZERS_ARG) |
| && !commandLine.hasOption(DO_NOT_ENCRYPT_NIFI_PROPERTIES_ARG) |
| ) { |
| printUsageAndThrow("One or more of [" + |
| "'-n'/'--${NIFI_PROPERTIES_ARG}', " + |
| "'-l'/'--${LOGIN_IDENTITY_PROVIDERS_ARG}', " + |
| "'-a'/'--${AUTHORIZERS_ARG}'" + |
| "] must be provided unless " + |
| "'-x'/--'${DO_NOT_ENCRYPT_NIFI_PROPERTIES_ARG}' is specified", ExitCode.INVALID_ARGS) |
| } |
| |
| if (commandLine.hasOption(MIGRATION_ARG)) { |
| migration = true |
| if (isVerbose) { |
| logger.info("Key migration mode activated") |
| } |
| if (commandLine.hasOption(PROTECTION_SCHEME_MIGRATION_ARG)) { |
| migrationProtectionScheme = PropertyProtectionScheme.valueOf(commandLine.getOptionValue(PROTECTION_SCHEME_MIGRATION_ARG)) |
| } |
| |
| if (migrationProtectionScheme.requiresSecretKey()) { |
| if (commandLine.hasOption(PASSWORD_MIGRATION_ARG)) { |
| usingPasswordMigration = true |
| if (commandLine.hasOption(KEY_MIGRATION_ARG)) { |
| printUsageAndThrow("Only one of '-w'/'--${PASSWORD_MIGRATION_ARG}' and '-e'/'--${KEY_MIGRATION_ARG}' can be used", ExitCode.INVALID_ARGS) |
| } else { |
| migrationPassword = commandLine.getOptionValue(PASSWORD_MIGRATION_ARG) |
| } |
| } else { |
| migrationKeyHex = commandLine.getOptionValue(KEY_MIGRATION_ARG) |
| // Use the "migration password" value if the migration key hex is absent |
| usingPasswordMigration = !migrationKeyHex |
| } |
| } |
| } else { |
| if (commandLine.hasOption(PASSWORD_MIGRATION_ARG) || commandLine.hasOption(KEY_MIGRATION_ARG)) { |
| printUsageAndThrow("'-w'/'--${PASSWORD_MIGRATION_ARG}' and '-e'/'--${KEY_MIGRATION_ARG}' are ignored unless '-m'/'--${MIGRATION_ARG}' is enabled", ExitCode.INVALID_ARGS) |
| } |
| } |
| |
| if (protectionScheme.requiresSecretKey()) { |
| if (commandLine.hasOption(PASSWORD_ARG)) { |
| usingPassword = true |
| if (commandLine.hasOption(KEY_ARG)) { |
| printUsageAndThrow("Only one of '-p'/'--${PASSWORD_ARG}' and '-k'/'--${KEY_ARG}' can be used", ExitCode.INVALID_ARGS) |
| } else { |
| password = commandLine.getOptionValue(PASSWORD_ARG) |
| } |
| } else { |
| keyHex = commandLine.getOptionValue(KEY_ARG) |
| usingPassword = !keyHex |
| } |
| |
| if (commandLine.hasOption(USE_KEY_ARG)) { |
| if (keyHex || password) { |
| logger.warn("If the key or password is provided in the arguments, '-r'/'--${USE_KEY_ARG}' is ignored") |
| } else { |
| usingPassword = false |
| } |
| } |
| } |
| |
| if (commandLine.hasOption(PROPS_KEY_ARG)) { |
| flowPropertiesPassword = commandLine.getOptionValue(PROPS_KEY_ARG) |
| } |
| } catch (ParseException e) { |
| if (isVerbose) { |
| logger.error("Encountered an error", e) |
| } |
| printUsageAndThrow("Error parsing command line. (" + e.getMessage() + ")", ExitCode.ERROR_PARSING_COMMAND_LINE) |
| } |
| return commandLine |
| } |
| |
| /** |
| * Returns true if the {@code commandLine} object has flags other than the {@code help} or {@code verbose} flags or any of the acceptable args provided in an optional parameter. This is used to detect incompatible arguments for specific modes. |
| * |
| * @param commandLine the commandLine object |
| * @param acceptableOptionStrings an optional list of acceptable options that can be present without returning true |
| * @return true if incompatible flags are present |
| */ |
| boolean commandLineHasActionFlags(CommandLine commandLine, List<String> acceptableOptionStrings = []) { |
| // Resolve the list of Option objects corresponding to "help" and "verbose" |
| final List<Option> ALWAYS_ACCEPTABLE_OPTIONS = resolveOptions([HELP_ARG, VERBOSE_ARG]) |
| |
| // Resolve the list of Option objects corresponding to the provided "additional acceptable options" |
| List<Option> acceptableOptions = resolveOptions(acceptableOptionStrings) |
| |
| // Determine the options submitted to the command line that are not acceptable |
| List<Option> invalidOptions = commandLine.options - (acceptableOptions + ALWAYS_ACCEPTABLE_OPTIONS) |
| if (invalidOptions) { |
| if (isVerbose) { |
| logger.error("In this mode, the following options are invalid: ${invalidOptions}") |
| } |
| return true |
| } else { |
| return false |
| } |
| } |
| |
| static List<Option> resolveOptions(List<String> strings) { |
| strings?.collect { String opt -> |
| getCliOptions().options.find { it.opt == opt || it.longOpt == opt } |
| } |
| } |
| |
| /** |
| * The method returns the provided, derived, or securely-entered key in hex format. The reason the parameters must be provided instead of read from the fields is because this is used for the regular key/password and the migration key/password. |
| * |
| * @param device |
| * @param keyHex |
| * @param password |
| * @param usingPassword |
| * @return |
| */ |
| private String getKeyInternal(TextDevice device = TextDevices.defaultTextDevice(), String keyHex, String password, boolean usingPassword) { |
| if (usingPassword) { |
| if (!password) { |
| if (isVerbose) { |
| logger.info("Reading password from secure console") |
| } |
| password = readPasswordFromConsole(device) |
| } |
| keyHex = deriveKeyFromPassword(password) |
| password = null |
| return keyHex |
| } else { |
| if (!keyHex) { |
| if (isVerbose) { |
| logger.info("Reading hex key from secure console") |
| } |
| keyHex = readKeyFromConsole(device) |
| } |
| return keyHex |
| } |
| } |
| |
| private String getKey(TextDevice textDevice = TextDevices.defaultTextDevice()) { |
| getKeyInternal(textDevice, keyHex, password, usingPassword) |
| } |
| |
| private String getMigrationKey() { |
| return getKeyInternal(TextDevices.defaultTextDevice(), migrationKeyHex, migrationPassword, usingPasswordMigration) |
| } |
| |
| private static String getFlowPassword(TextDevice textDevice = TextDevices.defaultTextDevice()) { |
| readPasswordFromConsole(textDevice) |
| } |
| |
| private static String readKeyFromConsole(TextDevice textDevice) { |
| textDevice.printf("Enter the root key in hexadecimal format (spaces acceptable): ") |
| new String(textDevice.readPassword()) |
| } |
| |
| private static String readPasswordFromConsole(TextDevice textDevice) { |
| textDevice.printf("Enter the password: ") |
| new String(textDevice.readPassword()) |
| } |
| |
| /** |
| * Returns the key in uppercase hexadecimal format with delimiters (spaces, '-', etc.) removed. All non-hex chars are removed. If the result is not a valid length (32, 48, 64 chars depending on the JCE), an exception is thrown. |
| * |
| * @param rawKey the unprocessed key input |
| * @return the formatted hex string in uppercase |
| * @throws KeyException if the key is not a valid length after parsing |
| */ |
| private static String parseKey(String rawKey) throws KeyException { |
| String hexKey = rawKey.replaceAll("[^0-9a-fA-F]", "") |
| def validKeyLengths = getValidKeyLengths() |
| List<Integer> validHexCharLengths = validKeyLengths.collect {it / 4 } |
| if (!validKeyLengths.contains(hexKey.size() * 4)) { |
| throw new KeyException("The key (${hexKey.size()} hex chars) must be of length ${validKeyLengths} " + |
| "bits (${validHexCharLengths} hex characters)") |
| } |
| hexKey.toUpperCase() |
| } |
| |
| /** |
| * Returns the list of acceptable key lengths in bits based on the current JCE policies. |
| * |
| * @return 128 , [192, 256] |
| */ |
| static List<Integer> getValidKeyLengths() { |
| Cipher.getMaxAllowedKeyLength("AES") > 128 ? [128, 192, 256] : [128] |
| } |
| |
| private NiFiPropertiesLoader getNiFiPropertiesLoader(final String keyHex) { |
| return protectionScheme.requiresSecretKey() || migrationProtectionScheme.requiresSecretKey() |
| ? NiFiPropertiesLoader.withKey(keyHex) : new NiFiPropertiesLoader() |
| } |
| |
| /** |
| * Loads the {@link NiFiProperties} instance from the provided file path (restoring the original value of the System property {@code nifi.properties.file.path} after loading this instance). |
| * |
| * @return the NiFiProperties instance |
| * @throw IOException if the nifi.properties file cannot be read |
| */ |
| private NiFiProperties loadNiFiProperties(String existingKeyHex = keyHex) throws IOException { |
| File niFiPropertiesFile |
| if (niFiPropertiesPath && (niFiPropertiesFile = new File(niFiPropertiesPath)).exists()) { |
| NiFiProperties properties |
| try { |
| properties = getNiFiPropertiesLoader(existingKeyHex).load(niFiPropertiesFile) |
| logger.info("Loaded NiFiProperties instance with ${properties.size()} properties") |
| return properties |
| } catch (RuntimeException e) { |
| if (isVerbose) { |
| logger.error("Encountered an error", e) |
| } |
| throw new IOException("Cannot load NiFiProperties from [${niFiPropertiesPath}]", e) |
| } |
| } else { |
| printUsageAndThrow("Cannot load NiFiProperties from [${niFiPropertiesPath}]", ExitCode.ERROR_READING_NIFI_PROPERTIES) |
| } |
| } |
| |
| /** |
| * Loads the login identity providers configuration from the provided file path. |
| * |
| * @param existingKeyHex the key used to encrypt the configs (defaults to the current key) |
| * |
| * @return the file content |
| * @throw IOException if the login-identity-providers.xml file cannot be read |
| */ |
| private String loadLoginIdentityProviders(String existingKeyHex = keyHex) throws IOException { |
| File loginIdentityProvidersFile |
| if (loginIdentityProvidersPath && (loginIdentityProvidersFile = new File(loginIdentityProvidersPath)).exists()) { |
| try { |
| List<String> lines = loginIdentityProvidersFile.readLines() |
| String xmlContent = lines.join("\n") |
| logger.info("Loaded login identity providers content (${lines.size()} lines)") |
| String decryptedXmlContent = decryptLoginIdentityProviders(xmlContent, existingKeyHex) |
| return decryptedXmlContent |
| } catch (RuntimeException e) { |
| if (isVerbose) { |
| logger.error("Encountered an error", e) |
| } |
| throw new IOException("Cannot load login identity providers from [${loginIdentityProvidersPath}]", e) |
| } |
| } else { |
| printUsageAndThrow("Cannot load login identity providers from [${loginIdentityProvidersPath}]", ExitCode.ERROR_READING_NIFI_PROPERTIES) |
| } |
| } |
| |
| /** |
| * Loads the authorizers configuration from the provided file path. |
| * |
| * @param existingKeyHex the key used to encrypt the configs (defaults to the current key) |
| * |
| * @return the file content |
| * @throw IOException if the authorizers.xml file cannot be read |
| */ |
| private String loadAuthorizers(String existingKeyHex = keyHex) throws IOException { |
| File authorizersFile |
| if (authorizersPath && (authorizersFile = new File(authorizersPath)).exists()) { |
| try { |
| List<String> lines = authorizersFile.readLines() |
| String xmlContent = lines.join("\n") |
| logger.info("Loaded authorizers content (${lines.size()} lines)") |
| String decryptedXmlContent = decryptAuthorizers(xmlContent, existingKeyHex) |
| return decryptedXmlContent |
| } catch (RuntimeException e) { |
| if (isVerbose) { |
| logger.error("Encountered an error", e) |
| } |
| throw new IOException("Cannot load authorizers from [${authorizersPath}]", e) |
| } |
| } else { |
| printUsageAndThrow("Cannot load authorizers from [${authorizersPath}]", ExitCode.ERROR_READING_NIFI_PROPERTIES) |
| } |
| } |
| |
| /** |
| * Loads the flow definition from the provided file path, handling the GZIP file compression. Unlike {@link #loadLoginIdentityProviders()} this method does not decrypt the content (for performance and separation of concern reasons). |
| * |
| * @param The path to the XML file |
| * @return The file content |
| * @throw IOException if the flow.xml.gz file cannot be read |
| */ |
| private InputStream loadFlowXml(String filePath) throws IOException { |
| if (filePath && (new File(filePath)).exists()) { |
| try { |
| return new GZIPInputStream(new FileInputStream(filePath)) |
| } catch (ZipException e) { |
| return new FileInputStream(filePath) |
| } catch (RuntimeException e) { |
| if (isVerbose) { |
| logger.error("Encountered an error", e) |
| } |
| throw new IOException("Cannot load flow from [${filePath}]", e) |
| } |
| } else { |
| printUsageAndThrow("Cannot load flow from [${filePath}]", ExitCode.ERROR_READING_NIFI_PROPERTIES) |
| } |
| } |
| |
| /** |
| * Scans XML content and decrypts each encrypted element, then re-encrypts it with the new key, and returns the final XML content. |
| * |
| * @param flowXmlContent the original flow.xml.gz as an input stream |
| * @param existingFlowPassword the existing value of nifi.sensitive.props.key (not a raw key, but rather a password) |
| * @param newFlowPassword the password to use to for encryption (not a raw key, but rather a password) |
| * @param existingAlgorithm the KDF algorithm to use (defaults to PBEWITHMD5AND256BITAES-CBC-OPENSSL) |
| * @param existingProvider the {@link java.security.Provider} to use (defaults to BC) |
| * @return the encrypted XML content as an InputStream |
| */ |
| private InputStream migrateFlowXmlContent(InputStream flowXmlContent, String existingFlowPassword, String newFlowPassword, String existingAlgorithm = DEFAULT_FLOW_ALGORITHM, String newAlgorithm = DEFAULT_FLOW_ALGORITHM) { |
| File tempFlowXmlFile = new File(getTemporaryFlowXmlFile(outputFlowXmlPath).toString()) |
| final OutputStream flowOutputStream = getFlowOutputStream(tempFlowXmlFile, flowXmlContent instanceof GZIPInputStream) |
| |
| NiFiProperties inputProperties = NiFiProperties.createBasicNiFiProperties("", [ |
| (NiFiProperties.SENSITIVE_PROPS_KEY): existingFlowPassword, |
| (NiFiProperties.SENSITIVE_PROPS_ALGORITHM): existingAlgorithm |
| ]) |
| |
| NiFiProperties outputProperties = NiFiProperties.createBasicNiFiProperties("", [ |
| (NiFiProperties.SENSITIVE_PROPS_KEY): newFlowPassword, |
| (NiFiProperties.SENSITIVE_PROPS_ALGORITHM): newAlgorithm |
| ]) |
| |
| final PropertyEncryptor inputEncryptor = PropertyEncryptorFactory.getPropertyEncryptor(inputProperties) |
| final PropertyEncryptor outputEncryptor = PropertyEncryptorFactory.getPropertyEncryptor(outputProperties) |
| |
| final FlowEncryptor flowEncryptor = new StandardFlowEncryptor() |
| flowEncryptor.processFlow(flowXmlContent, flowOutputStream, inputEncryptor, outputEncryptor) |
| |
| // Overwrite the original flow file with the migrated flow file |
| Files.move(tempFlowXmlFile.toPath(), Paths.get(outputFlowXmlPath), StandardCopyOption.ATOMIC_MOVE) |
| loadFlowXml(outputFlowXmlPath) |
| } |
| |
| private OutputStream getFlowOutputStream(File outputFlowXmlPath, boolean isFileGZipped) { |
| OutputStream flowOutputStream = new FileOutputStream(outputFlowXmlPath) |
| if(isFileGZipped) { |
| flowOutputStream = new GZIPOutputStream(flowOutputStream) |
| } |
| return flowOutputStream |
| } |
| |
| // Create a temporary output file we can write the stream to |
| private Path getTemporaryFlowXmlFile(String originalOutputFlowXmlPath) { |
| String outputFilename = Paths.get(originalOutputFlowXmlPath).getFileName().toString() |
| String migratedFileName = "migrated-${outputFilename}" |
| Paths.get(originalOutputFlowXmlPath).resolveSibling(migratedFileName) |
| } |
| |
| private SensitivePropertyProviderFactory getSensitivePropertyProviderFactory(final String keyHex) { |
| StandardSensitivePropertyProviderFactory.withKeyAndBootstrapSupplier(keyHex, getBootstrapSupplier(bootstrapConfPath)) |
| } |
| |
| String decryptLoginIdentityProviders(String encryptedXml, String existingKeyHex = keyHex) { |
| final SensitivePropertyProviderFactory providerFactory = getSensitivePropertyProviderFactory(existingKeyHex) |
| |
| try { |
| def doc = getXmlSlurper().parseText(encryptedXml) |
| // Find the provider element by class even if it has been renamed |
| def provider = doc.provider.find { it.'class' as String == LDAP_PROVIDER_CLASS } |
| String groupIdentifier = provider.identifier.text() |
| def passwords = provider.property.findAll { |
| it.@name =~ "Password" && it.@encryption != "" |
| } |
| |
| if (passwords.isEmpty()) { |
| if (isVerbose) { |
| logger.info("No encrypted password property elements found in login-identity-providers.xml") |
| } |
| return encryptedXml |
| } |
| |
| passwords.each { password -> |
| final SensitivePropertyProvider sensitivePropertyProvider = providerFactory |
| .getProvider(PropertyProtectionScheme.fromIdentifier((String) password.@encryption)) |
| if (isVerbose) { |
| logger.info("Attempting to decrypt ${password.text()} using protection scheme ${password.@encryption}") |
| } |
| final ProtectedPropertyContext context = getContext(providerFactory, (String) password.@name, groupIdentifier) |
| String decryptedValue = sensitivePropertyProvider.unprotect((String) password.text().trim(), context) |
| password.replaceNode { |
| property(name: password.@name, encryption: "none", decryptedValue) |
| } |
| } |
| |
| // Does not preserve whitespace formatting or comments |
| String updatedXml = XmlUtil.serialize(doc) |
| logger.info("Updated XML content: ${updatedXml}") |
| updatedXml |
| } catch (Exception e) { |
| printUsageAndThrow("Cannot decrypt login identity providers XML content", ExitCode.SERVICE_ERROR) |
| } |
| } |
| |
| String decryptAuthorizers(String encryptedXml, String existingKeyHex = keyHex) { |
| final SensitivePropertyProviderFactory providerFactory = getSensitivePropertyProviderFactory(existingKeyHex) |
| |
| try { |
| def filename = "authorizers.xml" |
| def doc = getXmlSlurper().parseText(encryptedXml) |
| // Find the provider element by class even if it has been renamed |
| def userGroupProvider = doc.userGroupProvider.find { |
| it.'class' as String == LDAP_USER_GROUP_PROVIDER_CLASS |
| } |
| String groupIdentifier = userGroupProvider.identifier.text() |
| def passwords = userGroupProvider.property.findAll { |
| it.@name =~ "Password" && it.@encryption != "" |
| } |
| |
| if (passwords.isEmpty()) { |
| if (isVerbose) { |
| logger.info("No encrypted password property elements found in ${filename}") |
| } |
| return encryptedXml |
| } |
| |
| passwords.each { password -> |
| // TODO: Capture the raw password, and only display it in the log if the decrypted value is different (to avoid possibly printing an incorrectly provided plaintext password) |
| if (isVerbose) { |
| logger.info("Attempting to decrypt ${password.text()} using protection scheme ${password.@encryption}") |
| } |
| final SensitivePropertyProvider sensitivePropertyProvider = providerFactory |
| .getProvider(PropertyProtectionScheme.fromIdentifier((String) password.@encryption)) |
| final ProtectedPropertyContext context = getContext(providerFactory, (String) password.@name, groupIdentifier) |
| String decryptedValue = sensitivePropertyProvider.unprotect((String) password.text().trim(), context) |
| password.replaceNode { |
| property(name: password.@name, encryption: "none", decryptedValue) |
| } |
| } |
| |
| // Does not preserve whitespace formatting or comments |
| String updatedXml = XmlUtil.serialize(doc) |
| if (isVerbose) { |
| logger.info("Updated XML content: ${updatedXml}") |
| } |
| updatedXml |
| } catch (Exception e) { |
| printUsageAndThrow("Cannot decrypt authorizers XML content", ExitCode.SERVICE_ERROR) |
| } |
| } |
| |
| ProtectedPropertyContext getContext(final SensitivePropertyProviderFactory providerFactory, final String propertyName, final String groupIdentifier) { |
| providerFactory.getPropertyContext(groupIdentifier, propertyName); |
| } |
| |
| String encryptLoginIdentityProviders(final String plainXml, final String newKeyHex = keyHex, final PropertyProtectionScheme newProtectionScheme = protectionScheme) { |
| final SensitivePropertyProviderFactory providerFactory = getSensitivePropertyProviderFactory(newKeyHex) |
| |
| // TODO: Switch to XmlParser & XmlNodePrinter to maintain "empty" element structure |
| try { |
| def doc = getXmlSlurper().parseText(plainXml) |
| // Find the provider element by class even if it has been renamed |
| def provider = doc.provider.find { it.'class' as String == LDAP_PROVIDER_CLASS } |
| String groupIdentifier = provider.identifier.text() |
| def passwords = provider.property.findAll { |
| it.@name =~ "Password" && (it.@encryption == "none" || it.@encryption == "") && it.text() |
| } |
| |
| if (passwords.isEmpty()) { |
| if (isVerbose) { |
| logger.info("No unencrypted password property elements found in login-identity-providers.xml") |
| } |
| return plainXml |
| } |
| final SensitivePropertyProvider sensitivePropertyProvider = providerFactory.getProvider(newProtectionScheme) |
| |
| passwords.each { password -> |
| if (isVerbose) { |
| logger.info("Attempting to encrypt ${password.name()} using protection scheme ${sensitivePropertyProvider.identifierKey}") |
| } |
| final ProtectedPropertyContext context = getContext(providerFactory, (String) password.@name, groupIdentifier) |
| String encryptedValue = sensitivePropertyProvider.protect((String) password.text().trim(), context) |
| password.replaceNode { |
| property(name: password.@name, encryption: sensitivePropertyProvider.identifierKey, encryptedValue) |
| } |
| } |
| |
| // Does not preserve whitespace formatting or comments |
| String updatedXml = XmlUtil.serialize(doc) |
| logger.info("Updated XML content: ${updatedXml}") |
| updatedXml |
| } catch (Exception e) { |
| if (isVerbose) { |
| logger.error("Encountered exception", e) |
| } |
| printUsageAndThrow("Cannot encrypt login identity providers XML content", ExitCode.SERVICE_ERROR) |
| } |
| } |
| |
| String encryptAuthorizers(final String plainXml, final String newKeyHex = keyHex, final PropertyProtectionScheme newProtectionScheme = protectionScheme) { |
| final SensitivePropertyProviderFactory providerFactory = getSensitivePropertyProviderFactory(newKeyHex) |
| |
| // TODO: Switch to XmlParser & XmlNodePrinter to maintain "empty" element structure |
| try { |
| def filename = "authorizers.xml" |
| def doc = getXmlSlurper().parseText(plainXml) |
| |
| // Find the provider element by class even if it has been renamed |
| def userGroupProvider = doc.userGroupProvider.find { |
| it.'class' as String == LDAP_USER_GROUP_PROVIDER_CLASS |
| } |
| String groupIdentifier = userGroupProvider.identifier.text() |
| def passwords = userGroupProvider.property.findAll { |
| it.@name =~ "Password" && (it.@encryption == "none" || it.@encryption == "") && it.text() |
| } |
| |
| if (passwords.isEmpty()) { |
| if (isVerbose) { |
| logger.info("No unencrypted password property elements found in ${filename}") |
| } |
| return plainXml |
| } |
| final SensitivePropertyProvider sensitivePropertyProvider = providerFactory.getProvider(newProtectionScheme) |
| |
| passwords.each { password -> |
| if (isVerbose) { |
| logger.info("Attempting to encrypt ${password.name()} using protection scheme ${sensitivePropertyProvider.identifierKey}") |
| } |
| final ProtectedPropertyContext context = getContext(providerFactory, (String) password.@name, groupIdentifier) |
| String encryptedValue = sensitivePropertyProvider.protect((String) password.text().trim(), context) |
| password.replaceNode { |
| property(name: password.@name, encryption: sensitivePropertyProvider.identifierKey, encryptedValue) |
| } |
| } |
| |
| // Does not preserve whitespace formatting or comments |
| String updatedXml = XmlUtil.serialize(doc) |
| if (isVerbose) { |
| logger.info("Updated XML content: ${updatedXml}") |
| } |
| updatedXml |
| } catch (Exception e) { |
| if (isVerbose) { |
| logger.error("Encountered exception", e) |
| } |
| printUsageAndThrow("Cannot encrypt authorizers XML content", ExitCode.SERVICE_ERROR) |
| } |
| } |
| |
| /** |
| * Accepts a {@link NiFiProperties} instance, iterates over all non-empty sensitive properties which are not already marked as protected, encrypts them using the root key, and updates the property with the protected value. Additionally, adds a new sibling property {@code x.y.z.protected=aes/gcm/{128,256}} for each indicating the encryption scheme used. |
| * |
| * @param plainProperties the NiFiProperties instance containing the raw values |
| * @return the NiFiProperties containing protected values |
| */ |
| private NiFiProperties encryptSensitiveProperties(NiFiProperties plainProperties) { |
| if (!plainProperties) { |
| throw new IllegalArgumentException("Cannot encrypt empty NiFiProperties") |
| } |
| |
| ProtectedNiFiProperties protectedWrapper = new ProtectedNiFiProperties(plainProperties) |
| |
| List<String> sensitivePropertyKeys = protectedWrapper.getSensitivePropertyKeys() |
| if (sensitivePropertyKeys.isEmpty()) { |
| logger.info("No sensitive properties to encrypt") |
| return plainProperties |
| } |
| |
| // Holder for encrypted properties and protection schemes |
| Properties encryptedProperties = new Properties() |
| |
| final SensitivePropertyProviderFactory sensitivePropertyProviderFactory = getSensitivePropertyProviderFactory(keyHex) |
| final SensitivePropertyProvider spp = sensitivePropertyProviderFactory.getProvider(protectionScheme) |
| protectedWrapper.addSensitivePropertyProvider(spp) |
| |
| List<String> keysToSkip = [] |
| |
| // Iterate over each -- encrypt and add .protected if populated |
| sensitivePropertyKeys.each { String key -> |
| if (!plainProperties.getProperty(key)) { |
| logger.debug("Skipping encryption of ${key} because it is empty") |
| } else { |
| String protectedValue = spp.protect(plainProperties.getProperty(key), ProtectedPropertyContext.defaultContext(key)) |
| |
| // Add the encrypted value |
| encryptedProperties.setProperty(key, protectedValue) |
| logger.info("Protected ${key} with ${spp.getIdentifierKey()} -> \t${protectedValue}") |
| |
| // Add the protection key ("x.y.z.protected" -> "aes/gcm/{128,256}") |
| String protectionKey = ApplicationPropertiesProtector.getProtectionKey(key) |
| encryptedProperties.setProperty(protectionKey, spp.getIdentifierKey()) |
| logger.info("Updated protection key ${protectionKey}") |
| |
| keysToSkip << key << protectionKey |
| } |
| } |
| |
| // Combine the original raw NiFiProperties and the newly-encrypted properties |
| // Memory-wasteful but NiFiProperties are immutable -- no setter available (unless we monkey-patch...) |
| Set<String> nonSensitiveKeys = plainProperties.getPropertyKeys() - keysToSkip |
| nonSensitiveKeys.each { String key -> |
| encryptedProperties.setProperty(key, plainProperties.getProperty(key)) |
| } |
| NiFiProperties mergedProperties = new NiFiProperties(encryptedProperties) |
| logger.info("Final result: ${mergedProperties.size()} keys including ${ProtectedNiFiProperties.countProtectedProperties(mergedProperties)} protected keys") |
| |
| mergedProperties |
| } |
| |
| /** |
| * Returns the XML fragment serialized from the {@code GPathResult} without the leading XML declaration. |
| * |
| * @param gPathResult the XML node |
| * @return serialized XML without an inserted header declaration |
| */ |
| static String serializeXMLFragment(GPathResult gPathResult) { |
| XmlUtil.serialize(gPathResult).replaceFirst(XML_DECLARATION_REGEX, '') |
| } |
| |
| /** |
| * Reads the existing {@code bootstrap.conf} file, updates it to contain the root key, and persists it back to the same location. |
| * |
| * @throw IOException if there is a problem reading or writing the bootstrap.conf file |
| */ |
| private void writeKeyToBootstrapConf() throws IOException { |
| File bootstrapConfFile |
| if (bootstrapConfPath && (bootstrapConfFile = new File(bootstrapConfPath)).exists() && bootstrapConfFile.canRead() && bootstrapConfFile.canWrite()) { |
| try { |
| List<String> lines = bootstrapConfFile.readLines() |
| |
| updateBootstrapContentsWithKey(lines) |
| |
| // Write the updated values back to the file |
| bootstrapConfFile.text = lines.join("\n") |
| } catch (IOException e) { |
| def msg = "Encountered an exception updating the bootstrap.conf file with the root key" |
| logger.error(msg, e) |
| throw e |
| } |
| } else { |
| throw new IOException("The bootstrap.conf file at ${bootstrapConfPath} must exist and be readable and writable by the user running this tool") |
| } |
| } |
| |
| /** |
| * Accepts the lines of the {@code bootstrap.conf} file as a {@code List <String>} and updates or adds the key property (and associated comment). |
| * |
| * @param lines the lines of the bootstrap file |
| * @return the updated lines |
| */ |
| private List<String> updateBootstrapContentsWithKey(List<String> lines) { |
| String keyLine = "${BOOTSTRAP_KEY_PREFIX}${keyHex}" |
| // Try to locate the key property line |
| int keyLineIndex = lines.findIndexOf { it.startsWith(BOOTSTRAP_KEY_PREFIX) } |
| |
| // If it was found, update inline |
| if (keyLineIndex != -1) { |
| logger.debug("The key property was detected in bootstrap.conf") |
| lines[keyLineIndex] = keyLine |
| logger.debug("The bootstrap key value was updated") |
| |
| // Ensure the comment explaining the property immediately precedes it (check for edge case where key is first line) |
| int keyCommentLineIndex = keyLineIndex > 0 ? keyLineIndex - 1 : 0 |
| if (lines[keyCommentLineIndex] != BOOTSTRAP_KEY_COMMENT) { |
| lines.add(keyCommentLineIndex, BOOTSTRAP_KEY_COMMENT) |
| logger.debug("A comment explaining the bootstrap key property was added") |
| } |
| } else { |
| // If it wasn't present originally, add the comment and key property |
| lines.addAll(["\n", BOOTSTRAP_KEY_COMMENT, keyLine]) |
| logger.debug("The key property was not detected in bootstrap.conf so it was added along with a comment explaining it") |
| } |
| |
| lines |
| } |
| |
| /** |
| * Writes the contents of the login identity providers configuration file with encrypted values to the output {@code login-identity-providers.xml} file. |
| * |
| * @throw IOException if there is a problem reading or writing the login-identity-providers.xml file |
| */ |
| private void writeLoginIdentityProviders() throws IOException { |
| if (!outputLoginIdentityProvidersPath) { |
| throw new IllegalArgumentException("Cannot write encrypted properties to empty login-identity-providers.xml path") |
| } |
| |
| File outputLoginIdentityProvidersFile = new File(outputLoginIdentityProvidersPath) |
| |
| if (isSafeToWrite(outputLoginIdentityProvidersFile)) { |
| try { |
| String updatedXmlContent |
| File loginIdentityProvidersFile = new File(loginIdentityProvidersPath) |
| if (loginIdentityProvidersFile.exists() && loginIdentityProvidersFile.canRead()) { |
| // Instead of just writing the XML content to a file, this method attempts to maintain the structure of the original file and preserves comments |
| updatedXmlContent = serializeLoginIdentityProvidersAndPreserveFormat(loginIdentityProviders, loginIdentityProvidersFile).join("\n") |
| } else { |
| updatedXmlContent = loginIdentityProviders |
| } |
| |
| // Write the updated values back to the file |
| outputLoginIdentityProvidersFile.text = updatedXmlContent |
| } catch (IOException e) { |
| def msg = "Encountered an exception updating the login-identity-providers.xml file with the encrypted values" |
| logger.error(msg, e) |
| throw e |
| } |
| } else { |
| throw new IOException("The login-identity-providers.xml file at ${outputLoginIdentityProvidersPath} must be writable by the user running this tool") |
| } |
| } |
| |
| /** |
| * Writes the contents of the authorizers configuration file with encrypted values to the output {@code authorizers.xml} file. |
| * |
| * @throw IOException if there is a problem reading or writing the authorizers.xml file |
| */ |
| private void writeAuthorizers() throws IOException { |
| if (!outputAuthorizersPath) { |
| throw new IllegalArgumentException("Cannot write encrypted properties to empty authorizers.xml path") |
| } |
| |
| File outputAuthorizersFile = new File(outputAuthorizersPath) |
| |
| if (isSafeToWrite(outputAuthorizersFile)) { |
| try { |
| String updatedXmlContent |
| File authorizersFile = new File(authorizersPath) |
| if (authorizersFile.exists() && authorizersFile.canRead()) { |
| // Instead of just writing the XML content to a file, this method attempts to maintain the structure of the original file and preserves comments |
| updatedXmlContent = serializeAuthorizersAndPreserveFormat(authorizers, authorizersFile).join("\n") |
| } else { |
| updatedXmlContent = authorizers |
| } |
| |
| // Write the updated values back to the file |
| outputAuthorizersFile.text = updatedXmlContent |
| } catch (IOException e) { |
| def msg = "Encountered an exception updating the authorizers.xml file with the encrypted values" |
| logger.error(msg, e) |
| throw e |
| } |
| } else { |
| throw new IOException("The authorizers.xml file at ${outputAuthorizersPath} must be writable by the user running this tool") |
| } |
| } |
| |
| /** |
| * Writes the contents of the {@link NiFiProperties} instance with encrypted values to the output {@code nifi.properties} file. |
| * |
| * @throw IOException if there is a problem reading or writing the nifi.properties file |
| */ |
| private void writeNiFiProperties() throws IOException { |
| if (!outputNiFiPropertiesPath) { |
| throw new IllegalArgumentException("Cannot write encrypted properties to empty nifi.properties path") |
| } |
| |
| File outputNiFiPropertiesFile = new File(outputNiFiPropertiesPath) |
| |
| if (isSafeToWrite(outputNiFiPropertiesFile)) { |
| try { |
| List<String> linesToPersist |
| File niFiPropertiesFile = new File(niFiPropertiesPath) |
| if (niFiPropertiesFile.exists() && niFiPropertiesFile.canRead()) { |
| // Instead of just writing the NiFiProperties instance to a properties file, this method attempts to maintain the structure of the original file and preserves comments |
| linesToPersist = serializeNiFiPropertiesAndPreserveFormat(niFiProperties, niFiPropertiesFile) |
| } else { |
| linesToPersist = serializeNiFiProperties(niFiProperties) |
| } |
| |
| // Write the updated values back to the file |
| outputNiFiPropertiesFile.text = linesToPersist.join("\n") |
| } catch (IOException e) { |
| def msg = "Encountered an exception updating the nifi.properties file with the encrypted values" |
| logger.error(msg, e) |
| throw e |
| } |
| } else { |
| throw new IOException("The nifi.properties file at ${outputNiFiPropertiesPath} must be writable by the user running this tool") |
| } |
| } |
| |
| private |
| static List<String> serializeNiFiPropertiesAndPreserveFormat(NiFiProperties niFiProperties, File originalPropertiesFile) { |
| List<String> lines = originalPropertiesFile.readLines() |
| |
| ProtectedNiFiProperties protectedNiFiProperties = new ProtectedNiFiProperties(niFiProperties) |
| // Only need to replace the keys that have been protected AND nifi.sensitive.props.key |
| Map<String, String> protectedKeys = protectedNiFiProperties.getProtectedPropertyKeys() |
| if (!protectedKeys.containsKey(NiFiProperties.SENSITIVE_PROPS_KEY)) { |
| protectedKeys.put(NiFiProperties.SENSITIVE_PROPS_KEY, protectedNiFiProperties.getProperty(ApplicationPropertiesProtector |
| .getProtectionKey(NiFiProperties.SENSITIVE_PROPS_KEY))) |
| } |
| |
| protectedKeys.each { String key, String protectionScheme -> |
| int l = lines.findIndexOf { it.startsWith(key) } |
| if (l != -1) { |
| lines[l] = "${key}=${protectedNiFiProperties.getProperty(key)}" |
| } |
| // Get the index of the following line (or cap at max) |
| int p = l + 1 > lines.size() ? lines.size() : l + 1 |
| String protectionLine = "${ApplicationPropertiesProtector.getProtectionKey(key)}=${protectionScheme ?: ""}" |
| if (p < lines.size() && lines.get(p).startsWith("${ApplicationPropertiesProtector.getProtectionKey(key)}=")) { |
| lines.set(p, protectionLine) |
| } else { |
| lines.add(p, protectionLine) |
| } |
| } |
| |
| lines |
| } |
| |
| private static List<String> serializeNiFiProperties(NiFiProperties nifiProperties) { |
| OutputStream out = new ByteArrayOutputStream() |
| Writer writer = new GroovyPrintWriter(out) |
| |
| // Again, waste of memory, but respecting the interface |
| Properties properties = new Properties() |
| nifiProperties.getPropertyKeys().each { String key -> |
| properties.setProperty(key, nifiProperties.getProperty(key)) |
| } |
| |
| properties.store(writer, null) |
| writer.flush() |
| out.toString().split("\n") |
| } |
| |
| static List<String> serializeLoginIdentityProvidersAndPreserveFormat(String xmlContent, File originalLoginIdentityProvidersFile) { |
| // Find the provider element of the new XML in the file contents |
| String fileContents = originalLoginIdentityProvidersFile.text |
| try { |
| def parsedXml = getXmlSlurper().parseText(xmlContent) |
| def provider = parsedXml.provider.find { it.'class' as String == LDAP_PROVIDER_CLASS } |
| if (provider) { |
| def serializedProvider = serializeXMLFragment(provider) |
| fileContents = fileContents.replaceFirst(LDAP_PROVIDER_REGEX, Matcher.quoteReplacement(serializedProvider)) |
| return fileContents.split("\n") |
| } else { |
| throw new SAXException("No ldap-provider element found") |
| } |
| } catch (SAXException e) { |
| logger.error("No provider element with class {} found in XML content; " + |
| "the file could be empty or the element may be missing or commented out", LDAP_PROVIDER_CLASS) |
| return fileContents.split("\n") |
| } |
| } |
| |
| static List<String> serializeAuthorizersAndPreserveFormat(String xmlContent, File originalAuthorizersFile) { |
| // Find the provider element of the new XML in the file contents |
| String fileContents = originalAuthorizersFile.text |
| try { |
| def parsedXml = getXmlSlurper().parseText(xmlContent) |
| def provider = parsedXml.userGroupProvider.find { it.'class' as String == LDAP_USER_GROUP_PROVIDER_CLASS } |
| if (provider) { |
| def serializedProvider = serializeXMLFragment(provider) |
| fileContents = fileContents.replaceFirst(LDAP_USER_GROUP_PROVIDER_REGEX, Matcher.quoteReplacement(serializedProvider)) |
| return fileContents.split("\n") |
| } else { |
| throw new SAXException("No ldap-user-group-provider element found") |
| } |
| } catch (SAXException e) { |
| logger.error("No provider element with class {} found in XML content; " + |
| "the file could be empty or the element may be missing or commented out", LDAP_USER_GROUP_PROVIDER_CLASS) |
| return fileContents.split("\n") |
| } |
| } |
| |
| /** |
| * Helper method which returns true if it is "safe" to write to the provided file. |
| * |
| * Conditions: |
| * file does not exist and the parent directory is writable |
| * -OR- |
| * file exists and is writable |
| * |
| * @param fileToWrite the proposed file to be written to |
| * @return true if the caller can "safely" write to this file location |
| */ |
| private static boolean isSafeToWrite(File fileToWrite) { |
| fileToWrite && ((!fileToWrite.exists() && fileToWrite.absoluteFile.parentFile.canWrite()) || (fileToWrite.exists() && fileToWrite.canWrite())) |
| } |
| |
| private static String deriveKeyFromPassword(String password) { |
| password = password?.trim() |
| if (!password || password.length() < MIN_PASSWORD_LENGTH) { |
| throw new KeyException("Cannot derive key from empty/short password -- password must be at least ${MIN_PASSWORD_LENGTH} characters") |
| } |
| |
| // Generate a 128 bit salt |
| byte[] salt = generateScryptSaltForKeyDerivation() |
| int keyLengthInBytes = getValidKeyLengths().max() / 8 |
| byte[] derivedKeyBytes = SCrypt.generate(password.getBytes(StandardCharsets.UTF_8), salt, SCRYPT_N, SCRYPT_R, SCRYPT_P, keyLengthInBytes) |
| Hex.encodeHexString(derivedKeyBytes).toUpperCase() |
| } |
| |
| /** |
| * Returns a static "raw" salt (the 128 bits of random data used when generating the hash, not the "complete" {@code $s0$e0101$ABCDEFGHIJKLMNOPQRSTUV} salt format). |
| * @return the raw salt in byte[] form |
| */ |
| private static byte[] generateScryptSaltForKeyDerivation() { |
| // byte[] salt = new byte[16] |
| // new SecureRandom().nextBytes(salt) |
| // salt |
| /* It is not ideal to use a static salt, but the KDF operation must be deterministic |
| for a given password, and storing and retrieving the salt in bootstrap.conf causes |
| compatibility concerns |
| */ |
| "NIFI_SCRYPT_SALT".getBytes(StandardCharsets.UTF_8) |
| } |
| |
| private String getExistingFlowPassword() { |
| return niFiProperties.getProperty(NiFiProperties.SENSITIVE_PROPS_KEY) as String ?: DEFAULT_NIFI_SENSITIVE_PROPS_KEY |
| } |
| |
| /** |
| * Utility method which returns true if the {@link org.apache.nifi.util.NiFiProperties} instance has encrypted properties. |
| * |
| * @return true if the properties instance will require a key to access |
| */ |
| boolean niFiPropertiesAreEncrypted() { |
| if (niFiPropertiesPath) { |
| try { |
| def nfp = getNiFiPropertiesLoader(keyHex).readProtectedPropertiesFromDisk(new File(niFiPropertiesPath)) |
| return nfp.hasProtectedKeys() |
| } catch (SensitivePropertyProtectionException | IOException e) { |
| return true |
| } |
| } else { |
| return false |
| } |
| } |
| |
| /** |
| * Returns an {@link XmlSlurper} which is configured to maintain ignorable whitespace. |
| * |
| * @return a configured XmlSlurper |
| */ |
| static XmlSlurper getXmlSlurper() { |
| XmlSlurper xs = new XmlSlurper() |
| xs.setKeepIgnorableWhitespace(true) |
| xs |
| } |
| |
| /** |
| * Runs main tool logic (parsing arguments, reading files, protecting properties, and writing key and properties out to destination files). |
| * |
| * @param args the command-line arguments |
| */ |
| static void main(String[] args) { |
| Security.addProvider(new BouncyCastleProvider()) |
| |
| ConfigEncryptionTool tool = new ConfigEncryptionTool() |
| |
| try { |
| try { |
| tool.parse(args) |
| |
| // Handle the translate CLI case |
| if (tool.translatingCli) { |
| if (tool.bootstrapConfPath) { |
| // Check to see if bootstrap.conf has a root key |
| tool.keyHex = NiFiBootstrapUtils.extractKeyFromBootstrapFile(tool.bootstrapConfPath) |
| } |
| |
| if (!tool.keyHex) { |
| logger.info("No root key detected in ${tool.bootstrapConfPath} -- if ${tool.niFiPropertiesPath} is encrypted, the translation will fail") |
| } |
| |
| // Load the existing properties (decrypting if necessary) |
| tool.niFiProperties = tool.loadNiFiProperties(tool.keyHex) |
| |
| String cliOutput = tool.translateNiFiPropertiesToCLI() |
| |
| System.out.println(cliOutput) |
| System.exit(ExitCode.SUCCESS.ordinal()) |
| } |
| |
| boolean existingNiFiPropertiesAreEncrypted = tool.niFiPropertiesAreEncrypted() |
| if (!tool.ignorePropertiesFiles || (tool.handlingFlowXml && existingNiFiPropertiesAreEncrypted)) { |
| // If we are handling the flow.xml.gz and nifi.properties is already encrypted, try getting the key from bootstrap.conf rather than the console |
| if (tool.ignorePropertiesFiles) { |
| tool.keyHex = NiFiBootstrapUtils.extractKeyFromBootstrapFile(tool.bootstrapConfPath) |
| } else { |
| tool.keyHex = tool.getKey() |
| } |
| |
| if (!tool.keyHex) { |
| tool.printUsageAndThrow("Hex key must be provided", ExitCode.INVALID_ARGS) |
| } |
| |
| try { |
| // Validate the length and format |
| tool.keyHex = parseKey(tool.keyHex) |
| } catch (KeyException e) { |
| if (tool.isVerbose) { |
| logger.error("Encountered an error", e) |
| } |
| tool.printUsageAndThrow(e.getMessage(), ExitCode.INVALID_ARGS) |
| } |
| |
| if (tool.migration && tool.migrationProtectionScheme.requiresSecretKey()) { |
| String migrationKeyHex = tool.getMigrationKey() |
| |
| if (!migrationKeyHex) { |
| tool.printUsageAndThrow("Original hex key must be provided for migration", ExitCode.INVALID_ARGS) |
| } |
| |
| try { |
| // Validate the length and format |
| tool.migrationKeyHex = parseKey(migrationKeyHex) |
| } catch (KeyException e) { |
| if (tool.isVerbose) { |
| logger.error("Encountered an error", e) |
| } |
| tool.printUsageAndThrow(e.getMessage(), ExitCode.INVALID_ARGS) |
| } |
| } |
| } |
| String existingKeyHex = tool.migrationKeyHex ?: tool.keyHex |
| |
| // Load NiFiProperties for either scenario; only encrypt if "handling" (see after flow XML) |
| if (tool.handlingNiFiProperties || tool.handlingFlowXml) { |
| try { |
| tool.niFiProperties = tool.loadNiFiProperties(existingKeyHex) |
| } catch (Exception e) { |
| tool.printUsageAndThrow("Cannot migrate key if no previous encryption occurred", ExitCode.ERROR_READING_NIFI_PROPERTIES) |
| } |
| } |
| |
| if (tool.handlingLoginIdentityProviders) { |
| try { |
| tool.loginIdentityProviders = tool.loadLoginIdentityProviders(existingKeyHex) |
| } catch (Exception e) { |
| tool.printUsageAndThrow("Cannot migrate key if no previous encryption occurred", ExitCode.ERROR_INCORRECT_NUMBER_OF_PASSWORDS) |
| } |
| tool.loginIdentityProviders = tool.encryptLoginIdentityProviders(tool.loginIdentityProviders) |
| } |
| |
| if (tool.handlingAuthorizers) { |
| try { |
| tool.authorizers = tool.loadAuthorizers(existingKeyHex) |
| } catch (Exception e) { |
| tool.printUsageAndThrow("Cannot migrate key if no previous encryption occurred", ExitCode.ERROR_INCORRECT_NUMBER_OF_PASSWORDS) |
| } |
| tool.authorizers = tool.encryptAuthorizers(tool.authorizers) |
| } |
| |
| if (tool.handlingFlowXml) { |
| try { |
| tool.flowXmlInputStream = tool.loadFlowXml(flowXmlPath) |
| } catch (Exception e) { |
| if (tool.isVerbose) { |
| logger.error("Encountered an error: ", e) |
| } |
| tool.printUsageAndThrow("Cannot load flow.xml.gz", ExitCode.ERROR_READING_NIFI_PROPERTIES) |
| } |
| } |
| |
| if (tool.handlingNiFiProperties) { |
| // If the flow password was not set in nifi.properties, use the hard-coded default |
| tool.existingFlowPropertiesPassword = tool.getExistingFlowPassword() |
| |
| tool.niFiProperties = tool.encryptSensitiveProperties(tool.niFiProperties) |
| } |
| } catch (CommandLineParseException e) { |
| if (e.exitCode == ExitCode.HELP) { |
| System.exit(ExitCode.HELP.ordinal()) |
| } |
| throw e |
| } catch (Exception e) { |
| if (tool.isVerbose) { |
| logger.error("Encountered an error", e) |
| } |
| tool.printUsageAndThrow(e.message, ExitCode.ERROR_PARSING_COMMAND_LINE) |
| } |
| |
| try { |
| // Do this as part of a transaction? |
| synchronized (this) { |
| if (!tool.ignorePropertiesFiles) { |
| tool.writeKeyToBootstrapConf() |
| } |
| if (tool.handlingFlowXml) { |
| tool.handleFlowXml(tool.niFiPropertiesAreEncrypted()) |
| } |
| if (tool.handlingNiFiProperties || tool.handlingFlowXml) { |
| tool.writeNiFiProperties() |
| } |
| if (tool.handlingLoginIdentityProviders) { |
| tool.writeLoginIdentityProviders() |
| } |
| if (tool.handlingAuthorizers) { |
| tool.writeAuthorizers() |
| } |
| } |
| } catch (Exception e) { |
| if (tool.isVerbose) { |
| logger.error("Encountered an error", e) |
| } |
| tool.printUsageAndThrow("Encountered an error writing the root key to the bootstrap.conf file and the encrypted properties to nifi.properties", ExitCode.ERROR_GENERATING_CONFIG) |
| } |
| } catch (CommandLineParseException e) { |
| System.exit(e.exitCode.ordinal()) |
| } |
| |
| System.exit(ExitCode.SUCCESS.ordinal()) |
| } |
| |
| void handleFlowXml(boolean existingNiFiPropertiesAreEncrypted = false) { |
| String existingFlowPassword = existingFlowPropertiesPassword ?: getExistingFlowPassword() |
| |
| // If the new password was not provided in the arguments, read from the console. If that is empty, use the same value (essentially a copy no-op) |
| String newFlowPassword = flowPropertiesPassword ?: getFlowPassword() |
| if (!newFlowPassword) { |
| newFlowPassword = existingFlowPassword |
| } |
| |
| // Get the algorithms and providers |
| NiFiProperties nfp = niFiProperties |
| String existingAlgorithm = nfp?.getProperty(NiFiProperties.SENSITIVE_PROPS_ALGORITHM) ?: DEFAULT_FLOW_ALGORITHM |
| |
| String newAlgorithm = newFlowAlgorithm ?: existingAlgorithm |
| |
| try { |
| logger.info("Migrating flow.xml file at ${flowXmlPath}. This could take a while if the flow XML is very large.") |
| migrateFlowXmlContent(flowXmlInputStream, existingFlowPassword, newFlowPassword, existingAlgorithm, newAlgorithm) |
| } catch (Exception e) { |
| logger.error("Encountered an error: ${e.getLocalizedMessage()}") |
| if (e instanceof BadPaddingException) { |
| logger.error("This error is likely caused by providing the wrong existing flow password. Check that the existing flow password [-p] is the one used to encrypt the provided flow.xml.gz file") |
| } |
| if (isVerbose) { |
| logger.error("Exception: ", e) |
| } |
| printUsageAndThrow("Encountered an error migrating flow content", ExitCode.ERROR_MIGRATING_FLOW) |
| } |
| |
| // If the new key is the hard-coded internal value, don't persist it to nifi.properties |
| if (newFlowPassword != DEFAULT_NIFI_SENSITIVE_PROPS_KEY && newFlowPassword != existingFlowPassword) { |
| // Update the NiFiProperties object with the new flow password before it gets encrypted (wasteful, but NiFiProperties instances are immutable) |
| Properties rawProperties = new Properties() |
| nfp.getPropertyKeys().each { String k -> |
| rawProperties.put(k, nfp.getProperty(k)) |
| } |
| |
| // If the tool is supposed to encrypt NiFiProperties or the existing file is already encrypted, encrypt and update the new sensitive props key |
| if (handlingNiFiProperties || existingNiFiPropertiesAreEncrypted) { |
| final SensitivePropertyProviderFactory sensitivePropertyProviderFactory = getSensitivePropertyProviderFactory(keyHex) |
| SensitivePropertyProvider spp = sensitivePropertyProviderFactory.getProvider(protectionScheme) |
| String encryptedSPK = spp.protect(newFlowPassword, ProtectedPropertyContext.defaultContext(NiFiProperties.SENSITIVE_PROPS_KEY)) |
| rawProperties.put(NiFiProperties.SENSITIVE_PROPS_KEY, encryptedSPK) |
| // Manually update the protection scheme or it will be lost |
| rawProperties.put(ApplicationPropertiesProtector.getProtectionKey(NiFiProperties.SENSITIVE_PROPS_KEY), spp.getIdentifierKey()) |
| if (isVerbose) { |
| logger.info("Tool is not configured to encrypt nifi.properties, but the existing nifi.properties is encrypted and flow.xml.gz was migrated, so manually persisting the new encrypted value to nifi.properties") |
| } |
| } else { |
| rawProperties.put(NiFiProperties.SENSITIVE_PROPS_KEY, newFlowPassword) |
| rawProperties.put(ApplicationPropertiesProtector.getProtectionKey(NiFiProperties.SENSITIVE_PROPS_KEY), "") |
| } |
| niFiProperties = new NiFiProperties(rawProperties) |
| } |
| } |
| |
| String translateNiFiPropertiesToCLI() { |
| // Assemble the baseUrl |
| String baseUrl = determineBaseUrl(niFiProperties) |
| |
| // Copy the relevant properties to a Map using the "CLI" keys |
| List<String> cliOutput = ["baseUrl=${baseUrl}"] |
| PROPERTY_KEY_MAP.each { String nfpKey, String cliKey -> |
| cliOutput << "${cliKey}=${niFiProperties.getProperty(nfpKey)}" |
| } |
| |
| cliOutput << "proxiedEntity=" |
| |
| cliOutput.join("\n") |
| } |
| |
| static Supplier<BootstrapProperties> getBootstrapSupplier(final String bootstrapConfPath) { |
| new Supplier<BootstrapProperties>() { |
| @Override |
| BootstrapProperties get() { |
| try { |
| NiFiBootstrapUtils.loadBootstrapProperties(bootstrapConfPath) |
| } catch (final IOException e) { |
| logger.warn("Could not load default bootstrap.conf: " + e.getMessage()) |
| return BootstrapProperties.EMPTY |
| } |
| } |
| } |
| } |
| |
| static String determineBaseUrl(NiFiProperties niFiProperties) { |
| String protocol = niFiProperties.isHTTPSConfigured() ? "https" : "http" |
| String host = niFiProperties.isHTTPSConfigured() ? niFiProperties.getProperty(NiFiProperties.WEB_HTTPS_HOST) : niFiProperties.getProperty(NiFiProperties.WEB_HTTP_HOST) |
| String port = niFiProperties.getConfiguredHttpOrHttpsPort() |
| |
| "${protocol}://${host}:${port}" |
| } |
| } |