This is an automated email from the ASF dual-hosted git repository. szaszm pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/nifi-minifi-cpp.git
commit 40bc3b16ad8516fa83e13eb826415600daabca03 Author: Ferenc Gerlits <fgerl...@gmail.com> AuthorDate: Thu Apr 11 18:40:57 2024 +0200 MINIFICPP-2282 Support re-encryption of sensitive properties Added a new --re-encrypt option to the flow-config action of the encrypt-config binary, which can decrypt the sensitive properties using the .old key and re-encrypt them with a new key (either supported by the user or auto-generated). Closes #1739 Signed-off-by: Marton Szasz <sza...@apache.org> --- encrypt-config/EncryptConfig.cpp | 26 ++-- encrypt-config/EncryptConfig.h | 5 +- encrypt-config/EncryptConfigMain.cpp | 20 ++- encrypt-config/FlowConfigEncryptor.cpp | 156 +++++++++++++-------- encrypt-config/FlowConfigEncryptor.h | 20 ++- .../include/core/flow/AdaptiveConfiguration.h | 3 +- libminifi/include/core/flow/FlowSerializer.h | 17 ++- libminifi/include/core/json/JsonFlowSerializer.h | 4 +- libminifi/include/core/yaml/YamlFlowSerializer.h | 4 +- .../include/utils/crypto/EncryptionProvider.h | 2 +- libminifi/include/utils/crypto/EncryptionUtils.h | 4 + libminifi/include/utils/crypto/ciphers/XSalsa20.h | 6 +- libminifi/src/core/flow/AdaptiveConfiguration.cpp | 7 +- libminifi/src/core/flow/FlowSerializer.cpp | 52 +++++++ .../src/core/flow/StructuredConfiguration.cpp | 3 + libminifi/src/core/json/JsonFlowSerializer.cpp | 25 ++-- libminifi/src/core/yaml/YamlFlowSerializer.cpp | 23 +-- libminifi/src/utils/crypto/EncryptionUtils.cpp | 14 +- libminifi/test/unit/JsonFlowSerializerTests.cpp | 127 ++++++++++++++--- libminifi/test/unit/YamlFlowSerializerTests.cpp | 121 +++++++++++++--- 20 files changed, 489 insertions(+), 150 deletions(-) diff --git a/encrypt-config/EncryptConfig.cpp b/encrypt-config/EncryptConfig.cpp index a1a3c89c9..6e0b07296 100644 --- a/encrypt-config/EncryptConfig.cpp +++ b/encrypt-config/EncryptConfig.cpp @@ -57,7 +57,7 @@ EncryptConfig::EncryptConfig(const std::string& minifi_home) : minifi_home_(mini std::filesystem::current_path(minifi_home_); } -bool EncryptConfig::isReencrypting() const { +bool EncryptConfig::isReEncrypting() const { encrypt_config::ConfigFile bootstrap_file{std::ifstream{bootstrapFilePath()}}; std::string decryption_key_name = utils::string::join_pack(ENCRYPTION_KEY_PROPERTY_NAME, ".old"); @@ -200,16 +200,20 @@ void EncryptConfig::encryptSensitiveValuesInMinifiProperties() const { } void EncryptConfig::encryptSensitiveValuesInFlowConfig( - const std::optional<std::string>& component_id, const std::optional<std::string>& property_name, const std::optional<std::string>& property_value) const { - if (!component_id && !property_name && !property_value) { - EncryptionKeys keys = getEncryptionKeys(SENSITIVE_PROPERTIES_KEY_PROPERTY_NAME); - flow_config_encryptor::encryptSensitiveValuesInFlowConfig(keys, minifi_home_, flowConfigPath()); - } else if (component_id && property_name && property_value) { - EncryptionKeys keys = getEncryptionKeys(SENSITIVE_PROPERTIES_KEY_PROPERTY_NAME); - flow_config_encryptor::encryptSensitiveValuesInFlowConfig(keys, minifi_home_, flowConfigPath(), *component_id, *property_name, *property_value); - } else { - throw std::runtime_error("either all of --component-id, --property-name and --property-value should be given (for batch mode) or none of them (for interactive mode)"); - } + bool re_encrypt, const std::optional<std::string>& component_id, const std::optional<std::string>& property_name, const std::optional<std::string>& property_value) const { + EncryptionKeys keys = getEncryptionKeys(SENSITIVE_PROPERTIES_KEY_PROPERTY_NAME); + flow_config_encryptor::EncryptionRequest request_type = [&] { + if (re_encrypt) { + return flow_config_encryptor::EncryptionRequest{flow_config_encryptor::EncryptionType::ReEncrypt}; + } else if (!component_id && !property_name && !property_value) { + return flow_config_encryptor::EncryptionRequest{flow_config_encryptor::EncryptionType::Interactive}; + } else if (component_id && property_name && property_value) { + return flow_config_encryptor::EncryptionRequest{*component_id, *property_name, *property_value}; + } else { + throw std::runtime_error("either all of --component-id, --property-name and --property-value should be given (for batch mode) or none of them (for interactive mode)"); + } + }(); + flow_config_encryptor::encryptSensitiveValuesInFlowConfig(keys, minifi_home_, flowConfigPath(), request_type); } } // namespace org::apache::nifi::minifi::encrypt_config diff --git a/encrypt-config/EncryptConfig.h b/encrypt-config/EncryptConfig.h index 1f48f48fb..5d32fa500 100644 --- a/encrypt-config/EncryptConfig.h +++ b/encrypt-config/EncryptConfig.h @@ -28,10 +28,11 @@ class EncryptConfig { explicit EncryptConfig(const std::string& minifi_home); void encryptSensitiveValuesInMinifiProperties() const; - void encryptSensitiveValuesInFlowConfig(const std::optional<std::string>& component_id, const std::optional<std::string>& property_name, const std::optional<std::string>& property_value) const; + void encryptSensitiveValuesInFlowConfig( + bool re_encrypt, const std::optional<std::string>& component_id, const std::optional<std::string>& property_name, const std::optional<std::string>& property_value) const; void encryptWholeFlowConfigFile() const; - [[nodiscard]] bool isReencrypting() const; + [[nodiscard]] bool isReEncrypting() const; private: [[nodiscard]] std::filesystem::path bootstrapFilePath() const; diff --git a/encrypt-config/EncryptConfigMain.cpp b/encrypt-config/EncryptConfigMain.cpp index 1faa91007..a18e8d075 100644 --- a/encrypt-config/EncryptConfigMain.cpp +++ b/encrypt-config/EncryptConfigMain.cpp @@ -48,6 +48,9 @@ int main(int argc, char* argv[]) try { argument_parser.add_argument("--property-value") .metavar("VALUE") .help(minifi::utils::string::join_pack("The new value of the sensitive property (", OPERATION_FLOW_CONFIG, " only)")); + argument_parser.add_argument("--re-encrypt") + .flag() + .help(minifi::utils::string::join_pack("Decrypt all properties with the old key and re-encrypt them with a new key (", OPERATION_FLOW_CONFIG, " only)")); try { argument_parser.parse_args(argc, argv); @@ -57,15 +60,22 @@ int main(int argc, char* argv[]) try { } EncryptConfig encrypt_config{argument_parser.get("-m")}; + std::string operation = argument_parser.get("operation"); + const auto re_encrypt = argument_parser.get<bool>("--re-encrypt"); + const auto component_id = argument_parser.present("--component-id"); + const auto property_name = argument_parser.present("--property-name"); + const auto property_value = argument_parser.present("--property-value"); + + if (operation != OPERATION_FLOW_CONFIG && (re_encrypt || component_id || property_name || property_value)) { + std::cerr << "Unsupported option for operation '" << operation << "'\n\n" << argument_parser; + return 5; + } if (operation == OPERATION_MINIFI_PROPERTIES) { encrypt_config.encryptSensitiveValuesInMinifiProperties(); } else if (operation == OPERATION_FLOW_CONFIG) { - auto component_id = argument_parser.present("--component-id"); - auto property_name = argument_parser.present("--property-name"); - auto property_value = argument_parser.present("--property-value"); - encrypt_config.encryptSensitiveValuesInFlowConfig(component_id, property_name, property_value); + encrypt_config.encryptSensitiveValuesInFlowConfig(re_encrypt, component_id, property_name, property_value); } else if (operation == OPERATION_WHOLE_FLOW_CONFIG_FILE) { encrypt_config.encryptWholeFlowConfigFile(); } else { @@ -73,7 +83,7 @@ int main(int argc, char* argv[]) try { return 4; } - if ((operation == OPERATION_MINIFI_PROPERTIES || operation == OPERATION_WHOLE_FLOW_CONFIG_FILE) && encrypt_config.isReencrypting()) { + if ((operation == OPERATION_MINIFI_PROPERTIES || operation == OPERATION_WHOLE_FLOW_CONFIG_FILE) && encrypt_config.isReEncrypting()) { std::cout << "WARNING: an .old key was provided, which is used for both " << OPERATION_MINIFI_PROPERTIES << " and " << OPERATION_WHOLE_FLOW_CONFIG_FILE << ".\n" << "If both are currently encrypted, make sure to run " << argv[0] << " to re-encrypt both before removing the .old key,\n" << "otherwise you won't be able to recover the encrypted data!\n"; diff --git a/encrypt-config/FlowConfigEncryptor.cpp b/encrypt-config/FlowConfigEncryptor.cpp index aa9983091..e9e788296 100644 --- a/encrypt-config/FlowConfigEncryptor.cpp +++ b/encrypt-config/FlowConfigEncryptor.cpp @@ -30,26 +30,27 @@ namespace minifi = org::apache::nifi::minifi; namespace { -enum class Type { +enum class ComponentType { Processor, ControllerService }; struct SensitiveProperty { - Type type; + ComponentType component_type; minifi::utils::Identifier component_id; std::string component_name; std::string property_name; std::string property_display_name; + std::string property_value; }; } // namespace namespace magic_enum::customize { template<> -constexpr customize_t enum_name<Type>(Type type) noexcept { +constexpr customize_t enum_name<ComponentType>(ComponentType type) noexcept { switch (type) { - case Type::Processor: return "Processor"; - case Type::ControllerService: return "Controller service"; + case ComponentType::Processor: return "Processor"; + case ComponentType::ControllerService: return "Controller service"; } return invalid_tag; } @@ -61,16 +62,17 @@ std::vector<SensitiveProperty> listSensitiveProperties(const minifi::core::Proce std::vector<minifi::core::Processor *> processors; process_group.getAllProcessors(processors); - for (const auto *processor : processors) { + for (const auto* processor : processors) { gsl_Expects(processor); for (const auto& [_, property] : processor->getProperties()) { if (property.isSensitive()) { sensitive_properties.push_back(SensitiveProperty{ - .type = Type::Processor, + .component_type = ComponentType::Processor, .component_id = processor->getUUID(), .component_name = processor->getName(), .property_name = property.getName(), - .property_display_name = property.getDisplayName()}); + .property_display_name = property.getDisplayName(), + .property_value = property.getValue().to_string()}); } } } @@ -82,11 +84,12 @@ std::vector<SensitiveProperty> listSensitiveProperties(const minifi::core::Proce for (const auto& [_, property] : controller_service->getProperties()) { if (property.isSensitive()) { sensitive_properties.push_back(SensitiveProperty{ - .type = Type::ControllerService, + .component_type = ComponentType::ControllerService, .component_id = controller_service->getUUID(), .component_name = controller_service->getName(), .property_name = property.getName(), - .property_display_name = property.getDisplayName()}); + .property_display_name = property.getDisplayName(), + .property_value = property.getValue().to_string()}); } } } @@ -94,81 +97,116 @@ std::vector<SensitiveProperty> listSensitiveProperties(const minifi::core::Proce return sensitive_properties; } -template<typename Func> -void encryptSensitiveValuesInFlowConfigImpl( - const minifi::encrypt_config::EncryptionKeys& keys, const std::filesystem::path& minifi_home, const std::filesystem::path& flow_config_path, Func create_overrides) { - const auto configure = std::make_shared<minifi::Configure>(); +std::unordered_map<minifi::utils::Identifier, minifi::core::flow::Overrides> createOverridesInteractively(const std::vector<SensitiveProperty>& sensitive_properties) { + std::unordered_map<minifi::utils::Identifier, minifi::core::flow::Overrides> overrides; + std::cout << '\n'; + for (const auto& sensitive_property : sensitive_properties) { + std::cout << magic_enum::enum_name(sensitive_property.component_type) << " " << sensitive_property.component_name << " (" << sensitive_property.component_id.to_string() << ") " + << "has sensitive property " << sensitive_property.property_display_name << "\n enter a new value or press Enter to keep the current value unchanged: "; + std::cout.flush(); + std::string new_value; + std::getline(std::cin, new_value); + if (!new_value.empty()) { + overrides[sensitive_property.component_id].add(sensitive_property.property_name, new_value); + } + } + return overrides; +} + +std::unordered_map<minifi::utils::Identifier, minifi::core::flow::Overrides> createOverridesForSingleProperty( + const std::vector<SensitiveProperty>& sensitive_properties, const std::string& component_id, const std::string& property_name, const std::string& property_value) { + const auto sensitive_property_it = std::ranges::find_if(sensitive_properties, [&](const auto& sensitive_property) { + return sensitive_property.component_id.to_string().view() == component_id && (sensitive_property.property_name == property_name || sensitive_property.property_display_name == property_name); + }); + if (sensitive_property_it == sensitive_properties.end()) { + std::cout << "No sensitive property found with this component ID and property name.\n"; + return {}; + } + return {{sensitive_property_it->component_id, minifi::core::flow::Overrides{}.add(sensitive_property_it->property_name, property_value)}}; +} + +std::unordered_map<minifi::utils::Identifier, minifi::core::flow::Overrides> createOverridesForReEncryption(const std::vector<SensitiveProperty>& sensitive_properties) { + std::unordered_map<minifi::utils::Identifier, minifi::core::flow::Overrides> overrides; + for (const auto& sensitive_property : sensitive_properties) { + overrides[sensitive_property.component_id].addOptional(sensitive_property.property_name, sensitive_property.property_value); + } + return overrides; +} + +} // namespace + +namespace org::apache::nifi::minifi::encrypt_config::flow_config_encryptor { + +EncryptionRequest::EncryptionRequest(EncryptionType type) : type{type} { + gsl_Expects(type == EncryptionType::Interactive || type == EncryptionType::ReEncrypt); +} + +EncryptionRequest::EncryptionRequest(std::string_view component_id, std::string_view property_name, std::string_view property_value) + : type{EncryptionType::SingleProperty}, + component_id{component_id}, + property_name{property_name}, + property_value{property_value} {} + +void encryptSensitiveValuesInFlowConfig(const EncryptionKeys& keys, const std::filesystem::path& minifi_home, const std::filesystem::path& flow_config_path, const EncryptionRequest& request) { + const bool is_re_encrypting = keys.old_key.has_value(); + if (is_re_encrypting && request.type != EncryptionType::ReEncrypt) { + throw std::runtime_error("Error: found .old key; please run --re-encrypt and then remove the .old key"); + } + if (!is_re_encrypting && request.type == EncryptionType::ReEncrypt) { + throw std::runtime_error("Error: cannot re-encrypt without an .old key!"); + } + + const auto configure = std::make_shared<Configure>(); configure->setHome(minifi_home); configure->loadConfigureFile(DEFAULT_NIFI_PROPERTIES_FILE); - bool encrypt_whole_flow_config_file = (configure->get(minifi::Configure::nifi_flow_configuration_encrypt) | minifi::utils::andThen(minifi::utils::string::toBool)).value_or(false); - auto encryptor = encrypt_whole_flow_config_file ? minifi::utils::crypto::EncryptionProvider::create(minifi_home) : std::nullopt; - auto filesystem = std::make_shared<minifi::utils::file::FileSystem>(encrypt_whole_flow_config_file, encryptor); + bool encrypt_whole_flow_config_file = (configure->get(Configure::nifi_flow_configuration_encrypt) | utils::andThen(utils::string::toBool)).value_or(false); + auto whole_file_encryptor = encrypt_whole_flow_config_file ? utils::crypto::EncryptionProvider::create(minifi_home) : std::nullopt; + auto filesystem = std::make_shared<utils::file::FileSystem>(encrypt_whole_flow_config_file, whole_file_encryptor); + + auto sensitive_properties_decryptor = is_re_encrypting ? + utils::crypto::EncryptionProvider{utils::crypto::XSalsa20Cipher{*keys.old_key}} : + utils::crypto::EncryptionProvider{utils::crypto::XSalsa20Cipher{keys.encryption_key}}; - minifi::core::extension::ExtensionManager::get().initialize(configure); + core::extension::ExtensionManager::get().initialize(configure); - minifi::core::flow::AdaptiveConfiguration adaptive_configuration{minifi::core::ConfigurationContext{ + core::flow::AdaptiveConfiguration adaptive_configuration{core::ConfigurationContext{ .flow_file_repo = nullptr, .content_repo = nullptr, .configuration = configure, .path = flow_config_path, .filesystem = filesystem, - .sensitive_properties_encryptor = minifi::utils::crypto::EncryptionProvider{minifi::utils::crypto::XSalsa20Cipher{keys.encryption_key}} + .sensitive_properties_encryptor = sensitive_properties_decryptor }}; const auto flow_config_content = filesystem->read(flow_config_path); if (!flow_config_content) { - throw std::runtime_error(minifi::utils::string::join_pack("Could not read the flow configuration file \"", flow_config_path.string(), "\"")); + throw std::runtime_error(utils::string::join_pack("Could not read the flow configuration file \"", flow_config_path.string(), "\"")); } const auto process_group = adaptive_configuration.getRootFromPayload(*flow_config_content); gsl_Expects(process_group); const auto sensitive_properties = listSensitiveProperties(*process_group); - std::unordered_map<minifi::utils::Identifier, std::unordered_map<std::string, std::string>> overrides = create_overrides(sensitive_properties); + const auto overrides = [&]() -> std::unordered_map<utils::Identifier, core::flow::Overrides> { + switch (request.type) { + case EncryptionType::Interactive: return createOverridesInteractively(sensitive_properties); + case EncryptionType::SingleProperty: return createOverridesForSingleProperty(sensitive_properties, request.component_id, request.property_name, request.property_value); + case EncryptionType::ReEncrypt: return createOverridesForReEncryption(sensitive_properties); + } + return {}; + }(); if (overrides.empty()) { + std::cout << "Nothing to do, exiting.\n"; return; } + if (is_re_encrypting) { + adaptive_configuration.setSensitivePropertiesEncryptor(utils::crypto::EncryptionProvider{utils::crypto::XSalsa20Cipher{keys.encryption_key}}); + } + std::string flow_config_str = adaptive_configuration.serializeWithOverrides(*process_group, overrides); adaptive_configuration.persist(flow_config_str); } -} // namespace - -namespace org::apache::nifi::minifi::encrypt_config::flow_config_encryptor { - -void encryptSensitiveValuesInFlowConfig(const EncryptionKeys& keys, const std::filesystem::path& minifi_home, const std::filesystem::path& flow_config_path) { - encryptSensitiveValuesInFlowConfigImpl(keys, minifi_home, flow_config_path, - [](const auto& sensitive_properties) { - std::unordered_map<utils::Identifier, std::unordered_map<std::string, std::string>> overrides; - std::cout << '\n'; - for (const auto& sensitive_property : sensitive_properties) { - std::cout << magic_enum::enum_name(sensitive_property.type) << " " << sensitive_property.component_name << " (" << sensitive_property.component_id.to_string() << ") " - << "has sensitive property " << sensitive_property.property_display_name << "\n enter a new value or press Enter to keep the current value unchanged: "; - std::cout.flush(); - std::string new_value; - std::getline(std::cin, new_value); - if (!new_value.empty()) { - overrides[sensitive_property.component_id].emplace(sensitive_property.property_name, new_value); - } - } - return overrides; - }); -} - -void encryptSensitiveValuesInFlowConfig(const EncryptionKeys& keys, const std::filesystem::path& minifi_home, const std::filesystem::path& flow_config_path, - const std::string& component_id, const std::string& property_name, const std::string& property_value) { - encryptSensitiveValuesInFlowConfigImpl(keys, minifi_home, flow_config_path, - [&](const auto& sensitive_properties) -> std::unordered_map<utils::Identifier, std::unordered_map<std::string, std::string>> { - const auto sensitive_property_it = std::ranges::find_if(sensitive_properties, [&](const auto& sensitive_property) { - return sensitive_property.component_id.to_string() == component_id && (sensitive_property.property_name == property_name || sensitive_property.property_display_name == property_name); - }); - if (sensitive_property_it == sensitive_properties.end()) { - std::cout << "No sensitive property found with this component ID and property name.\n"; - return {}; - } - return {{sensitive_property_it->component_id, {{sensitive_property_it->property_name, property_value}}}}; - }); -} } // namespace org::apache::nifi::minifi::encrypt_config::flow_config_encryptor diff --git a/encrypt-config/FlowConfigEncryptor.h b/encrypt-config/FlowConfigEncryptor.h index b4109581b..4c699c91b 100644 --- a/encrypt-config/FlowConfigEncryptor.h +++ b/encrypt-config/FlowConfigEncryptor.h @@ -24,8 +24,22 @@ namespace org::apache::nifi::minifi::encrypt_config::flow_config_encryptor { -void encryptSensitiveValuesInFlowConfig(const EncryptionKeys& keys, const std::filesystem::path& minifi_home, const std::filesystem::path& flow_config_path); -void encryptSensitiveValuesInFlowConfig(const EncryptionKeys& keys, const std::filesystem::path& minifi_home, const std::filesystem::path& flow_config_path, - const std::string& component_id, const std::string& property_name, const std::string& property_value); +enum class EncryptionType { + Interactive, + SingleProperty, + ReEncrypt +}; + +struct EncryptionRequest { + explicit EncryptionRequest(EncryptionType type); + EncryptionRequest(std::string_view component_id, std::string_view property_name, std::string_view property_value); + + EncryptionType type; + std::string component_id; + std::string property_name; + std::string property_value; +}; + +void encryptSensitiveValuesInFlowConfig(const EncryptionKeys& keys, const std::filesystem::path& minifi_home, const std::filesystem::path& flow_config_path, const EncryptionRequest& request); } // namespace org::apache::nifi::minifi::encrypt_config::flow_config_encryptor diff --git a/libminifi/include/core/flow/AdaptiveConfiguration.h b/libminifi/include/core/flow/AdaptiveConfiguration.h index bc1ae9fbe..fe041ff9c 100644 --- a/libminifi/include/core/flow/AdaptiveConfiguration.h +++ b/libminifi/include/core/flow/AdaptiveConfiguration.h @@ -35,7 +35,8 @@ class AdaptiveConfiguration : public StructuredConfiguration { std::unique_ptr<core::ProcessGroup> getRootFromPayload(const std::string &payload) override; - std::string serializeWithOverrides(const core::ProcessGroup& process_group, const std::unordered_map<utils::Identifier, std::unordered_map<std::string, std::string>>& overrides) const; + void setSensitivePropertiesEncryptor(utils::crypto::EncryptionProvider sensitive_properties_encryptor); + std::string serializeWithOverrides(const core::ProcessGroup& process_group, const std::unordered_map<utils::Identifier, core::flow::Overrides>& overrides) const; }; } // namespace org::apache::nifi::minifi::core::flow diff --git a/libminifi/include/core/flow/FlowSerializer.h b/libminifi/include/core/flow/FlowSerializer.h index 02ec6c1cc..05f374fe8 100644 --- a/libminifi/include/core/flow/FlowSerializer.h +++ b/libminifi/include/core/flow/FlowSerializer.h @@ -26,6 +26,21 @@ namespace org::apache::nifi::minifi::core::flow { +class Overrides { + public: + Overrides& add(std::string_view property_name, std::string_view property_value); + Overrides& addOptional(std::string_view property_name, std::string_view property_value); + [[nodiscard]] std::optional<std::string> get(std::string_view property_name) const; + [[nodiscard]] std::vector<std::pair<std::string, std::string>> getRequired() const; + + private: + struct OverrideItem { + std::string value; + bool is_required; + }; + std::unordered_map<std::string, OverrideItem> overrides_; +}; + class FlowSerializer { public: FlowSerializer() = default; @@ -37,7 +52,7 @@ class FlowSerializer { FlowSerializer& operator=(FlowSerializer&&) = delete; [[nodiscard]] virtual std::string serialize(const core::ProcessGroup& process_group, const FlowSchema& schema, const utils::crypto::EncryptionProvider& encryption_provider, - const std::unordered_map<utils::Identifier, std::unordered_map<std::string, std::string>>& overrides) const = 0; + const std::unordered_map<utils::Identifier, Overrides>& overrides) const = 0; }; } // namespace org::apache::nifi::minifi::core::flow diff --git a/libminifi/include/core/json/JsonFlowSerializer.h b/libminifi/include/core/json/JsonFlowSerializer.h index 1faa95c93..9f3f6377d 100644 --- a/libminifi/include/core/json/JsonFlowSerializer.h +++ b/libminifi/include/core/json/JsonFlowSerializer.h @@ -26,12 +26,12 @@ class JsonFlowSerializer : public core::flow::FlowSerializer { explicit JsonFlowSerializer(rapidjson::Document document) : flow_definition_json_(std::move(document)) {} [[nodiscard]] std::string serialize(const core::ProcessGroup& process_group, const core::flow::FlowSchema& schema, const utils::crypto::EncryptionProvider& encryption_provider, - const std::unordered_map<utils::Identifier, std::unordered_map<std::string, std::string>>& overrides) const override; + const std::unordered_map<utils::Identifier, core::flow::Overrides>& overrides) const override; private: void encryptSensitiveProperties(rapidjson::Value& property_jsons, rapidjson::Document::AllocatorType& alloc, const std::map<std::string, Property>& properties, const utils::crypto::EncryptionProvider& encryption_provider, - std::unordered_map<std::string, std::string> component_overrides) const; + const core::flow::Overrides& overrides) const; rapidjson::Document flow_definition_json_; std::shared_ptr<logging::Logger> logger_{logging::LoggerFactory<JsonFlowSerializer>::getLogger()}; diff --git a/libminifi/include/core/yaml/YamlFlowSerializer.h b/libminifi/include/core/yaml/YamlFlowSerializer.h index 3483b1638..19a16e3d1 100644 --- a/libminifi/include/core/yaml/YamlFlowSerializer.h +++ b/libminifi/include/core/yaml/YamlFlowSerializer.h @@ -26,11 +26,11 @@ class YamlFlowSerializer : public core::flow::FlowSerializer { explicit YamlFlowSerializer(const YAML::Node& flow_definition_yaml) : flow_definition_yaml_(flow_definition_yaml) {} [[nodiscard]] std::string serialize(const core::ProcessGroup& process_group, const core::flow::FlowSchema& schema, const utils::crypto::EncryptionProvider& encryption_provider, - const std::unordered_map<utils::Identifier, std::unordered_map<std::string, std::string>>& overrides) const override; + const std::unordered_map<utils::Identifier, core::flow::Overrides>& overrides) const override; private: void encryptSensitiveProperties(YAML::Node property_yamls, const std::map<std::string, Property>& properties, const utils::crypto::EncryptionProvider& encryption_provider, - std::unordered_map<std::string, std::string> component_overrides) const; + const core::flow::Overrides& overrides) const; YAML::Node flow_definition_yaml_; std::shared_ptr<logging::Logger> logger_{logging::LoggerFactory<YamlFlowSerializer>::getLogger()}; diff --git a/libminifi/include/utils/crypto/EncryptionProvider.h b/libminifi/include/utils/crypto/EncryptionProvider.h index 15e14ccca..b72c38134 100644 --- a/libminifi/include/utils/crypto/EncryptionProvider.h +++ b/libminifi/include/utils/crypto/EncryptionProvider.h @@ -47,7 +47,7 @@ class EncryptionProvider { } private: - const XSalsa20Cipher cipher_impl_; + XSalsa20Cipher cipher_impl_; }; } // namespace org::apache::nifi::minifi::utils::crypto diff --git a/libminifi/include/utils/crypto/EncryptionUtils.h b/libminifi/include/utils/crypto/EncryptionUtils.h index 2349ce2ee..66b95cd17 100644 --- a/libminifi/include/utils/crypto/EncryptionUtils.h +++ b/libminifi/include/utils/crypto/EncryptionUtils.h @@ -47,6 +47,10 @@ struct EncryptedData { Bytes ciphertext_plus_mac; }; +struct EncryptionError : public std::runtime_error { + using runtime_error::runtime_error; +}; + /** * Encrypt the input (raw version). * diff --git a/libminifi/include/utils/crypto/ciphers/XSalsa20.h b/libminifi/include/utils/crypto/ciphers/XSalsa20.h index d5727671c..5ce17d14d 100644 --- a/libminifi/include/utils/crypto/ciphers/XSalsa20.h +++ b/libminifi/include/utils/crypto/ciphers/XSalsa20.h @@ -32,16 +32,16 @@ class XSalsa20Cipher { return utils::crypto::generateKey(); } - std::string encrypt(std::string_view data) const { + [[nodiscard]] std::string encrypt(std::string_view data) const { return utils::crypto::encrypt(data, encryption_key_); } - std::string decrypt(std::string_view data) const { + [[nodiscard]] std::string decrypt(std::string_view data) const { return utils::crypto::decrypt(data, encryption_key_); } private: - const Bytes encryption_key_; + Bytes encryption_key_; }; } // namespace org::apache::nifi::minifi::utils::crypto diff --git a/libminifi/src/core/flow/AdaptiveConfiguration.cpp b/libminifi/src/core/flow/AdaptiveConfiguration.cpp index 6eac01fbb..85c590a38 100644 --- a/libminifi/src/core/flow/AdaptiveConfiguration.cpp +++ b/libminifi/src/core/flow/AdaptiveConfiguration.cpp @@ -71,8 +71,11 @@ std::unique_ptr<core::ProcessGroup> AdaptiveConfiguration::getRootFromPayload(co } } -std::string AdaptiveConfiguration::serializeWithOverrides(const core::ProcessGroup& process_group, - const std::unordered_map<utils::Identifier, std::unordered_map<std::string, std::string>>& overrides) const { +void AdaptiveConfiguration::setSensitivePropertiesEncryptor(utils::crypto::EncryptionProvider sensitive_properties_encryptor) { + sensitive_properties_encryptor_ = std::move(sensitive_properties_encryptor); +} + +std::string AdaptiveConfiguration::serializeWithOverrides(const core::ProcessGroup& process_group, const std::unordered_map<utils::Identifier, core::flow::Overrides>& overrides) const { gsl_Expects(flow_serializer_); return flow_serializer_->serialize(process_group, schema_, sensitive_properties_encryptor_, overrides); } diff --git a/libminifi/src/core/flow/FlowSerializer.cpp b/libminifi/src/core/flow/FlowSerializer.cpp new file mode 100644 index 000000000..a55115ebc --- /dev/null +++ b/libminifi/src/core/flow/FlowSerializer.cpp @@ -0,0 +1,52 @@ +/** + * 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. + */ + +#include "core/flow/FlowSerializer.h" +#include "utils/OptionalUtils.h" + +namespace org::apache::nifi::minifi::core::flow { + +Overrides& Overrides::add(std::string_view property_name, std::string_view property_value) { + overrides_.emplace(property_name, OverrideItem{.value = std::string{property_value}, .is_required = true}); + return *this; +} + +Overrides& Overrides::addOptional(std::string_view property_name, std::string_view property_value) { + overrides_.emplace(property_name, OverrideItem{.value = std::string{property_value}, .is_required = false}); + return *this; +} + +std::optional<std::string> Overrides::get(std::string_view property_name) const { + const auto it = overrides_.find(std::string{property_name}); + if (it != overrides_.end()) { + return {it->second.value}; + } else { + return std::nullopt; + } +} + +std::vector<std::pair<std::string, std::string>> Overrides::getRequired() const { + std::vector<std::pair<std::string, std::string>> required_overrides; + for (const auto& [name, override_item] : overrides_) { + if (override_item.is_required) { + required_overrides.emplace_back(name, override_item.value); + } + } + return required_overrides; +} + +} // namespace org::apache::nifi::minifi::core::flow diff --git a/libminifi/src/core/flow/StructuredConfiguration.cpp b/libminifi/src/core/flow/StructuredConfiguration.cpp index df79c9b6d..8bcabf368 100644 --- a/libminifi/src/core/flow/StructuredConfiguration.cpp +++ b/libminifi/src/core/flow/StructuredConfiguration.cpp @@ -673,6 +673,9 @@ PropertyValue StructuredConfiguration::getValidatedProcessorPropertyForDefaultTy coercedValue = property_value_node.getScalarAsString().value(); } return coercedValue; + } catch (const utils::crypto::EncryptionError& e) { + logger_->log_error("Fetching property failed with a decryption error: {}", e.what()); + throw; } catch (const std::exception& e) { logger_->log_error("Fetching property failed with an exception of {}", e.what()); logger_->log_error("Invalid conversion for field {}. Value {}", property_from_processor.getName(), property_value_node.getDebugString()); diff --git a/libminifi/src/core/json/JsonFlowSerializer.cpp b/libminifi/src/core/json/JsonFlowSerializer.cpp index 82d6d961c..a62982a05 100644 --- a/libminifi/src/core/json/JsonFlowSerializer.cpp +++ b/libminifi/src/core/json/JsonFlowSerializer.cpp @@ -17,6 +17,8 @@ #include "core/json/JsonFlowSerializer.h" +#include <unordered_set> + #include "rapidjson/prettywriter.h" #include "utils/crypto/property_encryption/PropertyEncryptionUtils.h" @@ -37,8 +39,11 @@ rapidjson::Value& getMember(rapidjson::Value& node, const std::string& member_na } } -void JsonFlowSerializer::encryptSensitiveProperties(rapidjson::Value &property_jsons, rapidjson::Document::AllocatorType &alloc, - const std::map<std::string, Property> &properties, const utils::crypto::EncryptionProvider &encryption_provider, std::unordered_map<std::string, std::string> component_overrides) const { +void JsonFlowSerializer::encryptSensitiveProperties(rapidjson::Value& property_jsons, rapidjson::Document::AllocatorType& alloc, + const std::map<std::string, Property>& properties, const utils::crypto::EncryptionProvider& encryption_provider, + const core::flow::Overrides& overrides) const { + std::unordered_set<std::string> processed_property_names; + for (auto &property : property_jsons.GetObject()) { const std::string name{property.name.GetString(), property.name.GetStringLength()}; if (!properties.contains(name)) { @@ -47,22 +52,24 @@ void JsonFlowSerializer::encryptSensitiveProperties(rapidjson::Value &property_j } if (properties.at(name).isSensitive()) { auto& value = property.value; - const std::string_view value_sv = component_overrides.contains(name) ? component_overrides.at(name) : std::string_view{value.GetString(), value.GetStringLength()}; + const auto override_value = overrides.get(name); + const std::string_view value_sv = override_value ? *override_value : std::string_view{value.GetString(), value.GetStringLength()}; const std::string encrypted_value = utils::crypto::property_encryption::encrypt(value_sv, encryption_provider); value.SetString(encrypted_value.c_str(), encrypted_value.size(), alloc); } - component_overrides.erase(name); + processed_property_names.insert(name); } - for (const auto& [name, value] : component_overrides) { + for (const auto& [name, value] : overrides.getRequired()) { gsl_Expects(properties.contains(name) && properties.at(name).isSensitive()); + if (processed_property_names.contains(name)) { continue; } const std::string encrypted_value = utils::crypto::property_encryption::encrypt(value, encryption_provider); property_jsons.AddMember(rapidjson::Value(name, alloc), rapidjson::Value(encrypted_value, alloc), alloc); } } -std::string JsonFlowSerializer::serialize(const core::ProcessGroup &process_group, const core::flow::FlowSchema &schema, const utils::crypto::EncryptionProvider &encryption_provider, - const std::unordered_map<utils::Identifier, std::unordered_map<std::string, std::string>>& overrides) const { +std::string JsonFlowSerializer::serialize(const core::ProcessGroup& process_group, const core::flow::FlowSchema& schema, const utils::crypto::EncryptionProvider& encryption_provider, + const std::unordered_map<utils::Identifier, core::flow::Overrides>& overrides) const { gsl_Expects(schema.root_group.size() == 1 && schema.identifier.size() == 1 && schema.processors.size() == 1 && schema.processor_properties.size() == 1 && schema.controller_services.size() == 1 && schema.controller_service_properties.size() == 1); @@ -86,7 +93,7 @@ std::string JsonFlowSerializer::serialize(const core::ProcessGroup &process_grou logger_->log_warn("Processor {} not found in the flow definition", processor_id->to_string()); continue; } - const auto& processor_overrides = overrides.contains(*processor_id) ? overrides.at(*processor_id) : std::unordered_map<std::string, std::string>{}; + const auto processor_overrides = overrides.contains(*processor_id) ? overrides.at(*processor_id) : core::flow::Overrides{}; encryptSensitiveProperties(getMember(processor_json, schema.processor_properties[0]), alloc, processor->getProperties(), encryption_provider, processor_overrides); } @@ -109,7 +116,7 @@ std::string JsonFlowSerializer::serialize(const core::ProcessGroup &process_grou logger_->log_warn("Controller service {} not found in the flow definition", controller_service_id_str); continue; } - const auto& controller_service_overrides = overrides.contains(*controller_service_id) ? overrides.at(*controller_service_id) : std::unordered_map<std::string, std::string>{}; + const auto controller_service_overrides = overrides.contains(*controller_service_id) ? overrides.at(*controller_service_id) : core::flow::Overrides{}; encryptSensitiveProperties(getMember(controller_service_json, schema.controller_service_properties[0]), alloc, controller_service->getProperties(), encryption_provider, controller_service_overrides); } diff --git a/libminifi/src/core/yaml/YamlFlowSerializer.cpp b/libminifi/src/core/yaml/YamlFlowSerializer.cpp index 00bd87db3..3dc025a9e 100644 --- a/libminifi/src/core/yaml/YamlFlowSerializer.cpp +++ b/libminifi/src/core/yaml/YamlFlowSerializer.cpp @@ -17,12 +17,16 @@ #include "core/yaml/YamlFlowSerializer.h" +#include <unordered_set> + #include "utils/crypto/property_encryption/PropertyEncryptionUtils.h" namespace org::apache::nifi::minifi::core::yaml { void YamlFlowSerializer::encryptSensitiveProperties(YAML::Node property_yamls, const std::map<std::string, Property>& properties, const utils::crypto::EncryptionProvider& encryption_provider, - std::unordered_map<std::string, std::string> component_overrides) const { + const core::flow::Overrides& overrides) const { + std::unordered_set<std::string> processed_property_names; + for (auto kv : property_yamls) { auto name = kv.first.as<std::string>(); if (!properties.contains(name)) { @@ -32,25 +36,28 @@ void YamlFlowSerializer::encryptSensitiveProperties(YAML::Node property_yamls, c if (properties.at(name).isSensitive()) { if (kv.second.IsSequence()) { for (auto property_item : kv.second) { - auto value = component_overrides.contains(name) ? component_overrides.at(name) : property_item["value"].as<std::string>(); + const auto override_value = overrides.get(name); + auto value = override_value ? *override_value : property_item["value"].as<std::string>(); property_item["value"] = utils::crypto::property_encryption::encrypt(value, encryption_provider); } } else { - auto value = component_overrides.contains(name) ? component_overrides.at(name) : kv.second.as<std::string>(); + const auto override_value = overrides.get(name); + auto value = override_value ? *override_value : kv.second.as<std::string>(); property_yamls[name] = utils::crypto::property_encryption::encrypt(value, encryption_provider); } - component_overrides.erase(name); + processed_property_names.insert(name); } } - for (const auto& [name, value] : component_overrides) { + for (const auto& [name, value] : overrides.getRequired()) { gsl_Expects(properties.contains(name) && properties.at(name).isSensitive()); + if (processed_property_names.contains(name)) { continue; } property_yamls[name] = utils::crypto::property_encryption::encrypt(value, encryption_provider); } } std::string YamlFlowSerializer::serialize(const core::ProcessGroup& process_group, const core::flow::FlowSchema& schema, const utils::crypto::EncryptionProvider& encryption_provider, - const std::unordered_map<utils::Identifier, std::unordered_map<std::string, std::string>>& overrides) const { + const std::unordered_map<utils::Identifier, core::flow::Overrides>& overrides) const { gsl_Expects(schema.identifier.size() == 1 && schema.processors.size() == 1 && schema.processor_properties.size() == 1 && schema.controller_services.size() == 1 && schema.controller_service_properties.size() == 1); @@ -68,7 +75,7 @@ std::string YamlFlowSerializer::serialize(const core::ProcessGroup& process_grou logger_->log_warn("Processor {} not found in the flow definition", processor_id->to_string()); continue; } - const auto& processor_overrides = overrides.contains(*processor_id) ? overrides.at(*processor_id) : std::unordered_map<std::string, std::string>{}; + const auto processor_overrides = overrides.contains(*processor_id) ? overrides.at(*processor_id) : core::flow::Overrides{}; encryptSensitiveProperties(processor_yaml[schema.processor_properties[0]], processor->getProperties(), encryption_provider, processor_overrides); } @@ -89,7 +96,7 @@ std::string YamlFlowSerializer::serialize(const core::ProcessGroup& process_grou logger_->log_warn("Controller service {} not found in the flow definition", controller_service_id_str); continue; } - const auto& controller_service_overrides = overrides.contains(*controller_service_id) ? overrides.at(*controller_service_id) : std::unordered_map<std::string, std::string>{}; + const auto controller_service_overrides = overrides.contains(*controller_service_id) ? overrides.at(*controller_service_id) : core::flow::Overrides{}; encryptSensitiveProperties(controller_service_yaml[schema.controller_service_properties[0]], controller_service->getProperties(), encryption_provider, controller_service_overrides); } diff --git a/libminifi/src/utils/crypto/EncryptionUtils.cpp b/libminifi/src/utils/crypto/EncryptionUtils.cpp index 4d4215318..6d1b8fd12 100644 --- a/libminifi/src/utils/crypto/EncryptionUtils.cpp +++ b/libminifi/src/utils/crypto/EncryptionUtils.cpp @@ -59,11 +59,11 @@ std::string EncryptionType::separator() { return "||"; } Bytes encryptRaw(const Bytes& plaintext, const Bytes& key, const Bytes& nonce) { if (key.size() != EncryptionType::keyLength()) { - throw std::invalid_argument{"Expected key of " + std::to_string(EncryptionType::keyLength()) + + throw EncryptionError{"Expected key of " + std::to_string(EncryptionType::keyLength()) + " bytes, but got " + std::to_string(key.size()) + " bytes during encryption"}; } if (nonce.size() != EncryptionType::nonceLength()) { - throw std::invalid_argument{"Expected nonce of " + std::to_string(EncryptionType::nonceLength()) + + throw EncryptionError{"Expected nonce of " + std::to_string(EncryptionType::nonceLength()) + " bytes, but got " + std::to_string(nonce.size()) + " bytes during encryption"}; } @@ -84,22 +84,22 @@ std::string encrypt(std::string_view plaintext, const Bytes& key) { Bytes decryptRaw(const Bytes& input, const Bytes& key, const Bytes& nonce) { if (key.size() != EncryptionType::keyLength()) { - throw std::invalid_argument{"Expected key of " + std::to_string(EncryptionType::keyLength()) + + throw EncryptionError{"Expected key of " + std::to_string(EncryptionType::keyLength()) + " bytes, but got " + std::to_string(key.size()) + " bytes during decryption"}; } if (nonce.size() != EncryptionType::nonceLength()) { - throw std::invalid_argument{"Expected a nonce of " + std::to_string(EncryptionType::nonceLength()) + + throw EncryptionError{"Expected a nonce of " + std::to_string(EncryptionType::nonceLength()) + " bytes, but got " + std::to_string(nonce.size()) + " bytes during decryption"}; } if (input.size() < EncryptionType::macLength()) { - throw std::invalid_argument{"Input is too short: expected at least " + std::to_string(EncryptionType::macLength()) + + throw EncryptionError{"Input is too short: expected at least " + std::to_string(EncryptionType::macLength()) + " bytes, but got " + std::to_string(input.size()) + " bytes during decryption"}; } Bytes plaintext(input.size() - EncryptionType::macLength()); if (crypto_secretbox_open_easy(reinterpret_cast<unsigned char*>(plaintext.data()), reinterpret_cast<const unsigned char*>(input.data()), input.size(), reinterpret_cast<const unsigned char*>(nonce.data()), reinterpret_cast<const unsigned char*>(key.data()))) { - throw std::runtime_error{"Decryption failed; the input may be forged!"}; + throw EncryptionError{"Decryption failed; the input may be forged!"}; } return plaintext; } @@ -113,7 +113,7 @@ std::string decrypt(std::string_view input, const Bytes& key) { EncryptedData parseEncrypted(std::string_view input) { std::vector<std::string> nonce_and_rest = utils::string::split(input, EncryptionType::separator()); if (nonce_and_rest.size() != 2) { - throw std::invalid_argument{"Incorrect input; expected '<nonce>" + EncryptionType::separator() + "<ciphertext_plus_mac>'"}; + throw EncryptionError{"Incorrect input; expected '<nonce>" + EncryptionType::separator() + "<ciphertext_plus_mac>'"}; } Bytes nonce = utils::string::from_base64(nonce_and_rest[0]); diff --git a/libminifi/test/unit/JsonFlowSerializerTests.cpp b/libminifi/test/unit/JsonFlowSerializerTests.cpp index 49cd913aa..dd6f72aa4 100644 --- a/libminifi/test/unit/JsonFlowSerializerTests.cpp +++ b/libminifi/test/unit/JsonFlowSerializerTests.cpp @@ -527,6 +527,8 @@ constexpr std::string_view config_json_with_nifi_schema_part_2 = R"( const utils::crypto::Bytes secret_key = utils::string::from_hex("75536923a75928a970077f9dae2c2b166a5413e020cb5190de4fb8edce1a38c7"); const utils::crypto::EncryptionProvider encryption_provider{secret_key}; +using OverridesMap = std::unordered_map<utils::Identifier, core::flow::Overrides>; + TEST_CASE("JsonFlowSerializer can encrypt the sensitive properties") { ConfigurationTestController test_controller; core::flow::AdaptiveConfiguration json_configuration{test_controller.getContext()}; @@ -543,24 +545,24 @@ TEST_CASE("JsonFlowSerializer can encrypt the sensitive properties") { REQUIRE(res); const auto flow_serializer = core::json::JsonFlowSerializer{std::move(doc)}; - using Overrides = std::unordered_map<utils::Identifier, std::unordered_map<std::string, std::string>>; const auto processor_id = utils::Identifier::parse("469617f1-3898-4bbf-91fe-27d8f4dd2a75").value(); const auto controller_service_id = utils::Identifier::parse("b9801278-7b5d-4314-aed6-713fd4b5f933").value(); const auto [overrides, expected_results] = GENERATE_REF( - std::make_tuple(Overrides{}, + std::make_tuple(OverridesMap{}, std::array{"very_secure_password", "very_secure_passphrase"}), - std::make_tuple(Overrides{{processor_id, {{"invokehttp-proxy-password", "password123"}}}}, + std::make_tuple(OverridesMap{{processor_id, core::flow::Overrides{}.add("invokehttp-proxy-password", "password123")}}, std::array{"password123", "very_secure_passphrase"}), - std::make_tuple(Overrides{{controller_service_id, {{"Passphrase", "speak friend and enter"}}}}, + std::make_tuple(OverridesMap{{controller_service_id, core::flow::Overrides{}.add("Passphrase", "speak friend and enter")}}, std::array{"very_secure_password", "speak friend and enter"}), - std::make_tuple(Overrides{{processor_id, {{"invokehttp-proxy-password", "password123"}}}, {controller_service_id, {{"Passphrase", "speak friend and enter"}}}}, + std::make_tuple(OverridesMap{{processor_id, core::flow::Overrides{}.add("invokehttp-proxy-password", "password123")}, + {controller_service_id, core::flow::Overrides{}.add("Passphrase", "speak friend and enter")}}, std::array{"password123", "speak friend and enter"})); std::string config_json_encrypted = flow_serializer.serialize(*process_group, schema, encryption_provider, overrides); { - std::regex regex{R"_("invokehttp-proxy-password": "(enc\{.*\})",)_"}; + std::regex regex{R"_("invokehttp-proxy-password": "(.*)",)_"}; std::smatch match_results; CHECK(std::regex_search(config_json_encrypted, match_results, regex)); @@ -570,7 +572,7 @@ TEST_CASE("JsonFlowSerializer can encrypt the sensitive properties") { } { - std::regex regex{R"_("Passphrase": "(enc\{.*\})",)_"}; + std::regex regex{R"_("Passphrase": "(.*)",)_"}; std::smatch match_results; CHECK(std::regex_search(config_json_encrypted, match_results, regex)); @@ -595,17 +597,108 @@ TEST_CASE("JsonFlowSerializer with an override can add a new property to the flo const auto flow_serializer = core::json::JsonFlowSerializer{std::move(doc)}; const auto second_controller_service_id = utils::Identifier::parse("b418f4ff-e598-4ea2-921f-14f9dd864482").value(); - const auto overrides = std::unordered_map<utils::Identifier, std::unordered_map<std::string, std::string>>{{second_controller_service_id, {{"Passphrase", "new passphrase"}}}}; - std::string config_json_encrypted = flow_serializer.serialize(*process_group, schema, encryption_provider, overrides); + SECTION("with required overrides") { + const OverridesMap overrides{{second_controller_service_id, core::flow::Overrides{}.add("Passphrase", "new passphrase")}}; + + std::string config_json_encrypted = flow_serializer.serialize(*process_group, schema, encryption_provider, overrides); + + std::regex regex{R"_("Passphrase": "(.*)")_"}; + std::smatch match_results; + + // skip the first match + REQUIRE(std::regex_search(config_json_encrypted.cbegin(), config_json_encrypted.cend(), match_results, regex)); + + // verify the second match + REQUIRE(std::regex_search(match_results.suffix().first, config_json_encrypted.cend(), match_results, regex)); + REQUIRE(match_results.size() == 2); + std::string encrypted_value = match_results[1]; + CHECK(utils::crypto::property_encryption::decrypt(encrypted_value, encryption_provider) == "new passphrase"); + } + + SECTION("with optional overrides: the override is only used if the property is already in the flow config") { + const auto first_controller_service_id = utils::Identifier::parse("b9801278-7b5d-4314-aed6-713fd4b5f933").value(); + const OverridesMap overrides{{first_controller_service_id, core::flow::Overrides{}.addOptional("Passphrase", "first new passphrase")}, + {second_controller_service_id, core::flow::Overrides{}.addOptional("Passphrase", "second new passphrase")}}; + + std::string config_json_encrypted = flow_serializer.serialize(*process_group, schema, encryption_provider, overrides); + + std::regex regex{R"_("Passphrase": "(.*)")_"}; + std::smatch match_results; + + // verify the first match + REQUIRE(std::regex_search(config_json_encrypted.cbegin(), config_json_encrypted.cend(), match_results, regex)); + REQUIRE(match_results.size() == 2); + std::string encrypted_value = match_results[1]; + CHECK(utils::crypto::property_encryption::decrypt(encrypted_value, encryption_provider) == "first new passphrase"); + + // check that there is no second match + CHECK_FALSE(std::regex_search(match_results.suffix().first, config_json_encrypted.cend(), match_results, regex)); + } +} + +TEST_CASE("The encrypted flow configuration can be decrypted with the correct key") { + ConfigurationTestController test_controller; + auto configuration_context = test_controller.getContext(); + configuration_context.sensitive_properties_encryptor = encryption_provider; + core::flow::AdaptiveConfiguration json_configuration_before{configuration_context}; + + const auto schema = core::flow::FlowSchema::getNiFiFlowJson(); + const auto config_json_with_nifi_schema = utils::string::join_pack(config_json_with_nifi_schema_part_1, config_json_with_nifi_schema_part_2); + const auto process_group_before = json_configuration_before.getRootFromPayload(config_json_with_nifi_schema); + REQUIRE(process_group_before); + + rapidjson::Document doc; + rapidjson::ParseResult res = doc.Parse(config_json_with_nifi_schema.data(), config_json_with_nifi_schema.size()); + REQUIRE(res); + const auto flow_serializer = core::json::JsonFlowSerializer{std::move(doc)}; + std::string config_json_encrypted = flow_serializer.serialize(*process_group_before, schema, encryption_provider, {}); + + core::flow::AdaptiveConfiguration json_configuration_after{configuration_context}; + const auto process_group_after = json_configuration_after.getRootFromPayload(config_json_encrypted); + REQUIRE(process_group_after); + + const auto processor_id = utils::Identifier::parse("469617f1-3898-4bbf-91fe-27d8f4dd2a75").value(); + const auto* processor_before = process_group_before->findProcessorById(processor_id); + REQUIRE(processor_before); + const auto* processor_after = process_group_after->findProcessorById(processor_id); + REQUIRE(processor_after); + CHECK(processor_before->getProperties().at("HTTP Method").getValue() == processor_after->getProperties().at("HTTP Method").getValue()); + CHECK(processor_before->getProperties().at("invokehttp-proxy-password").getValue() == processor_after->getProperties().at("invokehttp-proxy-password").getValue()); + + const auto controller_service_id = "b9801278-7b5d-4314-aed6-713fd4b5f933"; + const auto controller_service_node_before = process_group_before->findControllerService(controller_service_id); + REQUIRE(controller_service_node_before); + const auto controller_service_before = controller_service_node_before->getControllerServiceImplementation(); + REQUIRE(controller_service_node_before); + const auto controller_service_node_after = process_group_after->findControllerService(controller_service_id); + REQUIRE(controller_service_node_after); + const auto controller_service_after = controller_service_node_before->getControllerServiceImplementation(); + REQUIRE(controller_service_after); + CHECK(controller_service_before->getProperties().at("CA Certificate").getValue() == controller_service_after->getProperties().at("CA Certificate").getValue()); + CHECK(controller_service_before->getProperties().at("Passphrase").getValue() == controller_service_after->getProperties().at("Passphrase").getValue()); +} + +TEST_CASE("The encrypted flow configuration cannot be decrypted with an incorrect key") { + ConfigurationTestController test_controller; + auto configuration_context = test_controller.getContext(); + configuration_context.sensitive_properties_encryptor = encryption_provider; + core::flow::AdaptiveConfiguration json_configuration_before{configuration_context}; + + const auto schema = core::flow::FlowSchema::getNiFiFlowJson(); + const auto config_json_with_nifi_schema = utils::string::join_pack(config_json_with_nifi_schema_part_1, config_json_with_nifi_schema_part_2); + const auto process_group_before = json_configuration_before.getRootFromPayload(config_json_with_nifi_schema); + REQUIRE(process_group_before); + + rapidjson::Document doc; + rapidjson::ParseResult res = doc.Parse(config_json_with_nifi_schema.data(), config_json_with_nifi_schema.size()); + REQUIRE(res); + const auto flow_serializer = core::json::JsonFlowSerializer{std::move(doc)}; + std::string config_json_encrypted = flow_serializer.serialize(*process_group_before, schema, encryption_provider, {}); - // find the second match - std::regex regex{R"_("Passphrase": "(enc\{.*\})")_"}; - std::smatch match_results; - REQUIRE(std::regex_search(config_json_encrypted.cbegin(), config_json_encrypted.cend(), match_results, regex)); - REQUIRE(std::regex_search(match_results.suffix().first, config_json_encrypted.cend(), match_results, regex)); - REQUIRE(match_results.size() == 2); + const utils::crypto::Bytes different_secret_key = utils::string::from_hex("ea55b7d0edc22280c9547e4d89712b3fae74f96d82f240a004fb9fbd0640eec7"); + configuration_context.sensitive_properties_encryptor = utils::crypto::EncryptionProvider{different_secret_key}; - std::string encrypted_value = match_results[1]; - CHECK(utils::crypto::property_encryption::decrypt(encrypted_value, encryption_provider) == "new passphrase"); + core::flow::AdaptiveConfiguration json_configuration_after{configuration_context}; + REQUIRE_THROWS_AS(json_configuration_after.getRootFromPayload(config_json_encrypted), utils::crypto::EncryptionError); } diff --git a/libminifi/test/unit/YamlFlowSerializerTests.cpp b/libminifi/test/unit/YamlFlowSerializerTests.cpp index 25dff1514..14d806247 100644 --- a/libminifi/test/unit/YamlFlowSerializerTests.cpp +++ b/libminifi/test/unit/YamlFlowSerializerTests.cpp @@ -173,6 +173,8 @@ NiFi Properties Overrides: {} const utils::crypto::Bytes secret_key = utils::string::from_hex("cb76fe6fe4cbfdc3770c0cb0afc910f81ced4d436b11f691395fc2a9dbea27ca"); const utils::crypto::EncryptionProvider encryption_provider{secret_key}; +using OverridesMap = std::unordered_map<utils::Identifier, core::flow::Overrides>; + TEST_CASE("YamlFlowSerializer can encrypt the sensitive properties") { ConfigurationTestController test_controller; core::YamlConfiguration yaml_configuration{test_controller.getContext()}; @@ -184,24 +186,24 @@ TEST_CASE("YamlFlowSerializer can encrypt the sensitive properties") { YAML::Node root_yaml_node = YAML::Load(std::string{config_yaml}); const auto flow_serializer = core::yaml::YamlFlowSerializer{root_yaml_node}; - using Overrides = std::unordered_map<utils::Identifier, std::unordered_map<std::string, std::string>>; const auto processor_id = utils::Identifier::parse("469617f1-3898-4bbf-91fe-27d8f4dd2a75").value(); const auto controller_service_id = utils::Identifier::parse("b9801278-7b5d-4314-aed6-713fd4b5f933").value(); const auto [overrides, expected_results] = GENERATE_REF( - std::make_tuple(Overrides{}, + std::make_tuple(OverridesMap{}, std::array{"very_secure_password", "very_secure_passphrase"}), - std::make_tuple(Overrides{{processor_id, {{"invokehttp-proxy-password", "password123"}}}}, + std::make_tuple(OverridesMap{{processor_id, core::flow::Overrides{}.add("invokehttp-proxy-password", "password123")}}, std::array{"password123", "very_secure_passphrase"}), - std::make_tuple(Overrides{{controller_service_id, {{"Passphrase", "speak friend and enter"}}}}, + std::make_tuple(OverridesMap{{controller_service_id, core::flow::Overrides{}.add("Passphrase", "speak friend and enter")}}, std::array{"very_secure_password", "speak friend and enter"}), - std::make_tuple(Overrides{{processor_id, {{"invokehttp-proxy-password", "password123"}}}, {controller_service_id, {{"Passphrase", "speak friend and enter"}}}}, + std::make_tuple(OverridesMap{{processor_id, core::flow::Overrides{}.add("invokehttp-proxy-password", "password123")}, + {controller_service_id, core::flow::Overrides{}.add("Passphrase", "speak friend and enter")}}, std::array{"password123", "speak friend and enter"})); std::string config_yaml_encrypted = flow_serializer.serialize(*process_group, schema, encryption_provider, overrides); { - std::regex regex{R"_(invokehttp-proxy-password: (enc\{.*\}))_"}; + std::regex regex{R"_(invokehttp-proxy-password: (.*))_"}; std::smatch match_results; CHECK(std::regex_search(config_yaml_encrypted, match_results, regex)); @@ -211,7 +213,7 @@ TEST_CASE("YamlFlowSerializer can encrypt the sensitive properties") { } { - std::regex regex{R"_(Passphrase: (enc\{.*\}))_"}; + std::regex regex{R"_(Passphrase: (.*))_"}; std::smatch match_results; CHECK(std::regex_search(config_yaml_encrypted, match_results, regex)); @@ -233,17 +235,102 @@ TEST_CASE("YamlFlowSerializer with an override can add a new property to the flo const auto flow_serializer = core::yaml::YamlFlowSerializer{root_yaml_node}; const auto second_controller_service_id = utils::Identifier::parse("b418f4ff-e598-4ea2-921f-14f9dd864482").value(); - const auto overrides = std::unordered_map<utils::Identifier, std::unordered_map<std::string, std::string>>{{second_controller_service_id, {{"Passphrase", "new passphrase"}}}}; - std::string config_yaml_encrypted = flow_serializer.serialize(*process_group, schema, encryption_provider, overrides); + SECTION("with required overrides") { + const OverridesMap overrides{{second_controller_service_id, core::flow::Overrides{}.add("Passphrase", "new passphrase")}}; + + std::string config_yaml_encrypted = flow_serializer.serialize(*process_group, schema, encryption_provider, overrides); + + std::regex regex{R"_(Passphrase: (.*))_"}; + std::smatch match_results; + + // skip the first match + REQUIRE(std::regex_search(config_yaml_encrypted.cbegin(), config_yaml_encrypted.cend(), match_results, regex)); + + // verify the second match + REQUIRE(std::regex_search(match_results.suffix().first, config_yaml_encrypted.cend(), match_results, regex)); + REQUIRE(match_results.size() == 2); + std::string encrypted_value = match_results[1]; + CHECK(utils::crypto::property_encryption::decrypt(encrypted_value, encryption_provider) == "new passphrase"); + } + + SECTION("with optional overrides: the override is only used if the property is already in the flow config") { + const auto first_controller_service_id = utils::Identifier::parse("b9801278-7b5d-4314-aed6-713fd4b5f933").value(); + const OverridesMap overrides{{first_controller_service_id, core::flow::Overrides{}.addOptional("Passphrase", "first new passphrase")}, + {second_controller_service_id, core::flow::Overrides{}.addOptional("Passphrase", "second new passphrase")}}; + + std::string config_yaml_encrypted = flow_serializer.serialize(*process_group, schema, encryption_provider, overrides); + + std::regex regex{R"_(Passphrase: (.*))_"}; + std::smatch match_results; + + // verify the first match + REQUIRE(std::regex_search(config_yaml_encrypted.cbegin(), config_yaml_encrypted.cend(), match_results, regex)); + REQUIRE(match_results.size() == 2); + std::string encrypted_value = match_results[1]; + CHECK(utils::crypto::property_encryption::decrypt(encrypted_value, encryption_provider) == "first new passphrase"); + + // check that there is no second match + CHECK_FALSE(std::regex_search(match_results.suffix().first, config_yaml_encrypted.cend(), match_results, regex)); + } +} + +TEST_CASE("The encrypted flow configuration can be decrypted with the correct key") { + ConfigurationTestController test_controller; + auto configuration_context = test_controller.getContext(); + configuration_context.sensitive_properties_encryptor = encryption_provider; + + core::flow::AdaptiveConfiguration yaml_configuration_before{configuration_context}; + const auto process_group_before = yaml_configuration_before.getRootFromPayload(std::string{config_yaml}); + REQUIRE(process_group_before); + + const auto schema = core::flow::FlowSchema::getDefault(); + YAML::Node root_yaml_node = YAML::Load(std::string{config_yaml}); + const auto flow_serializer_before = core::yaml::YamlFlowSerializer{root_yaml_node}; + std::string config_yaml_encrypted = flow_serializer_before.serialize(*process_group_before, schema, encryption_provider, {}); + + core::flow::AdaptiveConfiguration yaml_configuration_after{configuration_context}; + const auto process_group_after = yaml_configuration_after.getRootFromPayload(config_yaml_encrypted); + REQUIRE(process_group_after); + + const auto processor_id = utils::Identifier::parse("469617f1-3898-4bbf-91fe-27d8f4dd2a75").value(); + const auto* processor_before = process_group_before->findProcessorById(processor_id); + REQUIRE(processor_before); + const auto* processor_after = process_group_after->findProcessorById(processor_id); + REQUIRE(processor_after); + CHECK(processor_before->getProperties().at("HTTP Method").getValue() == processor_after->getProperties().at("HTTP Method").getValue()); + CHECK(processor_before->getProperties().at("invokehttp-proxy-password").getValue() == processor_after->getProperties().at("invokehttp-proxy-password").getValue()); + + const auto controller_service_id = "b9801278-7b5d-4314-aed6-713fd4b5f933"; + const auto controller_service_node_before = process_group_before->findControllerService(controller_service_id); + REQUIRE(controller_service_node_before); + const auto controller_service_before = controller_service_node_before->getControllerServiceImplementation(); + REQUIRE(controller_service_node_before); + const auto controller_service_node_after = process_group_after->findControllerService(controller_service_id); + REQUIRE(controller_service_node_after); + const auto controller_service_after = controller_service_node_before->getControllerServiceImplementation(); + REQUIRE(controller_service_after); + CHECK(controller_service_before->getProperties().at("CA Certificate").getValue() == controller_service_after->getProperties().at("CA Certificate").getValue()); + CHECK(controller_service_before->getProperties().at("Passphrase").getValue() == controller_service_after->getProperties().at("Passphrase").getValue()); +} + +TEST_CASE("The encrypted flow configuration cannot be decrypted with an incorrect key") { + ConfigurationTestController test_controller; + auto configuration_context = test_controller.getContext(); + configuration_context.sensitive_properties_encryptor = encryption_provider; + + core::flow::AdaptiveConfiguration yaml_configuration_before{configuration_context}; + const auto process_group_before = yaml_configuration_before.getRootFromPayload(std::string{config_yaml}); + REQUIRE(process_group_before); + + const auto schema = core::flow::FlowSchema::getDefault(); + YAML::Node root_yaml_node = YAML::Load(std::string{config_yaml}); + const auto flow_serializer = core::yaml::YamlFlowSerializer{root_yaml_node}; + std::string config_yaml_encrypted = flow_serializer.serialize(*process_group_before, schema, encryption_provider, {}); - // find the second match - std::regex regex{R"_(Passphrase: (enc\{.*\}))_"}; - std::smatch match_results; - REQUIRE(std::regex_search(config_yaml_encrypted.cbegin(), config_yaml_encrypted.cend(), match_results, regex)); - REQUIRE(std::regex_search(match_results.suffix().first, config_yaml_encrypted.cend(), match_results, regex)); - REQUIRE(match_results.size() == 2); + const utils::crypto::Bytes different_secret_key = utils::string::from_hex("ea55b7d0edc22280c9547e4d89712b3fae74f96d82f240a004fb9fbd0640eec7"); + configuration_context.sensitive_properties_encryptor = utils::crypto::EncryptionProvider{different_secret_key}; - std::string encrypted_value = match_results[1]; - CHECK(utils::crypto::property_encryption::decrypt(encrypted_value, encryption_provider) == "new passphrase"); + core::flow::AdaptiveConfiguration yaml_configuration_after{configuration_context}; + REQUIRE_THROWS_AS(yaml_configuration_after.getRootFromPayload(config_yaml_encrypted), utils::crypto::EncryptionError); }