This is an automated email from the ASF dual-hosted git repository.

fgerlits pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi-minifi-cpp.git


The following commit(s) were added to refs/heads/main by this push:
     new 7b54eb7db MINIFICPP-2787 Do not encrypt empty properties and 
parameters (#2217)
7b54eb7db is described below

commit 7b54eb7db84cb020434937ff429945ec9e529bbe
Author: Ferenc Gerlits <[email protected]>
AuthorDate: Mon Jul 27 17:35:54 2026 +0200

    MINIFICPP-2787 Do not encrypt empty properties and parameters (#2217)
    
    This also fixes a bug: if a sensitive value was set as
      sensitive_property:
    instead of
      sensitive_property: ""
    in a YAML flow configuration, then the value was null instead of an empty
    string, and it got expanded to the string "null", and then encrypted.
---
 .../PropertyEncryptionUtils.cpp                    |  2 +-
 libminifi/src/core/yaml/YamlFlowSerializer.cpp     |  8 ++---
 libminifi/test/libtest/unit/Catch.h                | 17 ++++++++-
 libminifi/test/unit/ExpectedTest.cpp               |  9 +++++
 libminifi/test/unit/JsonFlowSerializerTests.cpp    | 40 ++++++++++++++++++++++
 .../test/unit/PropertyEncryptionUtilsTests.cpp     | 12 +++----
 libminifi/test/unit/YamlFlowSerializerTests.cpp    | 39 ++++++++++++++++++++-
 7 files changed, 112 insertions(+), 15 deletions(-)

diff --git 
a/core-framework/src/utils/crypto/property_encryption/PropertyEncryptionUtils.cpp
 
b/core-framework/src/utils/crypto/property_encryption/PropertyEncryptionUtils.cpp
index 7f9cbdb74..f88ab1416 100644
--- 
a/core-framework/src/utils/crypto/property_encryption/PropertyEncryptionUtils.cpp
+++ 
b/core-framework/src/utils/crypto/property_encryption/PropertyEncryptionUtils.cpp
@@ -39,7 +39,7 @@ std::string decrypt(std::string_view input, const 
utils::crypto::EncryptionProvi
 }
 
 std::string encrypt(std::string_view input, const 
utils::crypto::EncryptionProvider& encryption_provider) {
-  if (isEncrypted(input)) {
+  if (input.empty() || isEncrypted(input)) {
     return std::string{input};
   }
   return utils::string::join_pack("enc{", encryption_provider.encrypt(input), 
"}");
diff --git a/libminifi/src/core/yaml/YamlFlowSerializer.cpp 
b/libminifi/src/core/yaml/YamlFlowSerializer.cpp
index eff5f49aa..eba2d016c 100644
--- a/libminifi/src/core/yaml/YamlFlowSerializer.cpp
+++ b/libminifi/src/core/yaml/YamlFlowSerializer.cpp
@@ -65,7 +65,7 @@ void 
YamlFlowSerializer::addProviderCreatedParameterContexts(YAML::Node flow_def
 
     int64_t index = -1;
     for (int64_t i = 0; i < 
gsl::narrow<int64_t>(parameter_contexts_node.size()); ++i) {
-      if (parameter_contexts_node[i][schema.name[0]].as<std::string>() == 
parameter_context->getName()) {
+      if (parameter_contexts_node[i][schema.name[0]].Scalar() == 
parameter_context->getName()) {
         index = i;
         break;
       }
@@ -85,7 +85,7 @@ void 
YamlFlowSerializer::encryptSensitiveProperties(YAML::Node property_yamls,
   std::unordered_set<std::string> processed_property_names;
 
   for (auto kv : property_yamls) {
-    auto name = kv.first.as<std::string>();
+    auto name = kv.first.Scalar();
     if (!properties.contains(name)) {
       logger_->log_warn("Property {} found in flow definition does not 
exist!", name);
       continue;
@@ -94,12 +94,12 @@ void 
YamlFlowSerializer::encryptSensitiveProperties(YAML::Node property_yamls,
       if (kv.second.IsSequence()) {
         for (auto property_item : kv.second) {
           const auto override_value = overrides.get(name);
-          auto value = override_value ? *override_value : 
property_item["value"].as<std::string>();
+          auto value = override_value ? *override_value : 
property_item["value"].Scalar();
           property_item["value"] = 
utils::crypto::property_encryption::encrypt(value, encryption_provider);
         }
       } else {
         const auto override_value = overrides.get(name);
-        auto value = override_value ? *override_value : 
kv.second.as<std::string>();
+        auto value = override_value ? *override_value : kv.second.Scalar();
         property_yamls[name] = 
utils::crypto::property_encryption::encrypt(value, encryption_provider);
       }
       processed_property_names.insert(name);
diff --git a/libminifi/test/libtest/unit/Catch.h 
b/libminifi/test/libtest/unit/Catch.h
index 3a6ff3bce..742acdb81 100644
--- a/libminifi/test/libtest/unit/Catch.h
+++ b/libminifi/test/libtest/unit/Catch.h
@@ -17,9 +17,11 @@
 
 #pragma once
 
+#include <chrono>
+#include <expected>
 #include <optional>
 #include <string>
-#include <chrono>
+
 #include "fmt/format.h"
 #include "catch2/catch_test_macros.hpp"
 #include "catch2/catch_tostring.hpp"
@@ -56,6 +58,19 @@ struct StringMaker<std::chrono::file_clock::duration> {
     return fmt::format("{} {} s", duration.count(), 
Catch::ratio_string<std::chrono::file_clock::duration::period>::symbol());
   }
 };
+
+template <typename T, typename E>
+struct StringMaker<std::expected<T, E>> {
+  static std::string convert(const std::expected<T, E>& expected) {
+    if (!expected) {
+      return fmt::format("unexpected({})", 
Catch::Detail::stringify(expected.error()));
+    }
+    if constexpr (!std::is_void_v<T>) {
+      return fmt::format("expected({})", Catch::Detail::stringify(*expected));
+    }
+    return "expected(void)";
+  }
+};
 }  // namespace Catch
 
 namespace org::apache::nifi::minifi::test {
diff --git a/libminifi/test/unit/ExpectedTest.cpp 
b/libminifi/test/unit/ExpectedTest.cpp
index 11bc0e9ec..b68e95ed4 100644
--- a/libminifi/test/unit/ExpectedTest.cpp
+++ b/libminifi/test/unit/ExpectedTest.cpp
@@ -578,3 +578,12 @@ TEST_CASE("This fails to compile with std::expected on GCC 
15.1 due to https://g
 
   CHECK(a == b);
 }
+
+TEST_CASE("Test Catch2 stringification of expected") {
+  CHECK(Catch::Detail::stringify(std::expected<int, std::string>{123}) == 
"expected(123)");
+  CHECK(Catch::Detail::stringify(std::expected<std::string, 
std::string>{"hello"}) == R"(expected("hello"))");
+  CHECK(Catch::Detail::stringify(std::expected<std::string, 
std::error_code>{"hello"}) == R"(expected("hello"))");
+  CHECK(Catch::Detail::stringify(std::expected<std::string, 
std::unique_ptr<int>>{"hello"}) == R"(expected("hello"))");
+  CHECK(Catch::Detail::stringify(std::expected<void, std::string>{}) == 
"expected(void)");
+  CHECK(Catch::Detail::stringify(std::expected<std::string, 
std::string>{std::unexpect, "doesn't compute"}) == R"(unexpected("doesn't 
compute"))");
+}
diff --git a/libminifi/test/unit/JsonFlowSerializerTests.cpp 
b/libminifi/test/unit/JsonFlowSerializerTests.cpp
index 62fad1d3d..88097b040 100644
--- a/libminifi/test/unit/JsonFlowSerializerTests.cpp
+++ b/libminifi/test/unit/JsonFlowSerializerTests.cpp
@@ -749,6 +749,46 @@ TEST_CASE("The encrypted flow configuration cannot be 
decrypted with an incorrec
   
REQUIRE_THROWS_AS(json_configuration_after.getRootFromPayload(config_json_encrypted),
 minifi::utils::crypto::EncryptionError);
 }
 
+TEST_CASE("Empty sensitive properties and parameters are not encrypted and 
round-trip as empty") {
+  ConfigurationTestController test_controller;
+  auto configuration_context = test_controller.getContext();
+  configuration_context.sensitive_values_encryptor = encryption_provider;
+
+  const auto schema = core::flow::FlowSchema::getNiFiFlowJson();
+  std::string config_json_with_empty_passphrase_and_parameter = 
minifi::utils::string::join_pack(config_json_with_nifi_schema_part_1, 
config_json_with_nifi_schema_part_2);
+  
minifi::utils::string::replaceAll(config_json_with_empty_passphrase_and_parameter,
 "very_secure_passphrase", "");
+  
minifi::utils::string::replaceAll(config_json_with_empty_passphrase_and_parameter,
 "param_value_1", "");
+
+  core::flow::AdaptiveConfiguration 
json_configuration_before{configuration_context};
+  const auto process_group_before = 
json_configuration_before.getRootFromPayload(config_json_with_empty_passphrase_and_parameter);
+  REQUIRE(process_group_before);
+
+  rapidjson::Document doc;
+  rapidjson::ParseResult res = 
doc.Parse(config_json_with_empty_passphrase_and_parameter.data(), 
config_json_with_empty_passphrase_and_parameter.size());
+  REQUIRE(res);
+  const auto flow_serializer = core::json::JsonFlowSerializer{std::move(doc)};
+
+  const auto processor_id = 
minifi::utils::Identifier::parse("469617f1-3898-4bbf-91fe-27d8f4dd2a75").value();
+  const OverridesMap overrides{{processor_id, 
core::flow::Overrides{}.add("invokehttp-proxy-password", "")}};
+  const std::string serialized = 
flow_serializer.serialize(*process_group_before, schema, encryption_provider, 
overrides, {});
+  CHECK_FALSE(serialized.contains("enc{"));
+
+  core::flow::AdaptiveConfiguration 
json_configuration_after{configuration_context};
+  const auto process_group_after = 
json_configuration_after.getRootFromPayload(serialized);
+  REQUIRE(process_group_after);
+
+  const auto* processor_after = 
process_group_after->findProcessorById(processor_id);
+  REQUIRE(processor_after);
+  CHECK(processor_after->getProperty("invokehttp-proxy-password") == "");
+
+  const auto* const controller_service_node_after = 
process_group_after->findControllerService("b9801278-7b5d-4314-aed6-713fd4b5f933");
+  REQUIRE(controller_service_node_after);
+  
CHECK(controller_service_node_after->getControllerServiceImplementation()->getProperty("Passphrase")
 == "");
+
+  const auto& param_contexts = json_configuration_after.getParameterContexts();
+  
CHECK(param_contexts.at("my-context")->getParameter("secret_parameter").value().value.empty());
+}
+
 TEST_CASE("Parameter provider generated parameter context is serialized 
correctly") {
   ConfigurationTestController test_controller;
   auto configuration_context = test_controller.getContext();
diff --git a/libminifi/test/unit/PropertyEncryptionUtilsTests.cpp 
b/libminifi/test/unit/PropertyEncryptionUtilsTests.cpp
index 93885b672..155cbd60c 100644
--- a/libminifi/test/unit/PropertyEncryptionUtilsTests.cpp
+++ b/libminifi/test/unit/PropertyEncryptionUtilsTests.cpp
@@ -31,11 +31,6 @@ const crypto::Bytes secret_key = crypto::generateKey();
 const crypto::EncryptionProvider property_encryptor{secret_key};
 
 TEST_CASE("A property value can be encrypted") {
-  std::string encrypted_blank = property_encryption::encrypt("", 
property_encryptor);
-  CHECK(encrypted_blank.starts_with("enc{"));
-  CHECK(encrypted_blank.ends_with("}"));
-  CHECK(encrypted_blank.size() == 63);
-
   std::string encrypted_foo = property_encryption::encrypt("foo", 
property_encryptor);
   CHECK(encrypted_foo.starts_with("enc{"));
   CHECK(encrypted_foo.ends_with("}"));
@@ -48,6 +43,10 @@ TEST_CASE("A property value can be encrypted") {
   CHECK(encrypted_long_text.size() == 283);
 }
 
+TEST_CASE("An empty property value is not encrypted") {
+  CHECK(property_encryption::encrypt("", property_encryptor).empty());
+}
+
 TEST_CASE("Encrypting the same value a second time doesn't change its value") {
   std::string encrypted_foo = property_encryption::encrypt("foo", 
property_encryptor);
   std::string re_encrypted_foo = property_encryption::encrypt(encrypted_foo, 
property_encryptor);
@@ -58,9 +57,6 @@ TEST_CASE("Encrypting the same value a second time doesn't 
change its value") {
 }
 
 TEST_CASE("We can decrypt an encrypted value") {
-  std::string encrypted_blank = property_encryption::encrypt("", 
property_encryptor);
-  CHECK(property_encryption::decrypt(encrypted_blank, 
property_encryptor).empty());
-
   std::string encrypted_foo = property_encryption::encrypt("foo", 
property_encryptor);
   CHECK(property_encryption::decrypt(encrypted_foo, property_encryptor) == 
"foo");
 
diff --git a/libminifi/test/unit/YamlFlowSerializerTests.cpp 
b/libminifi/test/unit/YamlFlowSerializerTests.cpp
index 445a3cef4..c76c2ac2a 100644
--- a/libminifi/test/unit/YamlFlowSerializerTests.cpp
+++ b/libminifi/test/unit/YamlFlowSerializerTests.cpp
@@ -26,7 +26,6 @@
 #include "utils/crypto/EncryptionProvider.h"
 #include "utils/crypto/property_encryption/PropertyEncryptionUtils.h"
 #include "utils/StringUtils.h"
-#include "core/Resource.h"
 #include "utils/Environment.h"
 
 namespace org::apache::nifi::minifi::test {
@@ -340,6 +339,44 @@ TEST_CASE("The encrypted flow configuration can be 
decrypted with the correct ke
   
CHECK(param_contexts.at("my-context")->getParameter("secret_parameter")->value 
== "param_value_1");
 }
 
+TEST_CASE("Empty sensitive properties and parameters are not encrypted and 
round-trip as empty") {
+  ConfigurationTestController test_controller;
+  auto configuration_context = test_controller.getContext();
+  configuration_context.sensitive_values_encryptor = encryption_provider;
+
+  std::string config_yaml_with_empty_passphrase_and_parameter{config_yaml};
+  
minifi::utils::string::replaceAll(config_yaml_with_empty_passphrase_and_parameter,
 "very_secure_passphrase", "");
+  
minifi::utils::string::replaceAll(config_yaml_with_empty_passphrase_and_parameter,
 "param_value_1", "");
+
+  core::flow::AdaptiveConfiguration 
yaml_configuration_before{configuration_context};
+  const auto process_group_before = 
yaml_configuration_before.getRootFromPayload(config_yaml_with_empty_passphrase_and_parameter);
+  REQUIRE(process_group_before);
+
+  const auto schema = core::flow::FlowSchema::getDefault();
+  YAML::Node root_yaml_node = 
YAML::Load(config_yaml_with_empty_passphrase_and_parameter);
+  const auto flow_serializer = core::yaml::YamlFlowSerializer{root_yaml_node};
+
+  const auto processor_id = 
minifi::utils::Identifier::parse("469617f1-3898-4bbf-91fe-27d8f4dd2a75").value();
+  const OverridesMap overrides{{processor_id, 
core::flow::Overrides{}.add("invokehttp-proxy-password", "")}};
+  const std::string serialized = 
flow_serializer.serialize(*process_group_before, schema, encryption_provider, 
overrides, {});
+  CHECK_FALSE(serialized.contains("enc{"));
+
+  core::flow::AdaptiveConfiguration 
yaml_configuration_after{configuration_context};
+  const auto process_group_after = 
yaml_configuration_after.getRootFromPayload(serialized);
+  REQUIRE(process_group_after);
+
+  const auto* processor_after = 
process_group_after->findProcessorById(processor_id);
+  REQUIRE(processor_after);
+  CHECK(processor_after->getProperty("invokehttp-proxy-password") == "");
+
+  const auto* const controller_service_node_after = 
process_group_after->findControllerService("b9801278-7b5d-4314-aed6-713fd4b5f933");
+  REQUIRE(controller_service_node_after);
+  
CHECK(controller_service_node_after->getControllerServiceImplementation()->getProperty("Passphrase")
 == "");
+
+  const auto& param_contexts = yaml_configuration_after.getParameterContexts();
+  
CHECK(param_contexts.at("my-context")->getParameter("secret_parameter").value().value.empty());
+}
+
 TEST_CASE("The encrypted flow configuration cannot be decrypted with an 
incorrect key") {
   ConfigurationTestController test_controller;
   auto configuration_context = test_controller.getContext();

Reply via email to