From 55b159d13b1589cc0528c31fd35d7698379a7872 Mon Sep 17 00:00:00 2001 From: exceptionfactory Date: Mon, 7 Sep 2026 23:24:04 -0500 Subject: [PATCH] NIFI-16303 Integrated Property Encryption Provider to Framework --- minifi/minifi-assembly/pom.xml | 6 + .../minifi-commons-framework/pom.xml | 5 - .../StandardFlowPropertyEncryptor.java | 11 +- .../StandardFlowPropertyEncryptorTest.java | 10 +- .../nifi-connector-mock-server/pom.xml | 5 - ...va => MockPropertyEncryptionProvider.java} | 17 +- .../server/StandardConnectorMockServer.java | 5 +- .../main/asciidoc/administration-guide.adoc | 37 +++- .../nifi-framework-components/pom.xml | 5 - .../flowanalysis/FlowAnalysisUtil.java | 26 ++- ...tandardVersionedComponentSynchronizer.java | 106 ++++++++---- .../nifi/groups/StandardProcessGroup.java | 43 +++-- .../FilterSensitiveParameterValueMapper.java | 3 +- .../nifi/parameter/ParameterValueMapper.java | 3 +- .../StandardParameterValueMapper.java | 43 +++-- .../mapping/VersionedComponentFlowMapper.java | 48 +++--- .../PropertyEncryptionProviderFactory.java | 135 +++++++++++++++ .../ProviderSensitiveValueDecryptor.java | 50 ++++++ ...ardVersionedComponentSynchronizerTest.java | 52 +++++- .../nifi/groups/StandardProcessGroupTest.java | 8 +- .../TestStandardParameterValueMapper.java | 62 +++++-- .../TestVersionedComponentFlowMapper.java | 111 +++++++++++- .../groups/FlowSynchronizationOptions.java | 60 +++++-- .../apache/nifi/groups/PropertyDecryptor.java | 24 --- .../flow/mapping/FlowMappingOptions.java | 38 +++-- .../flow/mapping/SensitiveValueEncryptor.java | 22 --- ...PassThroughPropertyEncryptionProvider.java | 41 +++++ .../encryption/PropertyEncryptionEncoder.java | 73 ++++++++ .../encryption/SensitivePropertyCodec.java | 76 +++++++++ .../SensitivePropertyContextFactory.java | 95 +++++++++++ ...ThroughPropertyEncryptionProviderTest.java | 57 +++++++ .../PropertyEncryptionEncoderTest.java | 82 +++++++++ .../SensitivePropertyCodecTest.java | 114 +++++++++++++ .../SensitivePropertyContextFactoryTest.java | 117 +++++++++++++ .../nifi-framework-core/pom.xml | 5 - .../nifi/controller/FlowController.java | 22 +-- .../FlowControllerFlowContextFactory.java | 3 +- ...owControllerProcessGroupFacadeFactory.java | 3 +- .../controller/flow/StandardFlowManager.java | 47 +----- .../StandardStatelessGroupNodeFactory.java | 6 +- .../serialization/FlowSerializer.java | 3 - .../FlowSynchronizationUtils.java | 53 ++++-- ...tandardVersionedReportingTaskImporter.java | 10 +- .../VersionedDataflowMapper.java | 7 +- .../VersionedFlowSerializer.java | 4 +- .../VersionedFlowSynchronizer.java | 60 ++++--- .../encrypt/PropertyEncryptorFactory.java | 46 ----- .../FlowControllerConfiguration.java | 105 +----------- .../VersionedFlowSynchronizerTest.java | 15 +- .../nifi-headless-server/pom.xml | 5 - .../nifi/headless/HeadlessNiFiServer.java | 11 +- .../nifi/web/StandardNiFiServiceFacade.java | 4 +- .../nifi-web/nifi-web-security/pom.xml | 5 - .../OidcSecurityConfiguration.java | 10 +- .../StandardAuthorizedClientConverter.java | 17 +- ...StandardAuthorizedClientConverterTest.java | 16 +- .../flow/diff/SensitiveValueDecryptor.java | 38 +++++ .../flow/diff/StandardFlowComparator.java | 83 ++++++--- .../flow/diff/TestStandardFlowComparator.java | 57 ++++++- .../nifi-stateless-engine/pom.xml | 5 - .../engine/StandardStatelessEngine.java | 13 -- .../stateless/engine/StatelessEngine.java | 2 - .../engine/StatelessFlowManager.java | 4 +- .../StandardStatelessDataflowFactory.java | 29 ---- .../PropertyEncryptionProviderIT.java | 158 ++++++++++++++++++ 65 files changed, 1820 insertions(+), 616 deletions(-) rename nifi-connector-mock-bundle/nifi-connector-mock-server/src/main/java/org/apache/nifi/mock/connector/server/{NopPropertyEncryptor.java => MockPropertyEncryptionProvider.java} (56%) create mode 100644 nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/security/encryption/PropertyEncryptionProviderFactory.java create mode 100644 nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/security/encryption/ProviderSensitiveValueDecryptor.java delete mode 100644 nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/PropertyDecryptor.java delete mode 100644 nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/mapping/SensitiveValueEncryptor.java create mode 100644 nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/security/encryption/InternalPassThroughPropertyEncryptionProvider.java create mode 100644 nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/security/encryption/PropertyEncryptionEncoder.java create mode 100644 nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/security/encryption/SensitivePropertyCodec.java create mode 100644 nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/security/encryption/SensitivePropertyContextFactory.java create mode 100644 nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/test/java/org/apache/nifi/security/encryption/InternalPassThroughPropertyEncryptionProviderTest.java create mode 100644 nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/test/java/org/apache/nifi/security/encryption/PropertyEncryptionEncoderTest.java create mode 100644 nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/test/java/org/apache/nifi/security/encryption/SensitivePropertyCodecTest.java create mode 100644 nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/test/java/org/apache/nifi/security/encryption/SensitivePropertyContextFactoryTest.java delete mode 100644 nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/encrypt/PropertyEncryptorFactory.java create mode 100644 nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/SensitiveValueDecryptor.java create mode 100644 nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/encryption/PropertyEncryptionProviderIT.java diff --git a/minifi/minifi-assembly/pom.xml b/minifi/minifi-assembly/pom.xml index 6702d1a71cd8..bf1b429fcc22 100644 --- a/minifi/minifi-assembly/pom.xml +++ b/minifi/minifi-assembly/pom.xml @@ -202,6 +202,12 @@ limitations under the License. 2.12.0-SNAPSHOT nar + + org.apache.nifi + nifi-password-based-property-encryption-nar + 2.12.0-SNAPSHOT + nar + org.apache.nifi nifi-record-serialization-services-nar diff --git a/minifi/minifi-commons/minifi-commons-framework/pom.xml b/minifi/minifi-commons/minifi-commons-framework/pom.xml index 1793d51d122a..30b4a0b68b55 100644 --- a/minifi/minifi-commons/minifi-commons-framework/pom.xml +++ b/minifi/minifi-commons/minifi-commons-framework/pom.xml @@ -79,11 +79,6 @@ limitations under the License. nifi-property-encryptor 2.12.0-SNAPSHOT - - org.apache.nifi - nifi-framework-core - 2.12.0-SNAPSHOT - diff --git a/minifi/minifi-commons/minifi-commons-framework/src/main/java/org/apache/nifi/minifi/commons/service/StandardFlowPropertyEncryptor.java b/minifi/minifi-commons/minifi-commons-framework/src/main/java/org/apache/nifi/minifi/commons/service/StandardFlowPropertyEncryptor.java index bc4c0ea38b93..a058f442d967 100644 --- a/minifi/minifi-commons/minifi-commons-framework/src/main/java/org/apache/nifi/minifi/commons/service/StandardFlowPropertyEncryptor.java +++ b/minifi/minifi-commons/minifi-commons-framework/src/main/java/org/apache/nifi/minifi/commons/service/StandardFlowPropertyEncryptor.java @@ -21,12 +21,12 @@ import org.apache.nifi.c2.protocol.component.api.PropertyDescriptor; import org.apache.nifi.c2.protocol.component.api.RuntimeManifest; import org.apache.nifi.controller.flow.VersionedDataflow; -import org.apache.nifi.controller.serialization.FlowSerializer; import org.apache.nifi.encrypt.PropertyEncryptor; import org.apache.nifi.flow.VersionedConfigurableExtension; import org.apache.nifi.flow.VersionedParameter; import org.apache.nifi.flow.VersionedProcessGroup; import org.apache.nifi.flow.VersionedPropertyDescriptor; +import org.apache.nifi.security.encryption.PropertyEncryptionEncoder; import java.util.List; import java.util.Map; @@ -41,12 +41,9 @@ import static java.util.stream.Collectors.toMap; import static java.util.stream.Collectors.toSet; import static java.util.stream.Stream.concat; -import static org.apache.commons.lang3.StringUtils.EMPTY; public class StandardFlowPropertyEncryptor implements FlowPropertyEncryptor { - private static final String ENCRYPTED_FORMAT = "enc{%s}"; - private final PropertyEncryptor propertyEncryptor; private final RuntimeManifest runTimeManifest; @@ -72,7 +69,7 @@ private void encryptParameterContextsProperties(VersionedDataflow flow) { .forEach(parameterContext -> ofNullable(parameterContext.getParameters()).orElse(Set.of()) .stream() .filter(VersionedParameter::isSensitive) - .filter(not(parameter -> ofNullable(parameter.getValue()).orElse(EMPTY).startsWith(FlowSerializer.ENC_PREFIX))) + .filter(not(parameter -> PropertyEncryptionEncoder.isEncrypted(parameter.getValue()))) .forEach(parameter -> parameter.setValue(encrypt(parameter.getValue())))); } @@ -150,13 +147,13 @@ private Set mergeSets(Set first, Set second) { private Function, String> encryptPropertyIfNeeded(Set sensitivePropertyNames) { return entry -> - sensitivePropertyNames.contains(entry.getKey()) && !entry.getValue().startsWith(FlowSerializer.ENC_PREFIX) + sensitivePropertyNames.contains(entry.getKey()) && !PropertyEncryptionEncoder.isEncrypted(entry.getValue()) ? encrypt(entry.getValue()) : entry.getValue(); } private String encrypt(String parameter) { - return String.format(ENCRYPTED_FORMAT, propertyEncryptor.encrypt(parameter)); + return PropertyEncryptionEncoder.getEncoded(propertyEncryptor.encrypt(parameter)); } } diff --git a/minifi/minifi-commons/minifi-commons-framework/src/test/java/org/apache/nifi/minifi/commons/service/StandardFlowPropertyEncryptorTest.java b/minifi/minifi-commons/minifi-commons-framework/src/test/java/org/apache/nifi/minifi/commons/service/StandardFlowPropertyEncryptorTest.java index ffc025214479..326f67822147 100644 --- a/minifi/minifi-commons/minifi-commons-framework/src/test/java/org/apache/nifi/minifi/commons/service/StandardFlowPropertyEncryptorTest.java +++ b/minifi/minifi-commons/minifi-commons-framework/src/test/java/org/apache/nifi/minifi/commons/service/StandardFlowPropertyEncryptorTest.java @@ -25,7 +25,6 @@ import org.apache.nifi.c2.protocol.component.api.PropertyDescriptor; import org.apache.nifi.c2.protocol.component.api.RuntimeManifest; import org.apache.nifi.controller.flow.VersionedDataflow; -import org.apache.nifi.controller.serialization.FlowSerializer; import org.apache.nifi.encrypt.PropertyEncryptor; import org.apache.nifi.flow.VersionedConfigurableExtension; import org.apache.nifi.flow.VersionedControllerService; @@ -34,6 +33,7 @@ import org.apache.nifi.flow.VersionedProcessGroup; import org.apache.nifi.flow.VersionedProcessor; import org.apache.nifi.flow.VersionedPropertyDescriptor; +import org.apache.nifi.security.encryption.PropertyEncryptionEncoder; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -126,9 +126,9 @@ public void shouldEncryptParameterContextsSensitiveVariables() { .flatMap(context -> context.getParameters().stream()) .forEach(parameter -> { if (parameter.isSensitive()) { - assertTrue(parameter.getValue().startsWith(FlowSerializer.ENC_PREFIX)); + assertTrue(PropertyEncryptionEncoder.isEncrypted(parameter.getValue())); } else { - assertFalse(parameter.getValue().startsWith(FlowSerializer.ENC_PREFIX)); + assertFalse(PropertyEncryptionEncoder.isEncrypted(parameter.getValue())); } }); } @@ -310,9 +310,9 @@ private void assertSensitiveFlowComponentPropertiesAreEncoded(VersionedDataflow .flatMap(properties -> properties.entrySet().stream()) .forEach(propertyEntry -> { if (propertyEntry.getKey().startsWith(SENSITIVE_PROPERTY_NAME_PREFIX)) { - assertTrue(propertyEntry.getValue().startsWith(FlowSerializer.ENC_PREFIX)); + assertTrue(PropertyEncryptionEncoder.isEncrypted(propertyEntry.getValue())); } else { - assertFalse(propertyEntry.getValue().startsWith(FlowSerializer.ENC_PREFIX)); + assertFalse(PropertyEncryptionEncoder.isEncrypted(propertyEntry.getValue())); } }); } diff --git a/nifi-connector-mock-bundle/nifi-connector-mock-server/pom.xml b/nifi-connector-mock-bundle/nifi-connector-mock-server/pom.xml index 21446531473a..84ca03a65a1d 100644 --- a/nifi-connector-mock-bundle/nifi-connector-mock-server/pom.xml +++ b/nifi-connector-mock-bundle/nifi-connector-mock-server/pom.xml @@ -45,11 +45,6 @@ 2.12.0-SNAPSHOT provided - - org.apache.nifi - nifi-property-encryptor - 2.12.0-SNAPSHOT - org.apache.nifi nifi-connector-mock-api diff --git a/nifi-connector-mock-bundle/nifi-connector-mock-server/src/main/java/org/apache/nifi/mock/connector/server/NopPropertyEncryptor.java b/nifi-connector-mock-bundle/nifi-connector-mock-server/src/main/java/org/apache/nifi/mock/connector/server/MockPropertyEncryptionProvider.java similarity index 56% rename from nifi-connector-mock-bundle/nifi-connector-mock-server/src/main/java/org/apache/nifi/mock/connector/server/NopPropertyEncryptor.java rename to nifi-connector-mock-bundle/nifi-connector-mock-server/src/main/java/org/apache/nifi/mock/connector/server/MockPropertyEncryptionProvider.java index 56fbe816b7f3..c4ca0e26b5c5 100644 --- a/nifi-connector-mock-bundle/nifi-connector-mock-server/src/main/java/org/apache/nifi/mock/connector/server/NopPropertyEncryptor.java +++ b/nifi-connector-mock-bundle/nifi-connector-mock-server/src/main/java/org/apache/nifi/mock/connector/server/MockPropertyEncryptionProvider.java @@ -17,16 +17,25 @@ package org.apache.nifi.mock.connector.server; -import org.apache.nifi.encrypt.PropertyEncryptor; +import org.apache.nifi.security.encryption.PropertyEncryptionProvider; +import org.apache.nifi.security.encryption.PropertyEncryptionProviderInitializationContext; +import org.apache.nifi.security.encryption.SensitivePropertyContext; + +/** + * Property Encryption Provider that returns values unchanged, because the mock server does not persist the flow. + */ +public class MockPropertyEncryptionProvider implements PropertyEncryptionProvider { + @Override + public void initialize(final PropertyEncryptionProviderInitializationContext context) { + } -public class NopPropertyEncryptor implements PropertyEncryptor { @Override - public String encrypt(final String property) { + public byte[] encrypt(final byte[] property, final SensitivePropertyContext context) { return property; } @Override - public String decrypt(final String encryptedProperty) { + public byte[] decrypt(final byte[] encryptedProperty, final SensitivePropertyContext context) { return encryptedProperty; } } diff --git a/nifi-connector-mock-bundle/nifi-connector-mock-server/src/main/java/org/apache/nifi/mock/connector/server/StandardConnectorMockServer.java b/nifi-connector-mock-bundle/nifi-connector-mock-server/src/main/java/org/apache/nifi/mock/connector/server/StandardConnectorMockServer.java index bbca1347a4de..1c40b6486117 100644 --- a/nifi-connector-mock-bundle/nifi-connector-mock-server/src/main/java/org/apache/nifi/mock/connector/server/StandardConnectorMockServer.java +++ b/nifi-connector-mock-bundle/nifi-connector-mock-server/src/main/java/org/apache/nifi/mock/connector/server/StandardConnectorMockServer.java @@ -57,7 +57,6 @@ import org.apache.nifi.controller.status.history.StatusHistoryRepository; import org.apache.nifi.controller.status.history.VolatileComponentStatusRepository; import org.apache.nifi.diagnostics.DiagnosticsFactory; -import org.apache.nifi.encrypt.PropertyEncryptor; import org.apache.nifi.engine.FlowEngine; import org.apache.nifi.events.VolatileBulletinRepository; import org.apache.nifi.flow.VersionedExternalFlow; @@ -137,7 +136,6 @@ public void start() { final FlowFileEventRepository flowFileEventRepository = new RingBufferEventRepository(5); final Authorizer authorizer = new PermitAllAuthorizer(); final AuditService auditService = new MockAuditService(); - final PropertyEncryptor propertyEncryptor = new NopPropertyEncryptor(); final BulletinRepository bulletinRepository = new VolatileBulletinRepository(); final StatusHistoryRepository statusHistoryRepository = new VolatileComponentStatusRepository(nifiProperties); final RuleViolationsManager ruleViolationManager = new MockRuleViolationsManager(); @@ -150,7 +148,7 @@ public void start() { authorizer, auditService, new DefaultComponentMetricReporter(), - propertyEncryptor, + new MockPropertyEncryptionProvider(), bulletinRepository, extensionManager, statusHistoryRepository, @@ -515,7 +513,6 @@ private VersionedExternalFlow createFlowSnapshot(final FrameworkFlowContext flow .mapSensitiveConfiguration(false) .mapPropertyDescriptors(true) .stateLookup(VersionedComponentStateLookup.ENABLED_OR_DISABLED) - .sensitiveValueEncryptor(value -> value) .componentIdLookup(ComponentIdLookup.VERSIONED_OR_GENERATE) .mapInstanceIdentifiers(true) .mapControllerServiceReferencesToVersionedId(true) diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc b/nifi-docs/src/main/asciidoc/administration-guide.adoc index 5836a0d8b943..2477e3956a02 100644 --- a/nifi-docs/src/main/asciidoc/administration-guide.adoc +++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc @@ -1928,16 +1928,39 @@ Clustered installations of NiFi require the same value to be configured on all n A Property Encryption Provider is a framework extension, packaged in a NiFi Archive, for protecting sensitive values. NiFi loads and initializes the provider during startup using the class name configured in -`nifi.security.property.encryption.provider.implementation`, and does not load a provider when the property is empty. -When the property is missing, NiFi loads the standard password-based implementation to support existing configurations. +`nifi.security.property.encryption.provider.implementation`. When the property is empty or missing, NiFi loads the +standard password-based implementation to support existing configurations. The standard distribution includes `org.apache.nifi.security.encryption.password.PasswordBasedPropertyEncryptionProvider`, which derives a secret key from `nifi.sensitive.props.key` using the Key Derivation Function selected in `nifi.sensitive.props.algorithm`. This provider reads and writes the encrypted representation described in <>. -Flow serialization continues to use `nifi.sensitive.props.key` and `nifi.sensitive.props.algorithm` directly. -Loading a Property Encryption Provider does not change the encryption used for persisted flow configuration. +The configured provider protects the following values: + +* Sensitive component properties written to and read from the persisted flow configuration +* Sensitive Parameter values written to and read from the persisted flow configuration +* OpenID Connect authorized client tokens stored on behalf of authenticated users + +Because the standard password-based provider produces the representation described in +<>, a flow configuration written by an earlier version of NiFi continues to load +without additional migration. + +==== Sensitive Property Context + +NiFi supplies a Sensitive Property Context with every encryption and decryption operation. The context carries a +category describing the kind of value being protected, together with attributes describing where the value is located. +Component properties supply the component identifier, the component type, and the property name. Parameters supply the +Parameter Context name and the Parameter name. Authorization tokens carry no attributes, because nothing describing the +authenticated user is known before the stored token is decrypted. + +An implementation can supply the context to an external Key Management Service as encryption context, which binds the +encrypted value to its location in the flow and records the location for auditing. NiFi supplies the same context when +decrypting a value as it supplied when the value was encrypted. An implementation that binds the context to the +encrypted value must therefore be prepared for a value to become unreadable if the component or the Parameter that owns +it is renamed or recreated with a different identifier. + +==== Provider Configuration A provider can read additional configuration from properties that begin with `nifi.security.property.encryption.provider.properties.`. NiFi removes the prefix before supplying the properties to the @@ -1945,6 +1968,10 @@ provider, so `nifi.security.property.encryption.provider.properties.endpoint` is Providers that call remote services, such as implementations backed by an external Key Management Service, can also use the TLS configuration that NiFi supplies during initialization. +Changing the configured provider, or changing the key material that a provider uses, makes existing encrypted values +unreadable. Sensitive values must be reentered after such a change unless the new configuration resolves to the same +key material. + [[admin-toolkit]] == NiFi Toolkit Administrative Tools The NiFi Toolkit also contains command line utilities for administrators to support NiFi maintenance in standalone and clustered environments. @@ -3622,7 +3649,7 @@ These properties pertain to various security features in NiFi. Many of these pro |*Property*|*Description* |`nifi.sensitive.props.key`|This is the password used to encrypt any sensitive property values that are configured in processors. By default, it is blank, but the system administrator should provide a value for it. It can be a string of any length, although the recommended minimum length is 10 characters. Be aware that once this password is set and one or more sensitive processor properties have been configured, this password should not be changed. |`nifi.sensitive.props.algorithm`|The algorithm used to encrypt sensitive properties. The default value is `NIFI_PBKDF2_AES_GCM_256`. -|`nifi.security.property.encryption.provider.implementation`|The class name of the Property Encryption Provider loaded and initialized during application startup. The default value is `org.apache.nifi.security.encryption.password.PasswordBasedPropertyEncryptionProvider`, which derives a secret key from `nifi.sensitive.props.key`. NiFi does not load a provider when the value is blank. See <> for additional information. +|`nifi.security.property.encryption.provider.implementation`|The class name of the Property Encryption Provider loaded and initialized during application startup. The provider protects sensitive component properties, sensitive Parameter values, and OpenID Connect authorized client tokens. The default value is `org.apache.nifi.security.encryption.password.PasswordBasedPropertyEncryptionProvider`, which derives a secret key from `nifi.sensitive.props.key`. NiFi loads the default implementation when the value is blank. See <> for additional information. |`nifi.security.property.encryption.provider.properties.`|The prefix for properties supplied to the configured Property Encryption Provider. NiFi removes the prefix from the property name before supplying the property to the provider. No properties are configured by default. |`nifi.security.autoreload.enabled`|Specifies whether the SSL context factory should be automatically reloaded if updates to the keystore and truststore are detected. By default, it is set to `false`. |`nifi.security.autoreload.interval`|Specifies the interval at which the keystore and truststore are checked for updates. Only applies if `nifi.security.autoreload.enabled` is set to `true`. The default value is `10 secs`. diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-components/pom.xml b/nifi-framework-bundle/nifi-framework/nifi-framework-components/pom.xml index cbb001b4511b..62ba5dcfd420 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-components/pom.xml +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-components/pom.xml @@ -120,11 +120,6 @@ org.apache.nifi nifi-nar-utils - - org.apache.nifi - nifi-property-encryptor - 2.12.0-SNAPSHOT - org.apache.nifi nifi-xml-processing diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/flowanalysis/FlowAnalysisUtil.java b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/flowanalysis/FlowAnalysisUtil.java index 6e6c58bd9287..8feab459d7fd 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/flowanalysis/FlowAnalysisUtil.java +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/flowanalysis/FlowAnalysisUtil.java @@ -21,6 +21,11 @@ import org.apache.nifi.registry.flow.mapping.FlowMappingOptions; import org.apache.nifi.registry.flow.mapping.VersionedComponentFlowMapper; import org.apache.nifi.registry.flow.mapping.VersionedComponentStateLookup; +import org.apache.nifi.security.encryption.PropertyEncryptionProvider; +import org.apache.nifi.security.encryption.PropertyEncryptionProviderInitializationContext; +import org.apache.nifi.security.encryption.SensitivePropertyContext; + +import java.nio.charset.StandardCharsets; public class FlowAnalysisUtil { public static final String ENCRYPTED_SENSITIVE_VALUE_SUBSTITUTE = "*****"; @@ -32,7 +37,7 @@ public static VersionedComponentFlowMapper createMapper(ExtensionManager extensi .stateLookup(VersionedComponentStateLookup.IDENTITY_LOOKUP) .componentIdLookup(ComponentIdLookup.USE_COMPONENT_ID) .mapSensitiveConfiguration(true) - .sensitiveValueEncryptor(value -> ENCRYPTED_SENSITIVE_VALUE_SUBSTITUTE) + .propertyEncryptionProvider(new PlaceholderPropertyEncryptionProvider()) .mapAssetReferences(true) .build(); @@ -43,7 +48,7 @@ public String getGroupId(String groupId) { } @Override - protected String encrypt(String value) { + protected String encrypt(String value, SensitivePropertyContext context) { return ENCRYPTED_SENSITIVE_VALUE_SUBSTITUTE; } }; @@ -51,4 +56,21 @@ protected String encrypt(String value) { return mapper; } + private static class PlaceholderPropertyEncryptionProvider implements PropertyEncryptionProvider { + + @Override + public void initialize(PropertyEncryptionProviderInitializationContext context) { + + } + + @Override + public byte[] encrypt(byte[] property, SensitivePropertyContext context) { + return ENCRYPTED_SENSITIVE_VALUE_SUBSTITUTE.getBytes(StandardCharsets.UTF_8); + } + + @Override + public byte[] decrypt(byte[] encryptedProperty, SensitivePropertyContext context) { + return encryptedProperty; + } + } } diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizer.java b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizer.java index 0b233d489bbd..e7930d84c329 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizer.java +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizer.java @@ -52,7 +52,6 @@ import org.apache.nifi.controller.service.ControllerServiceNode; import org.apache.nifi.controller.service.ControllerServiceProvider; import org.apache.nifi.controller.service.ControllerServiceState; -import org.apache.nifi.encrypt.EncryptionException; import org.apache.nifi.flow.BatchSize; import org.apache.nifi.flow.Bundle; import org.apache.nifi.flow.ComponentType; @@ -63,6 +62,7 @@ import org.apache.nifi.flow.VersionedAsset; import org.apache.nifi.flow.VersionedComponent; import org.apache.nifi.flow.VersionedComponentState; +import org.apache.nifi.flow.VersionedConfigurableExtension; import org.apache.nifi.flow.VersionedConnection; import org.apache.nifi.flow.VersionedControllerService; import org.apache.nifi.flow.VersionedExternalFlow; @@ -88,7 +88,6 @@ import org.apache.nifi.groups.FlowSynchronizationOptions; import org.apache.nifi.groups.FlowSynchronizationOptions.ComponentStopTimeoutAction; import org.apache.nifi.groups.ProcessGroup; -import org.apache.nifi.groups.PropertyDecryptor; import org.apache.nifi.groups.RemoteProcessGroup; import org.apache.nifi.groups.RemoteProcessGroupPortDescriptor; import org.apache.nifi.groups.StandardVersionedFlowStatus; @@ -119,6 +118,7 @@ import org.apache.nifi.registry.flow.diff.FlowComparatorVersionedStrategy; import org.apache.nifi.registry.flow.diff.FlowComparison; import org.apache.nifi.registry.flow.diff.FlowDifference; +import org.apache.nifi.registry.flow.diff.SensitiveValueDecryptor; import org.apache.nifi.registry.flow.diff.StandardComparableDataFlow; import org.apache.nifi.registry.flow.diff.StandardFlowComparator; import org.apache.nifi.registry.flow.diff.StaticDifferenceDescriptor; @@ -130,6 +130,12 @@ import org.apache.nifi.remote.protocol.SiteToSiteTransportProtocol; import org.apache.nifi.scheduling.ExecutionNode; import org.apache.nifi.scheduling.SchedulingStrategy; +import org.apache.nifi.security.encryption.PropertyEncryptionEncoder; +import org.apache.nifi.security.encryption.PropertyEncryptionException; +import org.apache.nifi.security.encryption.ProviderSensitiveValueDecryptor; +import org.apache.nifi.security.encryption.SensitivePropertyCodec; +import org.apache.nifi.security.encryption.SensitivePropertyContext; +import org.apache.nifi.security.encryption.SensitivePropertyContextFactory; import org.apache.nifi.util.FlowDifferenceFilters; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -163,9 +169,6 @@ public class StandardVersionedComponentSynchronizer implements VersionedComponentSynchronizer { private static final Logger LOG = LoggerFactory.getLogger(StandardVersionedComponentSynchronizer.class); private static final String TEMP_FUNNEL_ID_SUFFIX = "-temp-funnel"; - public static final String ENC_PREFIX = "enc{"; - public static final String ENC_SUFFIX = "}"; - private static final ParameterParser agnosticParameterParser = new ExpressionLanguageAgnosticParameterParser(); private final VersionedFlowSynchronizationContext context; private final Set updatedVersionedComponentIds = new HashSet<>(); @@ -330,9 +333,11 @@ public void synchronize(final ProcessGroup group, final VersionedExternalFlow ve final ComparableDataFlow localFlow = new StandardComparableDataFlow("Currently Loaded Flow", versionedGroup); final ComparableDataFlow proposedFlow = new StandardComparableDataFlow("Proposed Flow", versionedExternalFlow.getFlowContents()); - final PropertyDecryptor decryptor = options.getPropertyDecryptor(); + final SensitiveValueDecryptor sensitiveValueDecryptor = options.isDropEncryptedValues() + ? (owner, valueName, encryptedValue) -> null + : new ProviderSensitiveValueDecryptor(options.getPropertyEncryptionProvider()); final FlowComparator flowComparator = new StandardFlowComparator(localFlow, proposedFlow, - new StaticDifferenceDescriptor(), decryptor::decrypt, options.getComponentComparisonIdLookup(), FlowComparatorVersionedStrategy.DEEP); + new StaticDifferenceDescriptor(), sensitiveValueDecryptor, options.getComponentComparisonIdLookup(), FlowComparatorVersionedStrategy.DEEP); final FlowComparison flowComparison = flowComparator.compare(); updatedVersionedComponentIds.clear(); @@ -1460,7 +1465,7 @@ private ControllerServiceNode addControllerService(final ProcessGroup destinatio destination.addControllerService(newService); } - final Map decryptedProperties = getDecryptedProperties(proposed.getProperties()); + final Map decryptedProperties = getDecryptedProperties(proposed, proposed.getProperties()); createdAndModifiedExtensions.add(new CreatedOrModifiedExtension(newService, decryptedProperties)); updateControllerService(newService, proposed, topLevelGroup); @@ -1604,7 +1609,7 @@ private void updateControllerService(final ControllerServiceNode service, final } final Set sensitiveDynamicPropertyNames = getSensitiveDynamicPropertyNames(service, proposed.getProperties(), proposed.getPropertyDescriptors().values()); - final Map properties = populatePropertiesMap(service, proposed.getProperties(), proposed.getPropertyDescriptors(), service.getProcessGroup(), topLevelGroup); + final Map properties = populatePropertiesMap(service, proposed, proposed.getProperties(), proposed.getPropertyDescriptors(), service.getProcessGroup(), topLevelGroup); service.setProperties(properties, true, sensitiveDynamicPropertyNames); } finally { @@ -1631,7 +1636,7 @@ private Set getSensitiveDynamicPropertyNames( // Find Encrypted Property values and find associated dynamic Property Descriptor names proposedProperties.entrySet() .stream() - .filter(entry -> isValueEncrypted(entry.getValue())) + .filter(entry -> PropertyEncryptionEncoder.isEncrypted(entry.getValue())) .map(Map.Entry::getKey) .map(componentNode::getPropertyDescriptor) .filter(PropertyDescriptor::isDynamic) @@ -1641,9 +1646,14 @@ private Set getSensitiveDynamicPropertyNames( return sensitiveDynamicPropertyNames; } - private Map populatePropertiesMap(final ComponentNode componentNode, final Map proposedProperties, - final Map proposedPropertyDescriptors, - final ProcessGroup group, final ProcessGroup topLevelGroup) { + private Map populatePropertiesMap( + final ComponentNode componentNode, + final VersionedComponent proposedComponent, + final Map proposedProperties, + final Map proposedPropertyDescriptors, + final ProcessGroup group, + final ProcessGroup topLevelGroup + ) { // Explicitly set all existing properties to null, except for sensitive properties, so that if there isn't an entry in the proposedProperties // it will get removed from the processor. We don't do this for sensitive properties because when we retrieve the VersionedProcessGroup from registry, @@ -1717,7 +1727,10 @@ private Map populatePropertiesMap(final ComponentNode componentN // so we want to continue on and update the value to null. } - fullPropertyMap.put(propertyName, decrypt(value, syncOptions.getPropertyDecryptor())); + final String resolvedValue = PropertyEncryptionEncoder.isEncrypted(value) + ? decrypt(value, getSensitivePropertyContext(proposedComponent, propertyName)) + : value; + fullPropertyMap.put(propertyName, resolvedValue); } } @@ -1728,34 +1741,54 @@ private static boolean isReferencingParameter(String proposedValue) { return !agnosticParameterParser.parseTokens(proposedValue).toReferenceList().isEmpty(); } - private Map getDecryptedProperties(final Map properties) { + private Map getDecryptedProperties(final VersionedComponent component, final Map properties) { final Map decryptedProperties = new LinkedHashMap<>(); - final PropertyDecryptor decryptor = syncOptions.getPropertyDecryptor(); properties.forEach((propertyName, propertyValue) -> { - final String propertyValueDecrypted = decrypt(propertyValue, decryptor); - decryptedProperties.put(propertyName, propertyValueDecrypted); + final String resolvedValue = PropertyEncryptionEncoder.isEncrypted(propertyValue) + ? decrypt(propertyValue, getSensitivePropertyContext(component, propertyName)) + : propertyValue; + decryptedProperties.put(propertyName, resolvedValue); }); return decryptedProperties; } - private static String decrypt(final String value, final PropertyDecryptor decryptor) { - if (isValueEncrypted(value)) { + /** + * Get the context describing a sensitive property of the proposed component. The instance identifier is used rather than the + * identifier, because the identifier of a mapped component is a generated versioned identifier while the context supplied when + * the value was encrypted described the component instance. + */ + private static SensitivePropertyContext getSensitivePropertyContext(final VersionedComponent component, final String propertyName) { + final String componentType = component instanceof final VersionedConfigurableExtension extension ? extension.getType() : null; + return SensitivePropertyContextFactory.forComponent(component.getInstanceIdentifier(), componentType, propertyName); + } + + /** + * Decrypt a sensitive value read from a versioned flow. Callers must confirm that the value is encrypted using + * {@link PropertyEncryptionEncoder#isEncrypted(String)}, because a versioned flow also stores sensitive values as + * plaintext when the value is a Parameter reference or when the flow was mapped without a Property Encryption Provider. + * + * @param value Encrypted value wrapped with the standard prefix and suffix + * @param context Context describing the sensitive property, which must equal the context supplied on encryption + * @return Decrypted value, or null when synchronization is configured to drop encrypted values + */ + private String decrypt(final String value, final SensitivePropertyContext context) { + final String encryptedValue = PropertyEncryptionEncoder.getDecoded(value); + + final String decrypted; + + if (syncOptions.isDropEncryptedValues()) { + decrypted = null; + } else { try { - return decryptor.decrypt(value.substring(ENC_PREFIX.length(), value.length() - ENC_SUFFIX.length())); - } catch (EncryptionException e) { - final String moreDescriptiveMessage = "There was a problem decrypting a sensitive flow configuration value. " + - "Check that the nifi.sensitive.props.key value in nifi.properties matches the value used to encrypt the flow.json.gz file"; - throw new EncryptionException(moreDescriptiveMessage, e); + decrypted = SensitivePropertyCodec.decrypt(syncOptions.getPropertyEncryptionProvider(), encryptedValue, context); + } catch (final PropertyEncryptionException e) { + throw new PropertyEncryptionException("Configured Property Encryption Provider failed to decrypt sensitive values", e); } - } else { - return value; } - } - private static boolean isValueEncrypted(final String value) { - return value != null && value.startsWith(ENC_PREFIX) && value.endsWith(ENC_SUFFIX); + return decrypted; } private void verifyCanSynchronize(final ParameterContext parameterContext, final VersionedParameterContext proposed) throws FlowSynchronizationException { @@ -2797,7 +2830,7 @@ private ProcessorNode addProcessor(final ProcessGroup destination, final Version destination.addProcessor(procNode); - final Map decryptedProperties = getDecryptedProperties(proposed.getProperties()); + final Map decryptedProperties = getDecryptedProperties(proposed, proposed.getProperties()); createdAndModifiedExtensions.add(new CreatedOrModifiedExtension(procNode, decryptedProperties)); updateProcessor(procNode, proposed, topLevelGroup); @@ -3127,7 +3160,7 @@ private void updateProcessor(final ProcessorNode processor, final VersionedProce } final Set sensitiveDynamicPropertyNames = getSensitiveDynamicPropertyNames(processor, proposed.getProperties(), proposed.getPropertyDescriptors().values()); - final Map properties = populatePropertiesMap(processor, proposed.getProperties(), proposed.getPropertyDescriptors(), processor.getProcessGroup(), topLevelGroup); + final Map properties = populatePropertiesMap(processor, proposed, proposed.getProperties(), proposed.getPropertyDescriptors(), processor.getProcessGroup(), topLevelGroup); processor.setProperties(properties, true, sensitiveDynamicPropertyNames); processor.setRunDuration(proposed.getRunDurationMillis(), TimeUnit.MILLISECONDS); processor.setSchedulingStrategy(SchedulingStrategy.valueOf(proposed.getSchedulingStrategy())); @@ -3324,7 +3357,12 @@ private void updateRemoteProcessGroup(final RemoteProcessGroup rpg, final Versio rpg.setProxyHost(proposed.getProxyHost()); rpg.setProxyPort(proposed.getProxyPort()); rpg.setProxyUser(proposed.getProxyUser()); - rpg.setProxyPassword(decrypt(proposed.getProxyPassword(), syncOptions.getPropertyDecryptor())); + + final String proposedProxyPassword = proposed.getProxyPassword(); + final String proxyPassword = PropertyEncryptionEncoder.isEncrypted(proposedProxyPassword) + ? decrypt(proposedProxyPassword, SensitivePropertyContextFactory.forRemoteProcessGroupProxyPassword(proposed.getInstanceIdentifier())) + : proposedProxyPassword; + rpg.setProxyPassword(proxyPassword); rpg.setTransportProtocol(SiteToSiteTransportProtocol.valueOf(proposed.getTransportProtocol())); rpg.setYieldDuration(proposed.getYieldDuration()); @@ -3882,7 +3920,7 @@ private ReportingTaskNode addReportingTask(final VersionedReportingTask reportin final ReportingTaskNode taskNode = context.getFlowManager().createReportingTask(reportingTask.getType(), reportingTask.getInstanceIdentifier(), coordinate, false); updateReportingTask(taskNode, reportingTask); - final Map decryptedProperties = getDecryptedProperties(reportingTask.getProperties()); + final Map decryptedProperties = getDecryptedProperties(reportingTask, reportingTask.getProperties()); createdAndModifiedExtensions.add(new CreatedOrModifiedExtension(taskNode, decryptedProperties)); return taskNode; diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java index f2b41766f27e..600178a68661 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java @@ -66,7 +66,6 @@ import org.apache.nifi.controller.service.ControllerServiceProvider; import org.apache.nifi.controller.service.ControllerServiceState; import org.apache.nifi.controller.service.StandardConfigurationContext; -import org.apache.nifi.encrypt.PropertyEncryptor; import org.apache.nifi.flow.ExecutionEngine; import org.apache.nifi.flow.VersionedComponent; import org.apache.nifi.flow.VersionedExternalFlow; @@ -115,6 +114,9 @@ import org.apache.nifi.registry.flow.mapping.VersionedComponentStateLookup; import org.apache.nifi.remote.PublicPort; import org.apache.nifi.remote.RemoteGroupPort; +import org.apache.nifi.security.encryption.InternalPassThroughPropertyEncryptionProvider; +import org.apache.nifi.security.encryption.PropertyEncryptionProvider; +import org.apache.nifi.security.encryption.ProviderSensitiveValueDecryptor; import org.apache.nifi.util.FlowDifferenceFilters; import org.apache.nifi.util.FormatUtils; import org.apache.nifi.util.NiFiProperties; @@ -196,7 +198,12 @@ public final class StandardProcessGroup implements ProcessGroup { private final Map processors = new HashMap<>(); private final Map funnels = new HashMap<>(); private final Map controllerServices = new ConcurrentHashMap<>(); - private final PropertyEncryptor encryptor; + /** + * Provider that protects sensitive values when a flow is persisted and restored, and that recovers them for flow + * comparison. Null in the Stateless runtime, which neither restores a persisted flow nor places a group under + * version control, so no sensitive value passes through this group. + */ + private final PropertyEncryptionProvider propertyEncryptionProvider; private final VersionControlFields versionControlFields = new VersionControlFields(); private volatile ParameterContext parameterContext; private final NodeTypeProvider nodeTypeProvider; @@ -237,7 +244,8 @@ public final class StandardProcessGroup implements ProcessGroup { private volatile String logFileSuffix; public StandardProcessGroup(final String id, final ControllerServiceProvider serviceProvider, final ProcessScheduler scheduler, - final PropertyEncryptor encryptor, final ExtensionManager extensionManager, + final PropertyEncryptionProvider propertyEncryptionProvider, + final ExtensionManager extensionManager, final StateManagerProvider stateManagerProvider, final FlowManager flowManager, final ReloadComponent reloadComponent, final NodeTypeProvider nodeTypeProvider, final ClusterTopologyProvider clusterTopologyProvider, @@ -249,7 +257,7 @@ public StandardProcessGroup(final String id, final ControllerServiceProvider ser this.parent = new AtomicReference<>(); this.scheduler = scheduler; this.comments = new AtomicReference<>(""); - this.encryptor = encryptor; + this.propertyEncryptionProvider = propertyEncryptionProvider; this.extensionManager = extensionManager; this.stateManagerProvider = stateManagerProvider; this.flowManager = flowManager; @@ -3945,7 +3953,7 @@ public ComponentAdditions addVersionedComponents(final VersionedComponentAdditio .componentIdGenerator(idGenerator) .componentComparisonIdLookup(VersionedComponent::getIdentifier) .componentScheduler(ComponentScheduler.NOP_SCHEDULER) - .propertyDecryptor(value -> null) + .dropEncryptedValues(true) .build(); writeLock.lock(); @@ -3982,10 +3990,11 @@ public void updateFlow(final VersionedExternalFlow proposedSnapshot, final Strin // survive a version-control update rather than being reverted to the name stored in the registry. .preservePublicPortNames(true); // Connectors should not have encrypted values copied from versioned flow. However we do need to decrypt parameter references. + // A Connector flow is mapped in memory using the Pass Through Provider, so the same Provider recovers the original values. if (getConnectorIdentifier().isPresent()) { - flowSynchronizationBuilder.propertyDecryptor(value -> value); + flowSynchronizationBuilder.propertyEncryptionProvider(new InternalPassThroughPropertyEncryptionProvider()); } else { - flowSynchronizationBuilder.propertyDecryptor(value -> null); + flowSynchronizationBuilder.dropEncryptedValues(true); } final FlowSynchronizationOptions synchronizationOptions = flowSynchronizationBuilder.build(); @@ -3994,7 +4003,7 @@ public void updateFlow(final VersionedExternalFlow proposedSnapshot, final Strin .mapSensitiveConfiguration(false) .mapPropertyDescriptors(true) .stateLookup(stateLookup) - .sensitiveValueEncryptor(null) + .propertyEncryptionProvider(null) .componentIdLookup(ComponentIdLookup.VERSIONED_OR_GENERATE) .mapInstanceIdentifiers(false) .mapControllerServiceReferencesToVersionedId(true) @@ -4029,25 +4038,24 @@ public void restoreFlowPreservingIdentifiers(final VersionedExternalFlow propose .updateDescendantVersionedFlows(true) .updateGroupSettings(true) .updateRpgUrls(false) - .propertyDecryptor(encryptor::decrypt) + .propertyEncryptionProvider(propertyEncryptionProvider) .build(); - // Sensitive property values in the proposed snapshot were encrypted using the same PropertyEncryptor when the snapshot - // was persisted (for example, when a Connector-managed flow is persisted in Troubleshooting mode). The currently loaded - // flow therefore must also be mapped with an equivalent SensitiveValueEncryptor so the comparison between "current" and - // "proposed" sensitive values operates on matching ciphertext; otherwise every sensitive property appears to differ and - // the decrypted value written back to the live component is the encrypted payload rather than the plaintext (or parameter - // reference) that was originally captured. + // Sensitive property values in the proposed snapshot were encrypted when the snapshot was persisted (for example, when a + // Connector-managed flow is persisted in Troubleshooting mode). The currently loaded flow therefore must also be mapped + // with encryption enabled so the comparison between "current" and "proposed" sensitive values operates on values that are + // decrypted the same way on both sides; otherwise every sensitive property appears to differ and the value written back to + // the live component is the encrypted payload rather than the plaintext (or parameter reference) originally captured. final FlowMappingOptions flowMappingOptions = new FlowMappingOptions.Builder() .mapSensitiveConfiguration(true) .mapPropertyDescriptors(true) .stateLookup(stateLookup) - .sensitiveValueEncryptor(encryptor::encrypt) .componentIdLookup(ComponentIdLookup.VERSIONED_OR_GENERATE) .mapInstanceIdentifiers(true) .mapControllerServiceReferencesToVersionedId(true) .mapFlowRegistryClientId(false) .mapAssetReferences(false) + .propertyEncryptionProvider(propertyEncryptionProvider) .build(); synchronizeFlow(proposedSnapshot, synchronizationOptions, flowMappingOptions); @@ -4141,7 +4149,8 @@ private Set getModifications() { final ComparableDataFlow snapshotFlow = new StandardComparableDataFlow("Versioned Flow", vci.getFlowSnapshot()); final FlowComparator flowComparator = new StandardFlowComparator(snapshotFlow, currentFlow, - new EvolvingDifferenceDescriptor(), encryptor::decrypt, VersionedComponent::getIdentifier, FlowComparatorVersionedStrategy.SHALLOW); + new EvolvingDifferenceDescriptor(), new ProviderSensitiveValueDecryptor(propertyEncryptionProvider), + VersionedComponent::getIdentifier, FlowComparatorVersionedStrategy.SHALLOW); final FlowComparison comparison = flowComparator.compare(); final Collection comparisonDifferences = comparison.getDifferences(); final FlowDifferenceFilters.EnvironmentalChangeContext environmentalContext = diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/FilterSensitiveParameterValueMapper.java b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/FilterSensitiveParameterValueMapper.java index f7bd4b4f4546..bff422862db5 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/FilterSensitiveParameterValueMapper.java +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/FilterSensitiveParameterValueMapper.java @@ -25,12 +25,13 @@ public class FilterSensitiveParameterValueMapper implements ParameterValueMapper /** * Get mapped Parameter value based on properties * + * @param parameterContextName Name of the Parameter Context that contains the Parameter * @param parameter Parameter with descriptor of attributes for mapping * @param value Parameter value to be mapped * @return Mapped Parameter value */ @Override - public String getMapped(final Parameter parameter, final String value) { + public String getMapped(final String parameterContextName, final Parameter parameter, final String value) { Objects.requireNonNull(parameter, "Parameter required"); final ParameterDescriptor descriptor = parameter.getDescriptor(); diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/ParameterValueMapper.java b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/ParameterValueMapper.java index c44ca21b6e74..a42218ddae88 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/ParameterValueMapper.java +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/ParameterValueMapper.java @@ -23,9 +23,10 @@ public interface ParameterValueMapper { /** * Get mapped Parameter value based on properties * + * @param parameterContextName Name of the Parameter Context that contains the Parameter * @param parameter Parameter with descriptor of attributes for mapping * @param value Parameter value to be mapped * @return Mapped Parameter value */ - String getMapped(Parameter parameter, String value); + String getMapped(String parameterContextName, Parameter parameter, String value); } diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/StandardParameterValueMapper.java b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/StandardParameterValueMapper.java index 3b3fffdd88ab..c12d279a6546 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/StandardParameterValueMapper.java +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/StandardParameterValueMapper.java @@ -16,7 +16,11 @@ */ package org.apache.nifi.parameter; -import org.apache.nifi.registry.flow.mapping.SensitiveValueEncryptor; +import org.apache.nifi.security.encryption.PropertyEncryptionEncoder; +import org.apache.nifi.security.encryption.PropertyEncryptionProvider; +import org.apache.nifi.security.encryption.SensitivePropertyCodec; +import org.apache.nifi.security.encryption.SensitivePropertyContext; +import org.apache.nifi.security.encryption.SensitivePropertyContextFactory; import java.util.Objects; @@ -26,23 +30,27 @@ public class StandardParameterValueMapper implements ParameterValueMapper { static final String PROVIDED_MAPPING = "provided:parameter"; - private static final String ENCRYPTED_FORMAT = "enc{%s}"; + private final PropertyEncryptionProvider propertyEncryptionProvider; - private final SensitiveValueEncryptor sensitiveValueEncryptor; - - public StandardParameterValueMapper(final SensitiveValueEncryptor sensitiveValueEncryptor) { - this.sensitiveValueEncryptor = sensitiveValueEncryptor; + /** + * Standard Parameter Value Mapper with the Property Encryption Provider + * + * @param propertyEncryptionProvider Provider applied to sensitive Parameter values, which may be null + */ + public StandardParameterValueMapper(final PropertyEncryptionProvider propertyEncryptionProvider) { + this.propertyEncryptionProvider = propertyEncryptionProvider; } /** * Get mapped Parameter value based on properties * + * @param parameterContextName Name of the Parameter Context that contains the Parameter * @param parameter Parameter with descriptor of attributes for mapping * @param value Parameter value to be mapped * @return Mapped Parameter value */ @Override - public String getMapped(final Parameter parameter, final String value) { + public String getMapped(final String parameterContextName, final Parameter parameter, final String value) { Objects.requireNonNull(parameter, "Parameter required"); final ParameterDescriptor descriptor = parameter.getDescriptor(); @@ -53,16 +61,25 @@ public String getMapped(final Parameter parameter, final String value) { } else if (parameter.isProvided()) { mapped = PROVIDED_MAPPING; } else if (descriptor.isSensitive()) { - if (sensitiveValueEncryptor == null) { - mapped = value; - } else { - final String encrypted = sensitiveValueEncryptor.encrypt(value); - mapped = ENCRYPTED_FORMAT.formatted(encrypted); - } + mapped = getEncrypted(parameterContextName, descriptor.getName(), value); } else { mapped = value; } return mapped; } + + private String getEncrypted(final String parameterContextName, final String parameterName, final String value) { + final String processed; + + if (propertyEncryptionProvider == null) { + processed = value; + } else { + final SensitivePropertyContext context = SensitivePropertyContextFactory.forParameter(parameterContextName, parameterName); + final String encrypted = SensitivePropertyCodec.encrypt(propertyEncryptionProvider, value, context); + processed = PropertyEncryptionEncoder.getEncoded(encrypted); + } + + return processed; + } } diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/registry/flow/mapping/VersionedComponentFlowMapper.java b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/registry/flow/mapping/VersionedComponentFlowMapper.java index ab1875e292d6..69ac2ff0af06 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/registry/flow/mapping/VersionedComponentFlowMapper.java +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/registry/flow/mapping/VersionedComponentFlowMapper.java @@ -113,6 +113,11 @@ import org.apache.nifi.registry.flow.VersionControlInformation; import org.apache.nifi.remote.PublicPort; import org.apache.nifi.remote.RemoteGroupPort; +import org.apache.nifi.security.encryption.PropertyEncryptionEncoder; +import org.apache.nifi.security.encryption.PropertyEncryptionProvider; +import org.apache.nifi.security.encryption.SensitivePropertyCodec; +import org.apache.nifi.security.encryption.SensitivePropertyContext; +import org.apache.nifi.security.encryption.SensitivePropertyContextFactory; import java.io.IOException; import java.io.UncheckedIOException; @@ -133,9 +138,6 @@ import java.util.stream.Collectors; public class VersionedComponentFlowMapper { - private static final String ENCRYPTED_PREFIX = "enc{"; - private static final String ENCRYPTED_SUFFIX = "}"; - private final ExtensionManager extensionManager; private final FlowMappingOptions flowMappingOptions; private final ParameterValueMapper parameterValueMapper; @@ -155,7 +157,7 @@ public VersionedComponentFlowMapper(final ExtensionManager extensionManager, fin this.flowMappingOptions = flowMappingOptions; if (flowMappingOptions.isMapSensitiveConfiguration()) { - this.parameterValueMapper = new StandardParameterValueMapper(flowMappingOptions.getSensitiveValueEncryptor()); + this.parameterValueMapper = new StandardParameterValueMapper(flowMappingOptions.getPropertyEncryptionProvider()); } else { this.parameterValueMapper = new FilterSensitiveParameterValueMapper(); } @@ -604,7 +606,9 @@ private Map mapProperties(final ComponentNode component, final C } if (property.isSensitive()) { - value = encrypt(value); + final SensitivePropertyContext context = SensitivePropertyContextFactory.forComponent( + component.getIdentifier(), component.getCanonicalClassName(), property.getName()); + value = encrypt(value, context); } mapped.put(property.getName(), value); @@ -613,19 +617,18 @@ private Map mapProperties(final ComponentNode component, final C return mapped; } - protected String encrypt(final String value) { + protected String encrypt(final String value, final SensitivePropertyContext context) { if (value == null) { return null; } - final SensitiveValueEncryptor encryptor = flowMappingOptions.getSensitiveValueEncryptor(); - if (encryptor == null) { - // This will happen only if the given property is mappable, which means that it is a parameter reference. + final PropertyEncryptionProvider propertyEncryptionProvider = flowMappingOptions.getPropertyEncryptionProvider(); + if (propertyEncryptionProvider == null) { return value; + } else { + final String encrypted = SensitivePropertyCodec.encrypt(propertyEncryptionProvider, value, context); + return PropertyEncryptionEncoder.getEncoded(encrypted); } - - final String encrypted = encryptor.encrypt(value); - return ENCRYPTED_PREFIX + encrypted + ENCRYPTED_SUFFIX; } private boolean isMappable(final PropertyDescriptor propertyDescriptor, final PropertyConfiguration propertyConfiguration) { @@ -875,7 +878,8 @@ public VersionedRemoteProcessGroup mapRemoteProcessGroup(final RemoteProcessGrou rpg.setProxyPort(remoteGroup.getProxyPort()); rpg.setProxyUser(remoteGroup.getProxyUser()); if (flowMappingOptions.isMapSensitiveConfiguration()) { - rpg.setProxyPassword(encrypt(remoteGroup.getProxyPassword())); + final SensitivePropertyContext context = SensitivePropertyContextFactory.forRemoteProcessGroupProxyPassword(remoteGroup.getIdentifier()); + rpg.setProxyPassword(encrypt(remoteGroup.getProxyPassword(), context)); } rpg.setTargetUris(remoteGroup.getTargetUris()); rpg.setTransportProtocol(remoteGroup.getTransportProtocol().name()); @@ -1011,7 +1015,7 @@ private VersionedParameter mapParameter(ParameterContext parameterContext, Param .getReferencedControllerServiceData(parameterContext, parameterDescriptor.getName()); if (referencedControllerServiceData.isEmpty()) { - versionedParameter = mapParameter(parameter); + versionedParameter = mapParameter(parameterContext.getName(), parameter); } else { final String referencedVersionServiceId = referencedControllerServiceData.getFirst().getVersionedServiceId(); final String parameterValue = parameter.getValue(); @@ -1019,19 +1023,19 @@ private VersionedParameter mapParameter(ParameterContext parameterContext, Param // If a referenced Versioned Service ID is available, use it directly. Do not attempt to // generate or cache a mapping using a null component identifier. if (referencedVersionServiceId != null) { - versionedParameter = mapParameter(parameter, referencedVersionServiceId); + versionedParameter = mapParameter(parameterContext.getName(), parameter, referencedVersionServiceId); } else if (parameterValue != null && !parameterValue.isBlank()) { // If the parameter has a concrete (non-empty) value referencing a service instance id, // generate a stable Versioned ID for it. final String serviceId = getId(Optional.empty(), parameterValue); - versionedParameter = mapParameter(parameter, serviceId); + versionedParameter = mapParameter(parameterContext.getName(), parameter, serviceId); } else { // No referenced service and no parameter value specified; map as null to avoid NPE - versionedParameter = mapParameter(parameter, null); + versionedParameter = mapParameter(parameterContext.getName(), parameter, null); } } } else { - versionedParameter = mapParameter(parameter); + versionedParameter = mapParameter(parameterContext.getName(), parameter); } return versionedParameter; @@ -1055,11 +1059,11 @@ private ParameterProviderReference createParameterProviderReference(final Parame return reference; } - private VersionedParameter mapParameter(final Parameter parameter) { - return mapParameter(parameter, parameter.getValue()); + private VersionedParameter mapParameter(final String parameterContextName, final Parameter parameter) { + return mapParameter(parameterContextName, parameter, parameter.getValue()); } - private VersionedParameter mapParameter(final Parameter parameter, final String value) { + private VersionedParameter mapParameter(final String parameterContextName, final Parameter parameter, final String value) { final ParameterDescriptor descriptor = parameter.getDescriptor(); final VersionedParameter versionedParameter = new VersionedParameter(); @@ -1070,7 +1074,7 @@ private VersionedParameter mapParameter(final Parameter parameter, final String final List referencedAssets = parameter.getReferencedAssets(); if (referencedAssets == null || referencedAssets.isEmpty()) { - final String mapped = parameterValueMapper.getMapped(parameter, value); + final String mapped = parameterValueMapper.getMapped(parameterContextName, parameter, value); versionedParameter.setValue(mapped); } else if (flowMappingOptions.isMapAssetReferences()) { final List assetIds = referencedAssets.stream() diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/security/encryption/PropertyEncryptionProviderFactory.java b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/security/encryption/PropertyEncryptionProviderFactory.java new file mode 100644 index 000000000000..4881bd856f37 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/security/encryption/PropertyEncryptionProviderFactory.java @@ -0,0 +1,135 @@ +/* + * 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.security.encryption; + +import org.apache.nifi.nar.ExtensionManager; +import org.apache.nifi.nar.NarCloseable; +import org.apache.nifi.nar.NarThreadContextClassLoader; +import org.apache.nifi.util.NiFiProperties; + +import java.io.IOException; +import java.util.Map; +import java.util.stream.Collectors; +import javax.net.ssl.SSLContext; +import javax.net.ssl.X509TrustManager; + +/** + * Factory for creating and initializing the Property Encryption Provider configured in application properties. + * Installations that do not configure an implementation class use the password-based provider, which derives a secret + * key from the sensitive properties key. + */ +public final class PropertyEncryptionProviderFactory { + private static final String DEFAULT_IMPLEMENTATION = "org.apache.nifi.security.encryption.password.PasswordBasedPropertyEncryptionProvider"; + + private PropertyEncryptionProviderFactory() { + } + + /** + * Create and initialize the configured Property Encryption Provider + * + * @param extensionManager Extension Manager for locating the provider in loaded NARs + * @param properties Application properties containing the implementation class and provider properties + * @param sslContext SSL Context supplied to the provider, which may be null + * @param trustManager Trust Manager supplied to the provider, which may be null + * @return Initialized Property Encryption Provider + */ + public static PropertyEncryptionProvider getPropertyEncryptionProvider( + final ExtensionManager extensionManager, + final NiFiProperties properties, + final SSLContext sslContext, + final X509TrustManager trustManager + ) { + final String configuredClassName = properties.getProperty(NiFiProperties.PROPERTY_ENCRYPTION_PROVIDER_IMPLEMENTATION); + final String className = configuredClassName == null || configuredClassName.isBlank() ? DEFAULT_IMPLEMENTATION : configuredClassName; + + final PropertyEncryptionProvider provider; + try { + provider = NarThreadContextClassLoader.createInstance(extensionManager, className, PropertyEncryptionProvider.class, properties); + } catch (final Exception e) { + throw new IllegalStateException("Failed to create PropertyEncryptionProvider with class [%s]".formatted(className), e); + } + + return initialize(provider, getProviderProperties(properties), sslContext, trustManager); + } + + private static PropertyEncryptionProvider initialize( + final PropertyEncryptionProvider provider, + final Map providerProperties, + final SSLContext sslContext, + final X509TrustManager trustManager + ) { + final PropertyEncryptionProvider wrapped = wrapWithComponentNarLoader(provider); + try { + final PropertyEncryptionProviderInitializationContext initializationContext = + new StandardPropertyEncryptionProviderInitializationContext(providerProperties, sslContext, trustManager); + wrapped.initialize(initializationContext); + return wrapped; + } catch (final RuntimeException e) { + try { + wrapped.close(); + } catch (final Exception closeException) { + e.addSuppressed(closeException); + } + throw e; + } + } + + /** + * Wrap the provider so that every callback runs with the Thread Context ClassLoader of the NAR that supplied the + * implementation, because NarThreadContextClassLoader sets the ClassLoader only while constructing the instance. + */ + private static PropertyEncryptionProvider wrapWithComponentNarLoader(final PropertyEncryptionProvider provider) { + final ClassLoader componentClassLoader = provider.getClass().getClassLoader(); + return new PropertyEncryptionProvider() { + @Override + public void initialize(final PropertyEncryptionProviderInitializationContext context) { + try (NarCloseable ignored = NarCloseable.withComponentNarLoader(componentClassLoader)) { + provider.initialize(context); + } + } + + @Override + public byte[] encrypt(final byte[] property, final SensitivePropertyContext context) { + try (NarCloseable ignored = NarCloseable.withComponentNarLoader(componentClassLoader)) { + return provider.encrypt(property, context); + } + } + + @Override + public byte[] decrypt(final byte[] encryptedProperty, final SensitivePropertyContext context) { + try (NarCloseable ignored = NarCloseable.withComponentNarLoader(componentClassLoader)) { + return provider.decrypt(encryptedProperty, context); + } + } + + @Override + public void close() throws IOException { + try (NarCloseable ignored = NarCloseable.withComponentNarLoader(componentClassLoader)) { + provider.close(); + } + } + }; + } + + private static Map getProviderProperties(final NiFiProperties properties) { + final String prefix = NiFiProperties.PROPERTY_ENCRYPTION_PROVIDER_PREFIX; + return properties.getPropertiesWithPrefix(prefix) + .entrySet() + .stream() + .collect(Collectors.toMap(entry -> entry.getKey().substring(prefix.length()), Map.Entry::getValue)); + } +} diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/security/encryption/ProviderSensitiveValueDecryptor.java b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/security/encryption/ProviderSensitiveValueDecryptor.java new file mode 100644 index 000000000000..2158eacaf032 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/security/encryption/ProviderSensitiveValueDecryptor.java @@ -0,0 +1,50 @@ +/* + * 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.security.encryption; + +import org.apache.nifi.flow.VersionedComponent; +import org.apache.nifi.flow.VersionedExtensionComponent; +import org.apache.nifi.flow.VersionedParameterContext; +import org.apache.nifi.registry.flow.diff.SensitiveValueDecryptor; + +import java.util.Objects; + +/** + * Decrypts sensitive values for flow comparison using a Property Encryption Provider. The context supplied for each + * value is rebuilt from the owning component so that it matches the context supplied when the flow was serialized. + */ +public class ProviderSensitiveValueDecryptor implements SensitiveValueDecryptor { + private final PropertyEncryptionProvider propertyEncryptionProvider; + + public ProviderSensitiveValueDecryptor(final PropertyEncryptionProvider propertyEncryptionProvider) { + this.propertyEncryptionProvider = Objects.requireNonNull(propertyEncryptionProvider, "Property Encryption Provider required"); + } + + @Override + public String decrypt(final VersionedComponent owner, final String valueName, final String encryptedValue) { + return SensitivePropertyCodec.decrypt(propertyEncryptionProvider, encryptedValue, getContext(owner, valueName)); + } + + private SensitivePropertyContext getContext(final VersionedComponent owner, final String valueName) { + if (owner instanceof final VersionedParameterContext parameterContext) { + return SensitivePropertyContextFactory.forParameter(parameterContext.getName(), valueName); + } + + final String componentType = owner instanceof final VersionedExtensionComponent extension ? extension.getType() : null; + return SensitivePropertyContextFactory.forComponent(owner.getInstanceIdentifier(), componentType, valueName); + } +} diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizerTest.java b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizerTest.java index 9c0953c1f455..86cdbf215bd1 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizerTest.java +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/flow/synchronization/StandardVersionedComponentSynchronizerTest.java @@ -89,11 +89,14 @@ import org.apache.nifi.reporting.InitializationException; import org.apache.nifi.scheduling.ExecutionNode; import org.apache.nifi.scheduling.SchedulingStrategy; +import org.apache.nifi.security.encryption.InternalPassThroughPropertyEncryptionProvider; +import org.apache.nifi.security.encryption.PropertyEncryptionEncoder; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.stubbing.Answer; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; @@ -101,6 +104,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.HexFormat; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -117,8 +121,6 @@ import java.util.function.Function; import java.util.stream.Collectors; -import static org.apache.nifi.flow.synchronization.StandardVersionedComponentSynchronizer.ENC_PREFIX; -import static org.apache.nifi.flow.synchronization.StandardVersionedComponentSynchronizer.ENC_SUFFIX; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -147,12 +149,15 @@ public class StandardVersionedComponentSynchronizerTest { - private static final String ENCODED_TEXT = "ENCODED"; + private static final String DECRYPTED_TEXT = "Decrypted Value"; - private static final String ENCRYPTED_PROPERTY_VALUE = "%s%s%s".formatted(ENC_PREFIX, ENCODED_TEXT, ENC_SUFFIX); + private static final String ENCRYPTED_PROPERTY_VALUE = PropertyEncryptionEncoder.getEncoded( + HexFormat.of().formatHex(DECRYPTED_TEXT.getBytes(StandardCharsets.UTF_8))); private static final String SENSITIVE_PROPERTY_NAME = "Access Token"; + private static final String SENSITIVE_PARAMETER_REFERENCE = "#{Access Token Parameter}"; + private static final String PARAM_ABC = "abc"; private static final String PARAM_SECRET = "secret"; @@ -293,6 +298,7 @@ public void setup() { .componentComparisonIdLookup(VersionedComponent::getIdentifier) .componentScheduler(componentScheduler) .scheduledStateChangeListener(scheduledStateChangeListener) + .propertyEncryptionProvider(new InternalPassThroughPropertyEncryptionProvider()) .build(); synchronizer = new StandardVersionedComponentSynchronizer(context); @@ -306,6 +312,7 @@ private FlowSynchronizationOptions createQuickFailSynchronizationOptions(final F .componentComparisonIdLookup(VersionedComponent::getIdentifier) .componentScheduler(componentScheduler) .scheduledStateChangeListener(scheduledStateChangeListener) + .propertyEncryptionProvider(new InternalPassThroughPropertyEncryptionProvider()) .componentStopTimeout(Duration.ofMillis(10)) .componentStopTimeoutAction(timeoutAction) .build(); @@ -435,7 +442,37 @@ public void testSynchronizeProcessorAddedMigrated() { final Map migratedProperties = propertiesCaptor.getValue(); final String propertyValue = migratedProperties.get(SENSITIVE_PROPERTY_NAME); - assertEquals(ENCODED_TEXT, propertyValue); + assertEquals(DECRYPTED_TEXT, propertyValue); + } + + /** + * A versioned flow stores a sensitive property as plaintext when the value is a Parameter reference, so the + * reference must be applied to the component without being processed as an encrypted value. + */ + @Test + public void testSynchronizeProcessorSensitiveParameterReferenceRetained() { + final ProcessGroup processGroup = createMockProcessGroup(); + + final VersionedProcessGroup rootGroup = new VersionedProcessGroup(); + rootGroup.setIdentifier("rootGroup"); + + final VersionedProcessor versionedProcessor = createMinimalVersionedProcessor(); + versionedProcessor.setProperties(Collections.singletonMap(SENSITIVE_PROPERTY_NAME, SENSITIVE_PARAMETER_REFERENCE)); + + final ProcessorNode processorNode = createMockProcessor(); + rootGroup.setProcessors(Set.of(versionedProcessor)); + + final VersionedExternalFlow externalFlow = new VersionedExternalFlow(); + externalFlow.setFlowContents(rootGroup); + + when(flowManager.createProcessor(any(), any(), any(), eq(true))).thenReturn(processorNode); + + synchronizer.synchronize(processGroup, externalFlow, synchronizationOptions); + + verify(processorNode).setProperties(propertiesCaptor.capture(), eq(true), eq(Collections.emptySet())); + + final Map appliedProperties = propertiesCaptor.getValue(); + assertEquals(SENSITIVE_PARAMETER_REFERENCE, appliedProperties.get(SENSITIVE_PROPERTY_NAME)); } @Test @@ -460,7 +497,7 @@ public void testAddVersionedComponents() { verify(processorNode).migrateConfiguration(propertiesCaptor.capture(), any()); Map migratedProperties = propertiesCaptor.getValue(); String propertyValue = migratedProperties.get(SENSITIVE_PROPERTY_NAME); - assertEquals(ENCODED_TEXT, propertyValue); + assertEquals(DECRYPTED_TEXT, propertyValue); verify(group).addControllerService(any(ControllerServiceNode.class)); verify(controllerServiceNode, atLeastOnce()).setName(eq(versionedService.getName())); @@ -2097,6 +2134,7 @@ private FlowSynchronizationOptions preservePublicPortNamesOptions(final boolean .componentComparisonIdLookup(VersionedComponent::getIdentifier) .componentScheduler(componentScheduler) .scheduledStateChangeListener(scheduledStateChangeListener) + .propertyEncryptionProvider(new InternalPassThroughPropertyEncryptionProvider()) .preservePublicPortNames(preserve) .build(); } @@ -2147,7 +2185,7 @@ private void assertSensitivePropertyDecrypted(final ComponentNode componentNode) final Map appliedProperties = propertiesCaptor.getValue(); final String appliedSensitivePropertyValue = appliedProperties.get(SENSITIVE_PROPERTY_NAME); - assertEquals(ENCODED_TEXT, appliedSensitivePropertyValue); + assertEquals(DECRYPTED_TEXT, appliedSensitivePropertyValue); } private record ScheduledStateUpdate(T component, org.apache.nifi.controller.ScheduledState state) { diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/groups/StandardProcessGroupTest.java b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/groups/StandardProcessGroupTest.java index 958ce530bef8..2e2659009673 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/groups/StandardProcessGroupTest.java +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/groups/StandardProcessGroupTest.java @@ -35,11 +35,11 @@ import org.apache.nifi.controller.queue.FlowFileQueue; import org.apache.nifi.controller.queue.QueueSize; import org.apache.nifi.controller.service.ControllerServiceProvider; -import org.apache.nifi.encrypt.PropertyEncryptor; import org.apache.nifi.flow.ExecutionEngine; import org.apache.nifi.nar.ExtensionManager; import org.apache.nifi.registry.flow.VersionControlInformation; import org.apache.nifi.registry.flow.VersionedFlowStatus; +import org.apache.nifi.security.encryption.PropertyEncryptionProvider; import org.apache.nifi.util.NiFiProperties; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -91,7 +91,7 @@ class StandardProcessGroupTest { private ProcessScheduler processScheduler; @Mock - private PropertyEncryptor propertyEncryptor; + private PropertyEncryptionProvider propertyEncryptionProvider; @Mock private ExtensionManager extensionManager; @@ -146,7 +146,7 @@ void setProcessGroup() throws IOException { ID, controllerServiceProvider, processScheduler, - propertyEncryptor, + propertyEncryptionProvider, extensionManager, stateManagerProvider, flowManager, @@ -485,7 +485,7 @@ private StandardProcessGroup createStandardProcessGroup(final String id, final S id, controllerServiceProvider, processScheduler, - propertyEncryptor, + propertyEncryptionProvider, extensionManager, stateManagerProvider, flowManager, diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/parameter/TestStandardParameterValueMapper.java b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/parameter/TestStandardParameterValueMapper.java index eb4669313576..18d969fd8595 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/parameter/TestStandardParameterValueMapper.java +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/parameter/TestStandardParameterValueMapper.java @@ -16,16 +16,28 @@ */ package org.apache.nifi.parameter; -import org.apache.nifi.registry.flow.mapping.SensitiveValueEncryptor; +import org.apache.nifi.security.encryption.PropertyEncryptionEncoder; +import org.apache.nifi.security.encryption.PropertyEncryptionProvider; +import org.apache.nifi.security.encryption.SensitivePropertyAttribute; +import org.apache.nifi.security.encryption.SensitivePropertyCategory; +import org.apache.nifi.security.encryption.SensitivePropertyContext; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class TestStandardParameterValueMapper { @@ -33,21 +45,33 @@ class TestStandardParameterValueMapper { private static final String VALUE = "ParameterValue"; + private static final String CONTEXT_NAME = "NamedParameterContext"; + + private static final byte[] PROVIDER_VALUE = {1, 2, 3, 4}; + + private static final String PROVIDER_MAPPED_VALUE = PropertyEncryptionEncoder.getEncoded("01020304"); + @Mock - private SensitiveValueEncryptor sensitiveValueEncryptor; + private PropertyEncryptionProvider propertyEncryptionProvider; + + @Captor + private ArgumentCaptor contextCaptor; + + @Captor + private ArgumentCaptor propertyCaptor; private StandardParameterValueMapper mapper; @BeforeEach void setMapper() { - mapper = new StandardParameterValueMapper(sensitiveValueEncryptor); + mapper = new StandardParameterValueMapper(propertyEncryptionProvider); } @Test void testGetMappedNotSensitiveNotProvided() { final Parameter parameter = getParameter(false, false); - final String mapped = mapper.getMapped(parameter, VALUE); + final String mapped = mapper.getMapped(CONTEXT_NAME, parameter, VALUE); assertEquals(VALUE, mapped); } @@ -56,7 +80,7 @@ void testGetMappedNotSensitiveNotProvided() { void testGetMappedNotSensitiveProvided() { final Parameter parameter = getParameter(false, true); - final String mapped = mapper.getMapped(parameter, VALUE); + final String mapped = mapper.getMapped(CONTEXT_NAME, parameter, VALUE); assertEquals(StandardParameterValueMapper.PROVIDED_MAPPING, mapped); } @@ -65,28 +89,38 @@ void testGetMappedNotSensitiveProvided() { void testGetMappedSensitiveProvided() { final Parameter parameter = getParameter(true, true); - final String mapped = mapper.getMapped(parameter, VALUE); + final String mapped = mapper.getMapped(CONTEXT_NAME, parameter, VALUE); assertEquals(StandardParameterValueMapper.PROVIDED_MAPPING, mapped); } @Test - void testGetMappedSensitiveNotProvided() { + void testGetMappedSensitiveNotProvidedNullValue() { final Parameter parameter = getParameter(true, false); - final String mapped = mapper.getMapped(parameter, VALUE); + final String mapped = mapper.getMapped(CONTEXT_NAME, parameter, null); - assertNotEquals(VALUE, mapped); - assertNotEquals(StandardParameterValueMapper.PROVIDED_MAPPING, mapped); + assertNull(mapped); } @Test - void testGetMappedSensitiveNotProvidedNullValue() { + void testGetMappedPropertyEncryptionProvider() { final Parameter parameter = getParameter(true, false); + when(propertyEncryptionProvider.encrypt(any(), any())).thenReturn(PROVIDER_VALUE); - final String mapped = mapper.getMapped(parameter, null); + final String mapped = mapper.getMapped(CONTEXT_NAME, parameter, VALUE); - assertNull(mapped); + assertEquals(PROVIDER_MAPPED_VALUE, mapped); + + verify(propertyEncryptionProvider).encrypt(propertyCaptor.capture(), contextCaptor.capture()); + assertArrayEquals(VALUE.getBytes(StandardCharsets.UTF_8), propertyCaptor.getValue()); + + final SensitivePropertyContext context = contextCaptor.getValue(); + assertEquals(SensitivePropertyCategory.PARAMETER, context.category()); + + final Map attributes = context.attributes(); + assertEquals(CONTEXT_NAME, attributes.get(SensitivePropertyAttribute.PARAMETER_CONTEXT_NAME.getKey())); + assertEquals(NAME, attributes.get(SensitivePropertyAttribute.PARAMETER_NAME.getKey())); } private Parameter getParameter(final boolean sensitive, final boolean provided) { diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/registry/flow/mapping/TestVersionedComponentFlowMapper.java b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/registry/flow/mapping/TestVersionedComponentFlowMapper.java index 9b0677ea35a2..5916ef43cd33 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/registry/flow/mapping/TestVersionedComponentFlowMapper.java +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/registry/flow/mapping/TestVersionedComponentFlowMapper.java @@ -46,8 +46,15 @@ import org.apache.nifi.processor.ProcessContext; import org.apache.nifi.processor.ProcessSession; import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.registry.flow.diff.SensitiveValueDecryptor; import org.apache.nifi.scheduling.ExecutionNode; import org.apache.nifi.scheduling.SchedulingStrategy; +import org.apache.nifi.security.encryption.PropertyEncryptionEncoder; +import org.apache.nifi.security.encryption.PropertyEncryptionProvider; +import org.apache.nifi.security.encryption.PropertyEncryptionProviderInitializationContext; +import org.apache.nifi.security.encryption.ProviderSensitiveValueDecryptor; +import org.apache.nifi.security.encryption.SensitivePropertyContext; +import org.apache.nifi.security.encryption.SensitivePropertyContextFactory; import org.junit.jupiter.api.Test; import org.mockito.stubbing.Answer; @@ -68,7 +75,13 @@ import static org.mockito.Mockito.when; public class TestVersionedComponentFlowMapper { - private static final SensitiveValueEncryptor ENCRYPTOR = value -> new StringBuilder(value).reverse().toString(); + private static final String PROCESSOR_TYPE = "org.apache.nifi.processors.standard.InvokeHTTP"; + + private static final String SENSITIVE_PROPERTY_NAME = "Sensitive Property B"; + + private static final String SENSITIVE_PROPERTY_VALUE = "A B C"; + + private static final String NON_SENSITIVE_PROPERTY_NAME = "Property A"; @Test public void testMappingProcessorWithSensitiveValuesGivesNullValue() { @@ -116,8 +129,8 @@ public void testMappingProcessorWithSensitiveValuesProvidesEncryptedValue() { final FlowMappingOptions mappingOptions = new FlowMappingOptions.Builder() .stateLookup(VersionedComponentStateLookup.IDENTITY_LOOKUP) .componentIdLookup(ComponentIdLookup.USE_COMPONENT_ID) + .propertyEncryptionProvider(new RecordingPropertyEncryptionProvider()) .mapSensitiveConfiguration(true) - .sensitiveValueEncryptor(ENCRYPTOR) .build(); final VersionedComponentFlowMapper mapper = new VersionedComponentFlowMapper(extensionManager, mappingOptions); @@ -133,7 +146,62 @@ public void testMappingProcessorWithSensitiveValuesProvidesEncryptedValue() { final Map versionedProperties = versionedProcessor.getProperties(); assertEquals("A", versionedProperties.get("Property A")); - assertEquals("enc{C B A}", versionedProperties.get("Sensitive Property B")); + assertEquals(PropertyEncryptionEncoder.getEncoded("4320422041"), versionedProperties.get("Sensitive Property B")); + } + + /** + * The context supplied when a sensitive property is mapped must describe the component instance and the property, + * because a Property Encryption Provider may bind the context to the encrypted value + */ + @Test + public void testMappingProcessorWithProviderSuppliesComponentContext() { + final RecordingPropertyEncryptionProvider provider = new RecordingPropertyEncryptionProvider(); + final VersionedComponentFlowMapper mapper = new VersionedComponentFlowMapper(mock(ExtensionManager.class), createProviderMappingOptions(provider)); + + final Map properties = new HashMap<>(); + properties.put(NON_SENSITIVE_PROPERTY_NAME, "A"); + properties.put(SENSITIVE_PROPERTY_NAME, SENSITIVE_PROPERTY_VALUE); + + final ProcessorNode procNode = createProcessorNode(properties); + mapper.mapProcessor(procNode, mock(ControllerServiceProvider.class), Collections.emptySet(), Collections.emptyMap()); + + assertEquals(1, provider.encryptionContexts.size()); + assertEquals( + SensitivePropertyContextFactory.forComponent(procNode.getIdentifier(), PROCESSOR_TYPE, SENSITIVE_PROPERTY_NAME), + provider.encryptionContexts.getFirst() + ); + } + + /** + * Synchronization rebuilds the context from the mapped component rather than from the component node, so the mapped + * component must carry enough information to reproduce the context supplied during mapping + */ + @Test + public void testSensitivePropertyContextEqualForMappingAndSynchronization() { + final RecordingPropertyEncryptionProvider provider = new RecordingPropertyEncryptionProvider(); + final VersionedComponentFlowMapper mapper = new VersionedComponentFlowMapper(mock(ExtensionManager.class), createProviderMappingOptions(provider)); + + final ProcessorNode procNode = createProcessorNode(Collections.singletonMap(SENSITIVE_PROPERTY_NAME, SENSITIVE_PROPERTY_VALUE)); + final VersionedProcessor versionedProcessor = mapper.mapProcessor(procNode, mock(ControllerServiceProvider.class), Collections.emptySet(), Collections.emptyMap()); + + final String mappedValue = versionedProcessor.getProperties().get(SENSITIVE_PROPERTY_NAME); + final String encryptedValue = PropertyEncryptionEncoder.getDecoded(mappedValue); + + final SensitiveValueDecryptor decryptor = new ProviderSensitiveValueDecryptor(provider); + final String decrypted = decryptor.decrypt(versionedProcessor, SENSITIVE_PROPERTY_NAME, encryptedValue); + + assertEquals(SENSITIVE_PROPERTY_VALUE, decrypted); + assertEquals(provider.encryptionContexts, provider.decryptionContexts); + } + + private FlowMappingOptions createProviderMappingOptions(final PropertyEncryptionProvider provider) { + return new FlowMappingOptions.Builder() + .stateLookup(VersionedComponentStateLookup.IDENTITY_LOOKUP) + .componentIdLookup(ComponentIdLookup.VERSIONED_OR_GENERATE) + .mapInstanceIdentifiers(true) + .mapSensitiveConfiguration(true) + .propertyEncryptionProvider(provider) + .build(); } @Test @@ -212,7 +280,7 @@ public void testStandaloneExportSkipsClusterStateForProcessorWithBothScopes() th final List localNodeStates = mappedState.getLocalNodeStates(); assertNotNull(localNodeStates); assertEquals(1, localNodeStates.size()); - assertEquals(localState, localNodeStates.get(0).getState()); + assertEquals(localState, localNodeStates.getFirst().getState()); } @Test @@ -272,6 +340,40 @@ public void onTrigger(final ProcessContext context, final ProcessSession session } } + /** + * Provider that records the contexts supplied to each operation so that encryption and decryption contexts can be + * compared, and reverses the supplied bytes so that the encoded value differs from the plain value + */ + private static class RecordingPropertyEncryptionProvider implements PropertyEncryptionProvider { + private final List encryptionContexts = new ArrayList<>(); + + private final List decryptionContexts = new ArrayList<>(); + + @Override + public void initialize(final PropertyEncryptionProviderInitializationContext context) { + } + + @Override + public byte[] encrypt(final byte[] property, final SensitivePropertyContext context) { + encryptionContexts.add(context); + return getReversed(property); + } + + @Override + public byte[] decrypt(final byte[] encryptedProperty, final SensitivePropertyContext context) { + decryptionContexts.add(context); + return getReversed(encryptedProperty); + } + + private byte[] getReversed(final byte[] bytes) { + final byte[] reversed = new byte[bytes.length]; + for (int i = 0; i < bytes.length; i++) { + reversed[i] = bytes[bytes.length - 1 - i]; + } + return reversed; + } + } + private ProcessorNode createProcessorNode(final Map properties) { final ExpressionLanguageAgnosticParameterParser parameterParser = new ExpressionLanguageAgnosticParameterParser(); @@ -310,6 +412,7 @@ private ProcessorNode createProcessorNode(final Map properties) final String id = UUID.randomUUID().toString(); when(procNode.getIdentifier()).thenReturn(id); + when(procNode.getCanonicalClassName()).thenReturn(PROCESSOR_TYPE); when(procNode.getBulletinLevel()).thenReturn(LogLevel.WARN); when(procNode.getExecutionNode()).thenReturn(ExecutionNode.ALL); diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/FlowSynchronizationOptions.java b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/FlowSynchronizationOptions.java index a9e1014d45fe..fe1d36ff9d54 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/FlowSynchronizationOptions.java +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/FlowSynchronizationOptions.java @@ -18,6 +18,7 @@ package org.apache.nifi.groups; import org.apache.nifi.flow.VersionedComponent; +import org.apache.nifi.security.encryption.PropertyEncryptionProvider; import java.time.Duration; import java.util.function.Function; @@ -26,7 +27,8 @@ public class FlowSynchronizationOptions { private final ComponentIdGenerator componentIdGenerator; private final Function componentComparisonIdLookup; private final ComponentScheduler componentScheduler; - private final PropertyDecryptor propertyDecryptor; + private final boolean dropEncryptedValues; + private final PropertyEncryptionProvider propertyEncryptionProvider; private final boolean ignoreLocalModifications; private final boolean updateSettings; private final boolean updateDescendantVersionedFlows; @@ -41,7 +43,8 @@ private FlowSynchronizationOptions(final Builder builder) { this.componentIdGenerator = builder.componentIdGenerator; this.componentComparisonIdLookup = builder.componentComparisonIdLookup; this.componentScheduler = builder.componentScheduler; - this.propertyDecryptor = builder.propertyDecryptor; + this.dropEncryptedValues = builder.dropEncryptedValues; + this.propertyEncryptionProvider = builder.propertyEncryptionProvider; this.ignoreLocalModifications = builder.ignoreLocalModifications; this.updateSettings = builder.updateSettings; this.updateDescendantVersionedFlows = builder.updateDescendantVersionedFlows; @@ -85,8 +88,23 @@ public boolean isPreservePublicPortNames() { return preservePublicPortNames; } - public PropertyDecryptor getPropertyDecryptor() { - return propertyDecryptor; + /** + * Indicates whether encrypted values in the proposed flow are dropped rather than decrypted. Dropping resolves an + * encrypted value to null, which leaves the corresponding sensitive property unset. + * + * @return true when encrypted values are dropped + */ + public boolean isDropEncryptedValues() { + return dropEncryptedValues; + } + + /** + * Get the Property Encryption Provider used to decrypt sensitive values in the proposed flow + * + * @return Property Encryption Provider, or null when encrypted values are dropped + */ + public PropertyEncryptionProvider getPropertyEncryptionProvider() { + return propertyEncryptionProvider; } public Duration getComponentStopTimeout() { @@ -115,7 +133,8 @@ public static class Builder { private boolean updateRpgUrls = false; private boolean preservePublicPortNames = false; private ScheduledStateChangeListener scheduledStateChangeListener; - private PropertyDecryptor propertyDecryptor = value -> value; + private boolean dropEncryptedValues = false; + private PropertyEncryptionProvider propertyEncryptionProvider; private Duration componentStopTimeout = Duration.ofSeconds(30); private ComponentStopTimeoutAction timeoutAction = ComponentStopTimeoutAction.THROW_TIMEOUT_EXCEPTION; private String topLevelGroupId; @@ -210,13 +229,27 @@ public Builder preservePublicPortNames(final boolean preservePublicPortNames) { } /** - * Specifies the decryptor to use for sensitive properties + * Specifies that encrypted values in the proposed flow are dropped rather than decrypted, which leaves the + * corresponding sensitive properties unset. This is used when the proposed flow carries sensitive values that + * must not be copied to the components being synchronized, such as a flow retrieved from a Flow Registry. * - * @param decryptor the decryptor to use + * @param dropEncryptedValues whether to drop encrypted values * @return the builder */ - public Builder propertyDecryptor(final PropertyDecryptor decryptor) { - this.propertyDecryptor = decryptor; + public Builder dropEncryptedValues(final boolean dropEncryptedValues) { + this.dropEncryptedValues = dropEncryptedValues; + return this; + } + + /** + * Specifies the Property Encryption Provider to use for decrypting sensitive properties. The Provider must be + * set unless {@link #dropEncryptedValues(boolean) dropEncryptedValues} is set. + * + * @param propertyEncryptionProvider the Property Encryption Provider to use + * @return the builder + */ + public Builder propertyEncryptionProvider(final PropertyEncryptionProvider propertyEncryptionProvider) { + this.propertyEncryptionProvider = propertyEncryptionProvider; return this; } @@ -260,6 +293,12 @@ public FlowSynchronizationOptions build() { if (componentScheduler == null) { throw new IllegalStateException("Must set Component Scheduler"); } + if (dropEncryptedValues && propertyEncryptionProvider != null) { + throw new IllegalStateException("Must not set Property Encryption Provider when dropping encrypted values"); + } + if (!dropEncryptedValues && propertyEncryptionProvider == null) { + throw new IllegalStateException("Must set Property Encryption Provider or drop encrypted values"); + } if (scheduledStateChangeListener == null) { scheduledStateChangeListener = ScheduledStateChangeListener.EMPTY; } @@ -287,7 +326,8 @@ public static Builder from(final FlowSynchronizationOptions options) { builder.updateDescendantVersionedFlows = options.isUpdateDescendantVersionedFlows(); builder.updateRpgUrls = options.isUpdateRpgUrls(); builder.preservePublicPortNames = options.isPreservePublicPortNames(); - builder.propertyDecryptor = options.getPropertyDecryptor(); + builder.dropEncryptedValues = options.isDropEncryptedValues(); + builder.propertyEncryptionProvider = options.getPropertyEncryptionProvider(); builder.componentStopTimeout = options.getComponentStopTimeout(); builder.timeoutAction = options.getComponentStopTimeoutAction(); builder.scheduledStateChangeListener = options.getScheduledStateChangeListener(); diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/PropertyDecryptor.java b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/PropertyDecryptor.java deleted file mode 100644 index b88bd68687a0..000000000000 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/groups/PropertyDecryptor.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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.groups; - -public interface PropertyDecryptor { - String decrypt(String value); - - PropertyDecryptor NO_OP_DECRYPTOR = value -> value; -} diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/mapping/FlowMappingOptions.java b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/mapping/FlowMappingOptions.java index eccf808188d2..020bb884bc92 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/mapping/FlowMappingOptions.java +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/mapping/FlowMappingOptions.java @@ -18,11 +18,12 @@ package org.apache.nifi.registry.flow.mapping; import org.apache.nifi.components.state.StateManagerProvider; +import org.apache.nifi.security.encryption.PropertyEncryptionProvider; import static java.util.Objects.requireNonNull; public class FlowMappingOptions { - private final SensitiveValueEncryptor encryptor; + private final PropertyEncryptionProvider propertyEncryptionProvider; private final VersionedComponentStateLookup stateLookup; private final ComponentIdLookup componentIdLookup; private final boolean mapPropertyDescriptors; @@ -36,7 +37,7 @@ public class FlowMappingOptions { private final int localNodeOrdinal; private FlowMappingOptions(final Builder builder) { - encryptor = builder.encryptor; + propertyEncryptionProvider = builder.propertyEncryptionProvider; stateLookup = builder.stateLookup; componentIdLookup = builder.componentIdLookup; mapPropertyDescriptors = builder.mapPropertyDescriptors; @@ -50,8 +51,8 @@ private FlowMappingOptions(final Builder builder) { localNodeOrdinal = builder.localNodeOrdinal; } - public SensitiveValueEncryptor getSensitiveValueEncryptor() { - return encryptor; + public PropertyEncryptionProvider getPropertyEncryptionProvider() { + return propertyEncryptionProvider; } public VersionedComponentStateLookup getStateLookup() { @@ -99,7 +100,7 @@ public int getLocalNodeOrdinal() { } public static class Builder { - private SensitiveValueEncryptor encryptor; + private PropertyEncryptionProvider propertyEncryptionProvider; private VersionedComponentStateLookup stateLookup; private ComponentIdLookup componentIdLookup; private boolean mapPropertyDescriptors; @@ -113,14 +114,14 @@ public static class Builder { private int localNodeOrdinal = 0; /** - * Sets the SensitiveValueEncryptor to use for encrypting sensitive values. This value must be set - * if {@link #mapSensitiveConfiguration(boolean) mapSensitiveConfiguration} is set to true. + * Sets the Property Encryption Provider to use for encrypting sensitive values. Must be set when + * {@link #mapSensitiveConfiguration(boolean) mapSensitiveConfiguration} is set to true. * - * @param encryptor the PropertyEncryptor to use + * @param propertyEncryptionProvider the Property Encryption Provider to use * @return the builder */ - public Builder sensitiveValueEncryptor(final SensitiveValueEncryptor encryptor) { - this.encryptor = encryptor; + public Builder propertyEncryptionProvider(final PropertyEncryptionProvider propertyEncryptionProvider) { + this.propertyEncryptionProvider = propertyEncryptionProvider; return this; } @@ -162,9 +163,9 @@ public Builder mapPropertyDescriptors(final boolean mapPropertyDescriptors) { } /** - * Sets whether or not to map sensitive values. If true, the {@link #sensitiveValueEncryptor(SensitiveValueEncryptor)} must be set + * Sets whether to map sensitive values * - * @param mapSensitiveConfiguration whether or not sensitive values should be mapped + * @param mapSensitiveConfiguration whether sensitive values should be mapped * @return the builder */ public Builder mapSensitiveConfiguration(final boolean mapSensitiveConfiguration) { @@ -254,15 +255,18 @@ public Builder localNodeOrdinal(final int localNodeOrdinal) { * * @return the FlowMappingOptions * @throws NullPointerException if the {@link #stateLookup(VersionedComponentStateLookup) StateLookup} is not set, the - * {@link #componentIdLookup(ComponentIdLookup) ComponentIdLookup} is not set, or if {@link #mapSensitiveConfiguration(boolean) mapSensitiveConfiguration} - * is set to true but the {@link #sensitiveValueEncryptor(SensitiveValueEncryptor) SensitiveValueEncryptor} has not been set + * {@link #componentIdLookup(ComponentIdLookup) ComponentIdLookup} is not set, or the + * {@link #stateManagerProvider(StateManagerProvider) StateManagerProvider} is not set when + * {@link #mapComponentState(boolean) mapComponentState} is set to true + * @throws IllegalArgumentException if {@link #mapSensitiveConfiguration(boolean) mapSensitiveConfiguration} is set to true but the + * {@link #propertyEncryptionProvider(PropertyEncryptionProvider) PropertyEncryptionProvider} has not been set */ public FlowMappingOptions build() { requireNonNull(stateLookup, "State Lookup must be set"); requireNonNull(componentIdLookup, "Component ID Lookup must be set"); - if (mapSensitiveConfiguration) { - requireNonNull(encryptor, "Property Encryptor must be set when sensitive configuration is to be mapped"); + if (mapSensitiveConfiguration && propertyEncryptionProvider == null) { + throw new IllegalArgumentException("Property Encryption Provider must be set when sensitive configuration is to be mapped"); } if (mapComponentState) { @@ -278,7 +282,7 @@ public FlowMappingOptions build() { * a dataflow to a NiFi Registry. */ public static final FlowMappingOptions DEFAULT_OPTIONS = new Builder() - .sensitiveValueEncryptor(null) + .propertyEncryptionProvider(null) .stateLookup(VersionedComponentStateLookup.ENABLED_OR_DISABLED) .componentIdLookup(ComponentIdLookup.VERSIONED_OR_GENERATE) .mapPropertyDescriptors(true) diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/mapping/SensitiveValueEncryptor.java b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/mapping/SensitiveValueEncryptor.java deleted file mode 100644 index ce43c465a85b..000000000000 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/registry/flow/mapping/SensitiveValueEncryptor.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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.registry.flow.mapping; - -public interface SensitiveValueEncryptor { - String encrypt(String value); -} diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/security/encryption/InternalPassThroughPropertyEncryptionProvider.java b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/security/encryption/InternalPassThroughPropertyEncryptionProvider.java new file mode 100644 index 000000000000..a47d7bc296b3 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/security/encryption/InternalPassThroughPropertyEncryptionProvider.java @@ -0,0 +1,41 @@ +/* + * 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.security.encryption; + +/** + * Internal Pass Through implementation of Property Encryption Provider that does not modify property values. + * + *

Intended for flows that are mapped and synchronized in memory rather than persisted. Sensitive values still pass + * through {@link SensitivePropertyCodec}, which represents them as hexadecimal, so a flow mapped using this Provider + * must also be synchronized using this Provider in order to recover the original values.

+ */ +public class InternalPassThroughPropertyEncryptionProvider implements PropertyEncryptionProvider { + @Override + public void initialize(final PropertyEncryptionProviderInitializationContext context) { + + } + + @Override + public byte[] encrypt(final byte[] property, final SensitivePropertyContext context) { + return property; + } + + @Override + public byte[] decrypt(final byte[] encryptedProperty, final SensitivePropertyContext context) { + return encryptedProperty; + } +} diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/security/encryption/PropertyEncryptionEncoder.java b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/security/encryption/PropertyEncryptionEncoder.java new file mode 100644 index 000000000000..dc1e4fad2c94 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/security/encryption/PropertyEncryptionEncoder.java @@ -0,0 +1,73 @@ +/* + * 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.security.encryption; + +import java.util.Objects; + +/** + * Encoder for the standard representation of encrypted sensitive values in serialized flows. + * + *

Encrypted values are wrapped with the {@code enc{}} prefix and suffix. The wrapper distinguishes an encrypted + * value from a value stored as plaintext, which occurs when a sensitive property references a Parameter or when a flow + * is mapped without a Property Encryption Provider.

+ */ +public final class PropertyEncryptionEncoder { + + private static final String PREFIX = "enc{"; + + private static final String SUFFIX = "}"; + + private PropertyEncryptionEncoder() { + + } + + /** + * Determine whether a value is encoded as an encrypted value + * + * @param value Value to be evaluated, which may be null + * @return Whether the value is wrapped with the encrypted value prefix and suffix + */ + public static boolean isEncrypted(final String value) { + return value != null && value.startsWith(PREFIX) && value.endsWith(SUFFIX); + } + + /** + * Get an encrypted value wrapped with the encrypted value prefix and suffix + * + * @param encryptedValue Encrypted value to be wrapped + * @return Encrypted value wrapped with the prefix and suffix + */ + public static String getEncoded(final String encryptedValue) { + Objects.requireNonNull(encryptedValue, "Encrypted value required"); + return PREFIX + encryptedValue + SUFFIX; + } + + /** + * Get an encrypted value with the encrypted value prefix and suffix removed + * + * @param encodedValue Encrypted value wrapped with the prefix and suffix + * @return Encrypted value without the prefix and suffix + * @throws IllegalArgumentException Thrown when the value is not wrapped with the prefix and suffix + */ + public static String getDecoded(final String encodedValue) { + if (isEncrypted(encodedValue)) { + return encodedValue.substring(PREFIX.length(), encodedValue.length() - SUFFIX.length()); + } + + throw new IllegalArgumentException("Value not encoded with required prefix and suffix delimiters"); + } +} diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/security/encryption/SensitivePropertyCodec.java b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/security/encryption/SensitivePropertyCodec.java new file mode 100644 index 000000000000..d95fa86b652a --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/security/encryption/SensitivePropertyCodec.java @@ -0,0 +1,76 @@ +/* + * 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.security.encryption; + +import java.nio.charset.StandardCharsets; +import java.util.HexFormat; +import java.util.Objects; + +/** + * Codec that adapts the byte-oriented Property Encryption Provider to the string representation of sensitive values + * stored in the flow configuration. + * + *

Values are encoded as UTF-8 before encryption and encrypted values are represented as hexadecimal, matching the + * representation written inside the {@code enc{}} wrapper of persisted flows.

+ */ +public final class SensitivePropertyCodec { + private static final HexFormat HEX_FORMAT = HexFormat.of(); + + private SensitivePropertyCodec() { + } + + /** + * Encrypt a sensitive value and return the hexadecimal representation of the encrypted value + * + * @param provider Property Encryption Provider + * @param value Sensitive value to be encrypted + * @param context Context describing the value being protected + * @return Hexadecimal representation of the encrypted value + * @throws PropertyEncryptionException Thrown when encryption fails + */ + public static String encrypt(final PropertyEncryptionProvider provider, final String value, final SensitivePropertyContext context) { + Objects.requireNonNull(provider, "Property Encryption Provider required"); + Objects.requireNonNull(value, "Value required"); + + final byte[] encrypted = provider.encrypt(value.getBytes(StandardCharsets.UTF_8), context); + return HEX_FORMAT.formatHex(encrypted); + } + + /** + * Decrypt the hexadecimal representation of an encrypted sensitive value + * + * @param provider Property Encryption Provider + * @param encryptedValue Hexadecimal representation of the encrypted value + * @param context Context describing the value being protected, which must equal the context supplied on encryption + * @return Decrypted sensitive value + * @throws PropertyEncryptionException Thrown when decryption fails + */ + public static String decrypt(final PropertyEncryptionProvider provider, final String encryptedValue, final SensitivePropertyContext context) { + Objects.requireNonNull(provider, "Property Encryption Provider required"); + Objects.requireNonNull(encryptedValue, "Encrypted value required"); + + final byte[] encrypted; + try { + encrypted = HEX_FORMAT.parseHex(encryptedValue); + } catch (final IllegalArgumentException e) { + throw new PropertyEncryptionException("Sensitive property is not a valid hexadecimal encrypted value", e); + } + + final byte[] decrypted = provider.decrypt(encrypted, context); + return new String(decrypted, StandardCharsets.UTF_8); + } +} diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/security/encryption/SensitivePropertyContextFactory.java b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/security/encryption/SensitivePropertyContextFactory.java new file mode 100644 index 000000000000..09e24f0617e5 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/security/encryption/SensitivePropertyContextFactory.java @@ -0,0 +1,95 @@ +/* + * 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.security.encryption; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Factory for the Sensitive Property Context supplied to Property Encryption Providers. + * + *

A Provider may bind the context to the encrypted value, so the context supplied when decrypting a value must equal + * the context supplied when the value was encrypted. Encryption and decryption call sites therefore build contexts + * through this factory rather than assembling attribute maps directly.

+ * + *

Attributes with a null value are omitted, which allows call sites that cannot resolve an attribute to produce the + * same context on both sides of the operation.

+ */ +public final class SensitivePropertyContextFactory { + + private static final String PROXY_PASSWORD_PROPERTY_NAME = "Proxy Password"; + + private SensitivePropertyContextFactory() { + } + + /** + * Get the context for a sensitive property configured on a flow component + * + * @param componentId Identifier of the component instance that owns the property + * @param componentType Type of the component that owns the property + * @param propertyName Name of the property + * @return Sensitive Property Context + */ + public static SensitivePropertyContext forComponent(final String componentId, final String componentType, final String propertyName) { + final Map attributes = new LinkedHashMap<>(); + putAttribute(attributes, SensitivePropertyAttribute.COMPONENT_ID, componentId); + putAttribute(attributes, SensitivePropertyAttribute.COMPONENT_TYPE, componentType); + putAttribute(attributes, SensitivePropertyAttribute.PROPERTY_NAME, propertyName); + return new SensitivePropertyContext(SensitivePropertyCategory.COMPONENT_PROPERTY, attributes); + } + + /** + * Get the context for the proxy password of a Remote Process Group. A Remote Process Group is not a configurable + * extension, so the context carries no component type. + * + * @param remoteProcessGroupId Identifier of the Remote Process Group instance + * @return Sensitive Property Context + */ + public static SensitivePropertyContext forRemoteProcessGroupProxyPassword(final String remoteProcessGroupId) { + return forComponent(remoteProcessGroupId, null, PROXY_PASSWORD_PROPERTY_NAME); + } + + /** + * Get the context for a sensitive Parameter value + * + * @param parameterContextName Name of the Parameter Context that contains the Parameter + * @param parameterName Name of the Parameter + * @return Sensitive Property Context + */ + public static SensitivePropertyContext forParameter(final String parameterContextName, final String parameterName) { + final Map attributes = new LinkedHashMap<>(); + putAttribute(attributes, SensitivePropertyAttribute.PARAMETER_CONTEXT_NAME, parameterContextName); + putAttribute(attributes, SensitivePropertyAttribute.PARAMETER_NAME, parameterName); + return new SensitivePropertyContext(SensitivePropertyCategory.PARAMETER, attributes); + } + + /** + * Get the context for an authorization token stored on behalf of an authenticated user. The context carries no + * attributes, because nothing describing the user is known before the stored token has been decrypted. + * + * @return Sensitive Property Context + */ + public static SensitivePropertyContext forAuthorizationToken() { + return new SensitivePropertyContext(SensitivePropertyCategory.AUTHORIZATION_TOKEN, Map.of()); + } + + private static void putAttribute(final Map attributes, final SensitivePropertyAttribute attribute, final String value) { + if (value != null) { + attributes.put(attribute.getKey(), value); + } + } +} diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/test/java/org/apache/nifi/security/encryption/InternalPassThroughPropertyEncryptionProviderTest.java b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/test/java/org/apache/nifi/security/encryption/InternalPassThroughPropertyEncryptionProviderTest.java new file mode 100644 index 000000000000..dbefe8b68711 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/test/java/org/apache/nifi/security/encryption/InternalPassThroughPropertyEncryptionProviderTest.java @@ -0,0 +1,57 @@ +/* + * 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.security.encryption; + +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +class InternalPassThroughPropertyEncryptionProviderTest { + private static final String VALUE = "Sensitive Value"; + + private static final SensitivePropertyContext CONTEXT = new SensitivePropertyContext(SensitivePropertyCategory.COMPONENT_PROPERTY, Map.of()); + + private final InternalPassThroughPropertyEncryptionProvider provider = new InternalPassThroughPropertyEncryptionProvider(); + + @Test + void testEncryptReturnsProperty() { + final byte[] property = VALUE.getBytes(StandardCharsets.UTF_8); + + assertArrayEquals(property, provider.encrypt(property, CONTEXT)); + } + + @Test + void testDecryptReturnsEncryptedProperty() { + final byte[] encryptedProperty = VALUE.getBytes(StandardCharsets.UTF_8); + + assertArrayEquals(encryptedProperty, provider.decrypt(encryptedProperty, CONTEXT)); + } + + @Test + void testCodecRoundTrip() { + final String encoded = SensitivePropertyCodec.encrypt(provider, VALUE, CONTEXT); + assertNotEquals(VALUE, encoded); + + final String decoded = SensitivePropertyCodec.decrypt(provider, encoded, CONTEXT); + assertEquals(VALUE, decoded); + } +} diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/test/java/org/apache/nifi/security/encryption/PropertyEncryptionEncoderTest.java b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/test/java/org/apache/nifi/security/encryption/PropertyEncryptionEncoderTest.java new file mode 100644 index 000000000000..ba5cdb1bbd47 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/test/java/org/apache/nifi/security/encryption/PropertyEncryptionEncoderTest.java @@ -0,0 +1,82 @@ +/* + * 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.security.encryption; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PropertyEncryptionEncoderTest { + + private static final String ENCRYPTED_VALUE = "0123456789abcdef"; + + private static final String ENCODED_VALUE = "enc{0123456789abcdef}"; + + private static final String PARAMETER_REFERENCE = "#{Parameter}"; + + @Test + void testIsEncrypted() { + assertTrue(PropertyEncryptionEncoder.isEncrypted(ENCODED_VALUE)); + } + + @Test + void testIsEncryptedNull() { + assertFalse(PropertyEncryptionEncoder.isEncrypted(null)); + } + + @Test + void testIsEncryptedPlaintext() { + assertFalse(PropertyEncryptionEncoder.isEncrypted(ENCRYPTED_VALUE)); + assertFalse(PropertyEncryptionEncoder.isEncrypted(PARAMETER_REFERENCE)); + } + + @Test + void testIsEncryptedSuffixNotFound() { + assertFalse(PropertyEncryptionEncoder.isEncrypted("enc{0123456789abcdef")); + } + + @Test + void testGetEncoded() { + assertEquals(ENCODED_VALUE, PropertyEncryptionEncoder.getEncoded(ENCRYPTED_VALUE)); + } + + @Test + void testGetEncodedNull() { + assertThrows(NullPointerException.class, () -> PropertyEncryptionEncoder.getEncoded(null)); + } + + @Test + void testGetDecoded() { + assertEquals(ENCRYPTED_VALUE, PropertyEncryptionEncoder.getDecoded(ENCODED_VALUE)); + } + + @Test + void testGetDecodedNotEncoded() { + assertThrows(IllegalArgumentException.class, () -> PropertyEncryptionEncoder.getDecoded(ENCRYPTED_VALUE)); + assertThrows(IllegalArgumentException.class, () -> PropertyEncryptionEncoder.getDecoded(null)); + } + + @Test + void testGetEncodedGetDecodedRoundTrip() { + final String encoded = PropertyEncryptionEncoder.getEncoded(ENCRYPTED_VALUE); + assertTrue(PropertyEncryptionEncoder.isEncrypted(encoded)); + assertEquals(ENCRYPTED_VALUE, PropertyEncryptionEncoder.getDecoded(encoded)); + } +} diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/test/java/org/apache/nifi/security/encryption/SensitivePropertyCodecTest.java b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/test/java/org/apache/nifi/security/encryption/SensitivePropertyCodecTest.java new file mode 100644 index 000000000000..b2c6b3c0d839 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/test/java/org/apache/nifi/security/encryption/SensitivePropertyCodecTest.java @@ -0,0 +1,114 @@ +/* + * 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.security.encryption; + +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.util.HexFormat; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class SensitivePropertyCodecTest { + private static final String VALUE = "Sensitive Value"; + + private static final String MULTIBYTE_VALUE = "Sensitive \u00e4\u00f6\u00fc Value"; + + private static final SensitivePropertyContext CONTEXT = new SensitivePropertyContext(SensitivePropertyCategory.COMPONENT_PROPERTY, Map.of()); + + private final PropertyEncryptionProvider provider = new ReversingPropertyEncryptionProvider(); + + @Test + void testEncryptDecrypt() { + final String encrypted = SensitivePropertyCodec.encrypt(provider, VALUE, CONTEXT); + assertNotNull(encrypted); + + final String decrypted = SensitivePropertyCodec.decrypt(provider, encrypted, CONTEXT); + assertEquals(VALUE, decrypted); + } + + @Test + void testEncryptDecryptMultibyteCharacters() { + final String encrypted = SensitivePropertyCodec.encrypt(provider, MULTIBYTE_VALUE, CONTEXT); + + final String decrypted = SensitivePropertyCodec.decrypt(provider, encrypted, CONTEXT); + assertEquals(MULTIBYTE_VALUE, decrypted); + } + + /** + * The encoded representation must be the hexadecimal encoding of the bytes returned from the Provider, which is the + * representation written inside the encryption wrapper of persisted flow configurations. + */ + @Test + void testEncryptHexadecimalRepresentation() { + final PropertyEncryptionProvider passThroughProvider = new InternalPassThroughPropertyEncryptionProvider(); + + final String encrypted = SensitivePropertyCodec.encrypt(passThroughProvider, VALUE, CONTEXT); + + assertEquals(HexFormat.of().formatHex(VALUE.getBytes(StandardCharsets.UTF_8)), encrypted); + } + + @Test + void testDecryptHexadecimalNotValid() { + final PropertyEncryptionException exception = assertThrows( + PropertyEncryptionException.class, + () -> SensitivePropertyCodec.decrypt(provider, "Not Hexadecimal", CONTEXT) + ); + + assertNotNull(exception.getCause()); + } + + @Test + void testEncryptProviderRequired() { + assertThrows(NullPointerException.class, () -> SensitivePropertyCodec.encrypt(null, VALUE, CONTEXT)); + } + + @Test + void testDecryptProviderRequired() { + assertThrows(NullPointerException.class, () -> SensitivePropertyCodec.decrypt(null, "00", CONTEXT)); + } + + /** + * Provider that reverses the supplied bytes, which distinguishes the encoded representation from the plain value + */ + private static class ReversingPropertyEncryptionProvider implements PropertyEncryptionProvider { + @Override + public void initialize(final PropertyEncryptionProviderInitializationContext context) { + } + + @Override + public byte[] encrypt(final byte[] property, final SensitivePropertyContext context) { + return getReversed(property); + } + + @Override + public byte[] decrypt(final byte[] encryptedProperty, final SensitivePropertyContext context) { + return getReversed(encryptedProperty); + } + + private byte[] getReversed(final byte[] bytes) { + final byte[] reversed = new byte[bytes.length]; + for (int i = 0; i < bytes.length; i++) { + reversed[i] = bytes[bytes.length - 1 - i]; + } + return reversed; + } + } +} diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/test/java/org/apache/nifi/security/encryption/SensitivePropertyContextFactoryTest.java b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/test/java/org/apache/nifi/security/encryption/SensitivePropertyContextFactoryTest.java new file mode 100644 index 000000000000..edd1a7a1ed0a --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/test/java/org/apache/nifi/security/encryption/SensitivePropertyContextFactoryTest.java @@ -0,0 +1,117 @@ +/* + * 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.security.encryption; + +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SensitivePropertyContextFactoryTest { + private static final String COMPONENT_ID = "0ce4d61b-e4f4-3f2a-b1d0-0d9f6b0e7a11"; + + private static final String COMPONENT_TYPE = "org.apache.nifi.processors.standard.InvokeHTTP"; + + private static final String PROPERTY_NAME = "Request Password"; + + private static final String PARAMETER_CONTEXT_NAME = "Production"; + + private static final String PARAMETER_NAME = "database.password"; + + @Test + void testForComponent() { + final SensitivePropertyContext context = SensitivePropertyContextFactory.forComponent(COMPONENT_ID, COMPONENT_TYPE, PROPERTY_NAME); + + assertEquals(SensitivePropertyCategory.COMPONENT_PROPERTY, context.category()); + assertEquals( + Map.of( + SensitivePropertyAttribute.COMPONENT_ID.getKey(), COMPONENT_ID, + SensitivePropertyAttribute.COMPONENT_TYPE.getKey(), COMPONENT_TYPE, + SensitivePropertyAttribute.PROPERTY_NAME.getKey(), PROPERTY_NAME + ), + context.attributes() + ); + } + + /** + * A call site that cannot resolve an attribute must produce the same context as the site that also cannot resolve + * it, so null attributes are omitted rather than stored + */ + @Test + void testForComponentNullAttributesOmitted() { + final SensitivePropertyContext context = SensitivePropertyContextFactory.forComponent(COMPONENT_ID, null, PROPERTY_NAME); + + assertEquals( + Map.of( + SensitivePropertyAttribute.COMPONENT_ID.getKey(), COMPONENT_ID, + SensitivePropertyAttribute.PROPERTY_NAME.getKey(), PROPERTY_NAME + ), + context.attributes() + ); + } + + @Test + void testForRemoteProcessGroupProxyPassword() { + final SensitivePropertyContext context = SensitivePropertyContextFactory.forRemoteProcessGroupProxyPassword(COMPONENT_ID); + + assertEquals(SensitivePropertyCategory.COMPONENT_PROPERTY, context.category()); + assertEquals(COMPONENT_ID, context.attributes().get(SensitivePropertyAttribute.COMPONENT_ID.getKey())); + assertTrue(context.attributes().containsKey(SensitivePropertyAttribute.PROPERTY_NAME.getKey())); + assertEquals(2, context.attributes().size()); + } + + @Test + void testForParameter() { + final SensitivePropertyContext context = SensitivePropertyContextFactory.forParameter(PARAMETER_CONTEXT_NAME, PARAMETER_NAME); + + assertEquals(SensitivePropertyCategory.PARAMETER, context.category()); + assertEquals( + Map.of( + SensitivePropertyAttribute.PARAMETER_CONTEXT_NAME.getKey(), PARAMETER_CONTEXT_NAME, + SensitivePropertyAttribute.PARAMETER_NAME.getKey(), PARAMETER_NAME + ), + context.attributes() + ); + } + + @Test + void testForAuthorizationToken() { + final SensitivePropertyContext context = SensitivePropertyContextFactory.forAuthorizationToken(); + + assertEquals(SensitivePropertyCategory.AUTHORIZATION_TOKEN, context.category()); + assertEquals(Map.of(), context.attributes()); + } + + /** + * Contexts built for the same value must be equal regardless of the call site, because a Provider may bind the + * context to the encrypted value + */ + @Test + void testContextsEqualForSameValue() { + assertEquals( + SensitivePropertyContextFactory.forComponent(COMPONENT_ID, COMPONENT_TYPE, PROPERTY_NAME), + SensitivePropertyContextFactory.forComponent(COMPONENT_ID, COMPONENT_TYPE, PROPERTY_NAME) + ); + assertEquals( + SensitivePropertyContextFactory.forParameter(PARAMETER_CONTEXT_NAME, PARAMETER_NAME), + SensitivePropertyContextFactory.forParameter(PARAMETER_CONTEXT_NAME, PARAMETER_NAME) + ); + assertEquals(SensitivePropertyContextFactory.forAuthorizationToken(), SensitivePropertyContextFactory.forAuthorizationToken()); + } +} diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml b/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml index 7aeefd09b884..384ebf9132e7 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml @@ -23,11 +23,6 @@ nifi-framework-core jar - - org.apache.nifi - nifi-property-encryptor - 2.12.0-SNAPSHOT - org.apache.nifi nifi-framework-core-api diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java index 155b0520113e..32cfc3b7ae53 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java @@ -165,7 +165,6 @@ import org.apache.nifi.diagnostics.StorageUsage; import org.apache.nifi.diagnostics.SystemDiagnostics; import org.apache.nifi.diagnostics.SystemDiagnosticsFactory; -import org.apache.nifi.encrypt.PropertyEncryptor; import org.apache.nifi.engine.FlowEngine; import org.apache.nifi.events.BulletinFactory; import org.apache.nifi.events.EventReporter; @@ -223,6 +222,7 @@ import org.apache.nifi.reporting.StandardEventAccess; import org.apache.nifi.reporting.UserAwareEventAccess; import org.apache.nifi.scheduling.SchedulingStrategy; +import org.apache.nifi.security.encryption.PropertyEncryptionProvider; import org.apache.nifi.services.FlowService; import org.apache.nifi.stream.io.LimitingInputStream; import org.apache.nifi.stream.io.StreamUtils; @@ -376,9 +376,9 @@ public class FlowController implements ReportingTaskProvider, FlowAnalysisRulePr private final int heartbeatDelaySeconds; /** - * The sensitive property string encryptor * + * Provider that protects sensitive values written to and read from the flow configuration */ - private final PropertyEncryptor encryptor; + private final PropertyEncryptionProvider propertyEncryptionProvider; private final ScheduledExecutorService clusterTaskExecutor = new FlowEngine(3, "Clustering Tasks", true); private final ResourceClaimManager resourceClaimManager = new StandardResourceClaimManager(); @@ -433,7 +433,7 @@ public static FlowController createStandaloneInstance( final Authorizer authorizer, final AuditService auditService, final ComponentMetricReporter componentMetricReporter, - final PropertyEncryptor encryptor, + final PropertyEncryptionProvider propertyEncryptionProvider, final BulletinRepository bulletinRepo, final ExtensionDiscoveringManager extensionManager, final StatusHistoryRepository statusHistoryRepository, @@ -449,7 +449,7 @@ public static FlowController createStandaloneInstance( authorizer, auditService, componentMetricReporter, - encryptor, + propertyEncryptionProvider, /* configuredForClustering */ false, /* NodeProtocolSender */ null, bulletinRepo, @@ -472,7 +472,7 @@ public static FlowController createClusteredInstance( final Authorizer authorizer, final AuditService auditService, final ComponentMetricReporter componentMetricReporter, - final PropertyEncryptor encryptor, + final PropertyEncryptionProvider propertyEncryptionProvider, final NodeProtocolSender protocolSender, final BulletinRepository bulletinRepo, final ClusterCoordinator clusterCoordinator, @@ -493,7 +493,7 @@ public static FlowController createClusteredInstance( authorizer, auditService, componentMetricReporter, - encryptor, + propertyEncryptionProvider, /* configuredForClustering */ true, protocolSender, bulletinRepo, @@ -516,7 +516,7 @@ private FlowController( final Authorizer authorizer, final AuditService auditService, final ComponentMetricReporter componentMetricReporter, - final PropertyEncryptor encryptor, + final PropertyEncryptionProvider propertyEncryptionProvider, final boolean configuredForClustering, final NodeProtocolSender protocolSender, final BulletinRepository bulletinRepo, @@ -533,7 +533,7 @@ private FlowController( maxTimerDrivenThreads = new AtomicInteger(10); - this.encryptor = encryptor; + this.propertyEncryptionProvider = requireNonNull(propertyEncryptionProvider, "Property Encryption Provider required"); this.nifiProperties = nifiProperties; this.heartbeatMonitor = heartbeatMonitor; this.leaderElectionManager = leaderElectionManager; @@ -1819,8 +1819,8 @@ public ConnectorValidationTrigger getConnectorValidationTrigger() { return connectorValidationTrigger; } - public PropertyEncryptor getEncryptor() { - return encryptor; + public PropertyEncryptionProvider getPropertyEncryptionProvider() { + return propertyEncryptionProvider; } /** diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/FlowControllerFlowContextFactory.java b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/FlowControllerFlowContextFactory.java index d7467b1b28fe..341ba781b361 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/FlowControllerFlowContextFactory.java +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/FlowControllerFlowContextFactory.java @@ -41,6 +41,7 @@ import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedProcessGroup; import org.apache.nifi.registry.flow.mapping.VersionedComponentFlowMapper; import org.apache.nifi.registry.flow.mapping.VersionedComponentStateLookup; +import org.apache.nifi.security.encryption.InternalPassThroughPropertyEncryptionProvider; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -93,7 +94,7 @@ private void copyGroupContents(final ProcessGroup sourceGroup, final ProcessGrou .mapSensitiveConfiguration(true) .mapPropertyDescriptors(true) .stateLookup(VersionedComponentStateLookup.ENABLED_OR_DISABLED) - .sensitiveValueEncryptor(value -> value) + .propertyEncryptionProvider(new InternalPassThroughPropertyEncryptionProvider()) .componentIdLookup(ComponentIdLookup.VERSIONED_OR_GENERATE) .mapInstanceIdentifiers(true) .mapControllerServiceReferencesToVersionedId(true) diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/FlowControllerProcessGroupFacadeFactory.java b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/FlowControllerProcessGroupFacadeFactory.java index 53bbe05b9ff2..5d4c9c191bf4 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/FlowControllerProcessGroupFacadeFactory.java +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/FlowControllerProcessGroupFacadeFactory.java @@ -32,6 +32,7 @@ import org.apache.nifi.registry.flow.mapping.FlowMappingOptions; import org.apache.nifi.registry.flow.mapping.VersionedComponentFlowMapper; import org.apache.nifi.registry.flow.mapping.VersionedComponentStateLookup; +import org.apache.nifi.security.encryption.InternalPassThroughPropertyEncryptionProvider; public class FlowControllerProcessGroupFacadeFactory implements ProcessGroupFacadeFactory { private final FlowController flowController; @@ -46,7 +47,7 @@ public ProcessGroupFacade create(final ProcessGroup processGroup, final Componen .mapSensitiveConfiguration(true) .mapPropertyDescriptors(true) .stateLookup(VersionedComponentStateLookup.IDENTITY_LOOKUP) - .sensitiveValueEncryptor(value -> value) + .propertyEncryptionProvider(new InternalPassThroughPropertyEncryptionProvider()) .componentIdLookup(ComponentIdLookup.VERSIONED_OR_GENERATE) .mapInstanceIdentifiers(true) .mapControllerServiceReferencesToVersionedId(true) diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/StandardFlowManager.java b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/StandardFlowManager.java index eefa16c8d1e5..6d155ec1f081 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/StandardFlowManager.java +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/StandardFlowManager.java @@ -66,8 +66,6 @@ import org.apache.nifi.controller.service.StandardConfigurationContext; import org.apache.nifi.deprecation.log.DeprecationLogger; import org.apache.nifi.deprecation.log.DeprecationLoggerFactory; -import org.apache.nifi.flow.VersionedExternalFlow; -import org.apache.nifi.flow.VersionedParameterContext; import org.apache.nifi.flowanalysis.FlowAnalysisRule; import org.apache.nifi.flowfile.FlowFilePrioritizer; import org.apache.nifi.groups.ProcessGroup; @@ -93,11 +91,6 @@ import org.apache.nifi.parameter.StandardParameterReferenceManager; import org.apache.nifi.processor.Processor; import org.apache.nifi.registry.flow.FlowRegistryClientNode; -import org.apache.nifi.registry.flow.mapping.ComponentIdLookup; -import org.apache.nifi.registry.flow.mapping.FlowMappingOptions; -import org.apache.nifi.registry.flow.mapping.InstantiatedVersionedProcessGroup; -import org.apache.nifi.registry.flow.mapping.VersionedComponentFlowMapper; -import org.apache.nifi.registry.flow.mapping.VersionedComponentStateLookup; import org.apache.nifi.remote.PublicPort; import org.apache.nifi.remote.StandardPublicPort; import org.apache.nifi.remote.StandardRemoteProcessGroup; @@ -305,7 +298,8 @@ public Port createLocalOutputPort(String id, String name) { public ProcessGroup createProcessGroup(final String id, final String connectorId) { final StatelessGroupNodeFactory statelessGroupNodeFactory = new StandardStatelessGroupNodeFactory(flowController, sslContext, flowController.createKerberosConfig(nifiProperties)); - final ProcessGroup group = new StandardProcessGroup(requireNonNull(id), flowController.getControllerServiceProvider(), processScheduler, flowController.getEncryptor(), + final ProcessGroup group = new StandardProcessGroup(requireNonNull(id), flowController.getControllerServiceProvider(), processScheduler, + flowController.getPropertyEncryptionProvider(), flowController.getExtensionManager(), flowController.getStateManagerProvider(), this, flowController.getReloadComponent(), flowController, flowController, nifiProperties, statelessGroupNodeFactory, flowController.getAssetManager(), connectorId); @@ -817,43 +811,6 @@ public ConnectorNode createConnector(final String type, final String id, final B return connectorNode; } - private void copyGroupContents(final ProcessGroup sourceGroup, final ProcessGroup destinationGroup, final String componentIdSeed) { - final FlowMappingOptions flowMappingOptions = new FlowMappingOptions.Builder() - .mapSensitiveConfiguration(true) - .mapPropertyDescriptors(true) - .stateLookup(VersionedComponentStateLookup.ENABLED_OR_DISABLED) - .sensitiveValueEncryptor(value -> value) - .componentIdLookup(ComponentIdLookup.VERSIONED_OR_GENERATE) - .mapInstanceIdentifiers(true) - .mapControllerServiceReferencesToVersionedId(true) - .mapFlowRegistryClientId(true) - .mapAssetReferences(true) - .build(); - - final VersionedComponentFlowMapper flowMapper = new VersionedComponentFlowMapper(flowController.getExtensionManager(), flowMappingOptions); - final Map parameterContexts = flowMapper.mapParameterContexts(sourceGroup, true, Map.of()); - final InstantiatedVersionedProcessGroup versionedGroup = flowMapper.mapProcessGroup(sourceGroup, flowController.getControllerServiceProvider(), this, true); - final VersionedExternalFlow versionedExternalFlow = new VersionedExternalFlow(); - versionedExternalFlow.setFlowContents(versionedGroup); - versionedExternalFlow.setExternalControllerServices(Map.of()); - versionedExternalFlow.setParameterProviders(Map.of()); - versionedExternalFlow.setParameterContexts(parameterContexts); - - destinationGroup.updateFlow(versionedExternalFlow, componentIdSeed, false, true, true); - } - - private void gatherParameterContexts(final ProcessGroup sourceGroup, final Map parameterContexts) { - final ParameterContext parameterContext = sourceGroup.getParameterContext(); - if (parameterContext != null && !parameterContexts.containsKey(parameterContext.getIdentifier())) { - parameterContexts.put(parameterContext.getIdentifier(), parameterContext); - } - - for (final ProcessGroup childGroup : sourceGroup.getProcessGroups()) { - gatherParameterContexts(childGroup, parameterContexts); - } - } - - @Override public List getAllConnectors() { return flowController.getConnectorRepository().getConnectors(ConnectorSyncMode.LOCAL_ONLY); diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/StandardStatelessGroupNodeFactory.java b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/StandardStatelessGroupNodeFactory.java index a7d25e24a165..6cba62b68a5c 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/StandardStatelessGroupNodeFactory.java +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/flow/StandardStatelessGroupNodeFactory.java @@ -64,6 +64,7 @@ import org.apache.nifi.registry.flow.mapping.VersionedComponentFlowMapper; import org.apache.nifi.registry.flow.mapping.VersionedComponentStateLookup; import org.apache.nifi.reporting.BulletinRepository; +import org.apache.nifi.security.encryption.InternalPassThroughPropertyEncryptionProvider; import org.apache.nifi.stateless.engine.ProcessContextFactory; import org.apache.nifi.stateless.engine.StandardStatelessEngine; import org.apache.nifi.stateless.engine.StatelessEngine; @@ -136,7 +137,7 @@ public StatelessGroupNode createStatelessGroupNode(final ProcessGroup group) { .mapInstanceIdentifiers(true) .mapPropertyDescriptors(false) .mapSensitiveConfiguration(true) - .sensitiveValueEncryptor(value -> value) // No need to encrypt, since we won't be persisting the flow + .propertyEncryptionProvider(new InternalPassThroughPropertyEncryptionProvider()) .stateLookup(VersionedComponentStateLookup.IDENTITY_LOOKUP) .mapAssetReferences(true) .build(); @@ -246,7 +247,6 @@ public Future> fetch(final Set bundleCoordinates, final StatelessEngine statelessEngine = new StandardStatelessEngine.Builder() .bulletinRepository(flowController.getBulletinRepository()) .counterRepository(flowController.getCounterRepository()) - .encryptor(flowController.getEncryptor()) .extensionManager(flowController.getExtensionManager()) .assetManager(flowController.getAssetManager()) .extensionRepository(extensionRepository) @@ -299,7 +299,7 @@ public Future> fetch(final Set bundleCoordinates, .componentIdGenerator(idGenerator) .componentScheduler(ComponentScheduler.NOP_SCHEDULER) .componentStopTimeout(Duration.ofSeconds(60)) - .propertyDecryptor(value -> value) + .propertyEncryptionProvider(new InternalPassThroughPropertyEncryptionProvider()) .topLevelGroupId(group.getIdentifier()) .updateDescendantVersionedFlows(true) .updateGroupSettings(true) diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/FlowSerializer.java b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/FlowSerializer.java index 403aee09489e..fdbd1f425855 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/FlowSerializer.java +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/FlowSerializer.java @@ -26,9 +26,6 @@ */ public interface FlowSerializer { - String ENC_PREFIX = "enc{"; - String ENC_SUFFIX = "}"; - /** * Transforms the flow configuration of a controller instance into something that can serialized * diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/FlowSynchronizationUtils.java b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/FlowSynchronizationUtils.java index 743eb2497a8a..ed3ebc1c0519 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/FlowSynchronizationUtils.java +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/FlowSynchronizationUtils.java @@ -19,12 +19,18 @@ import org.apache.nifi.bundle.BundleCoordinate; import org.apache.nifi.components.PropertyDescriptor; import org.apache.nifi.controller.ComponentNode; -import org.apache.nifi.encrypt.EncryptionException; -import org.apache.nifi.encrypt.PropertyEncryptor; import org.apache.nifi.flow.Bundle; +import org.apache.nifi.flow.VersionedComponent; import org.apache.nifi.flow.VersionedConfigurableExtension; +import org.apache.nifi.flow.VersionedExtensionComponent; import org.apache.nifi.flow.VersionedPropertyDescriptor; import org.apache.nifi.nar.ExtensionManager; +import org.apache.nifi.security.encryption.PropertyEncryptionEncoder; +import org.apache.nifi.security.encryption.PropertyEncryptionException; +import org.apache.nifi.security.encryption.PropertyEncryptionProvider; +import org.apache.nifi.security.encryption.SensitivePropertyCodec; +import org.apache.nifi.security.encryption.SensitivePropertyContext; +import org.apache.nifi.security.encryption.SensitivePropertyContextFactory; import org.apache.nifi.util.BundleUtils; import org.apache.nifi.web.api.dto.BundleDTO; import org.slf4j.Logger; @@ -62,7 +68,7 @@ static Set getSensitiveDynamicPropertyNames(final ComponentNode componen extension.getProperties() .entrySet() .stream() - .filter(entry -> isValueSensitive(entry.getValue())) + .filter(entry -> PropertyEncryptionEncoder.isEncrypted(entry.getValue())) .map(Map.Entry::getKey) .forEach(versionedSensitivePropertyNames::add); @@ -82,25 +88,42 @@ static Set getSensitiveDynamicPropertyNames(final ComponentNode componen .collect(Collectors.toSet()); } - static boolean isValueSensitive(final String value) { - return value != null && value.startsWith(FlowSerializer.ENC_PREFIX) && value.endsWith(FlowSerializer.ENC_SUFFIX); - } - - static Map decryptProperties(final Map encrypted, final PropertyEncryptor encryptor) { + /** + * Decrypt the properties of a versioned component. The context supplied for each value is built from the instance identifier + * and type of the component, matching the context supplied when the flow was serialized. + * + * @param component Versioned component that owns the properties + * @param encrypted Properties of the component, which may contain encrypted values + * @param propertyEncryptionProvider Provider used to decrypt sensitive values + * @return Properties with sensitive values decrypted + */ + static Map decryptProperties(final VersionedComponent component, final Map encrypted, + final PropertyEncryptionProvider propertyEncryptionProvider) { final Map decrypted = new HashMap<>(encrypted.size()); - encrypted.forEach((key, value) -> decrypted.put(key, decrypt(value, encryptor))); + encrypted.forEach((key, value) -> decrypted.put(key, decrypt(value, getSensitivePropertyContext(component, key), propertyEncryptionProvider))); return decrypted; } - static String decrypt(final String value, final PropertyEncryptor encryptor) { - if (isValueSensitive(value)) { + /** + * Get the context describing a sensitive property of a versioned component. The instance identifier is used rather than the + * identifier, because the identifier of a mapped component is a generated versioned identifier while the context supplied when + * the value was encrypted described the component instance. + */ + static SensitivePropertyContext getSensitivePropertyContext(final VersionedComponent component, final String propertyName) { + final String componentType = component instanceof final VersionedExtensionComponent extension ? extension.getType() : null; + return SensitivePropertyContextFactory.forComponent(component.getInstanceIdentifier(), componentType, propertyName); + } + + static String decrypt(final String value, final SensitivePropertyContext context, final PropertyEncryptionProvider propertyEncryptionProvider) { + if (PropertyEncryptionEncoder.isEncrypted(value)) { + final String encryptedValue = PropertyEncryptionEncoder.getDecoded(value); try { - return encryptor.decrypt(value.substring(FlowSerializer.ENC_PREFIX.length(), value.length() - FlowSerializer.ENC_SUFFIX.length())); - } catch (EncryptionException e) { + return SensitivePropertyCodec.decrypt(propertyEncryptionProvider, encryptedValue, context); + } catch (final PropertyEncryptionException e) { final String moreDescriptiveMessage = "There was a problem decrypting a sensitive flow configuration value. " + - "Check that the nifi.sensitive.props.key value in nifi.properties matches the value used to encrypt the flow.json.gz file"; + "Check that the Property Encryption Provider configuration matches the configuration used to encrypt the flow.json.gz file"; logger.error(moreDescriptiveMessage, e); - throw new EncryptionException(moreDescriptiveMessage, e); + throw new PropertyEncryptionException(moreDescriptiveMessage, e); } } else { return value; diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardVersionedReportingTaskImporter.java b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardVersionedReportingTaskImporter.java index d48d77d85650..248118971b33 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardVersionedReportingTaskImporter.java +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardVersionedReportingTaskImporter.java @@ -21,13 +21,13 @@ import org.apache.nifi.controller.ReportingTaskNode; import org.apache.nifi.controller.flow.FlowManager; import org.apache.nifi.controller.service.ControllerServiceNode; -import org.apache.nifi.encrypt.PropertyEncryptor; import org.apache.nifi.flow.VersionedControllerService; import org.apache.nifi.flow.VersionedReportingTask; import org.apache.nifi.flow.VersionedReportingTaskSnapshot; import org.apache.nifi.logging.LogLevel; import org.apache.nifi.nar.ExtensionManager; import org.apache.nifi.scheduling.SchedulingStrategy; +import org.apache.nifi.security.encryption.PropertyEncryptionProvider; import java.util.Collections; import java.util.HashSet; @@ -80,7 +80,7 @@ private ReportingTaskNode addReportingTask(final VersionedReportingTask reportin taskNode.setAnnotationData(reportingTask.getAnnotationData()); final Set sensitiveDynamicPropertyNames = getSensitiveDynamicPropertyNames(taskNode, reportingTask); - final Map decryptedProperties = decryptProperties(reportingTask.getProperties(), flowController.getEncryptor()); + final Map decryptedProperties = decryptProperties(reportingTask, reportingTask.getProperties(), flowController.getPropertyEncryptionProvider()); taskNode.setProperties(decryptedProperties, false, sensitiveDynamicPropertyNames); return taskNode; } @@ -98,7 +98,7 @@ private Set importControllerServices(final List sensitiveDynamicPropertyNames = getSensitiveDynamicPropertyNames(serviceNode, controllerService); - final Map decryptedProperties = decryptProperties(controllerService.getProperties(), encryptor); + final Map decryptedProperties = decryptProperties(controllerService, controllerService.getProperties(), propertyEncryptionProvider); serviceNode.setProperties(decryptedProperties, false, sensitiveDynamicPropertyNames); } finally { serviceNode.resumeValidationTrigger(); diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedDataflowMapper.java b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedDataflowMapper.java index 3b84ff727201..086a74c54d3c 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedDataflowMapper.java +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedDataflowMapper.java @@ -45,9 +45,9 @@ import org.apache.nifi.registry.flow.FlowRegistryClientNode; import org.apache.nifi.registry.flow.mapping.ComponentIdLookup; import org.apache.nifi.registry.flow.mapping.FlowMappingOptions; -import org.apache.nifi.registry.flow.mapping.SensitiveValueEncryptor; import org.apache.nifi.registry.flow.mapping.VersionedComponentFlowMapper; import org.apache.nifi.registry.flow.mapping.VersionedComponentStateLookup; +import org.apache.nifi.security.encryption.PropertyEncryptionProvider; import java.util.ArrayList; import java.util.Collections; @@ -60,7 +60,8 @@ public class VersionedDataflowMapper { private final VersionedComponentFlowMapper flowMapper; private final ScheduledStateLookup stateLookup; - public VersionedDataflowMapper(final FlowController flowController, final ExtensionManager extensionManager, final SensitiveValueEncryptor encryptor, final ScheduledStateLookup stateLookup) { + public VersionedDataflowMapper(final FlowController flowController, final ExtensionManager extensionManager, + final PropertyEncryptionProvider propertyEncryptionProvider, final ScheduledStateLookup stateLookup) { this.flowController = flowController; this.stateLookup = stateLookup; @@ -70,7 +71,7 @@ public VersionedDataflowMapper(final FlowController flowController, final Extens .mapSensitiveConfiguration(true) .mapPropertyDescriptors(false) .stateLookup(versionedComponentStateLookup) - .sensitiveValueEncryptor(encryptor) + .propertyEncryptionProvider(propertyEncryptionProvider) .componentIdLookup(ComponentIdLookup.VERSIONED_OR_GENERATE) .mapInstanceIdentifiers(true) .mapControllerServiceReferencesToVersionedId(false) diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedFlowSerializer.java b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedFlowSerializer.java index f57f00d3a994..c7485faa1919 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedFlowSerializer.java +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedFlowSerializer.java @@ -25,7 +25,6 @@ import com.fasterxml.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector; import org.apache.nifi.controller.FlowController; import org.apache.nifi.controller.flow.VersionedDataflow; -import org.apache.nifi.encrypt.PropertyEncryptor; import org.apache.nifi.nar.ExtensionManager; import java.io.IOException; @@ -47,8 +46,7 @@ public VersionedFlowSerializer(final ExtensionManager extensionManager) { @Override public VersionedDataflow transform(final FlowController controller, final ScheduledStateLookup stateLookup) throws FlowSerializationException { - final PropertyEncryptor encryptor = controller.getEncryptor(); - final VersionedDataflowMapper dataflowMapper = new VersionedDataflowMapper(controller, extensionManager, encryptor::encrypt, stateLookup); + final VersionedDataflowMapper dataflowMapper = new VersionedDataflowMapper(controller, extensionManager, controller.getPropertyEncryptionProvider(), stateLookup); final VersionedDataflow dataflow = dataflowMapper.createMapping(); return dataflow; } diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedFlowSynchronizer.java b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedFlowSynchronizer.java index bfee5269475b..1e75666801f7 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedFlowSynchronizer.java +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/VersionedFlowSynchronizer.java @@ -54,7 +54,6 @@ import org.apache.nifi.controller.inheritance.FlowInheritability; import org.apache.nifi.controller.inheritance.FlowInheritabilityCheck; import org.apache.nifi.controller.service.ControllerServiceNode; -import org.apache.nifi.encrypt.PropertyEncryptor; import org.apache.nifi.flow.Bundle; import org.apache.nifi.flow.ExecutionEngine; import org.apache.nifi.flow.ScheduledState; @@ -106,6 +105,10 @@ import org.apache.nifi.registry.flow.mapping.VersionedComponentStateLookup; import org.apache.nifi.remote.RemoteGroupPort; import org.apache.nifi.scheduling.SchedulingStrategy; +import org.apache.nifi.security.encryption.PropertyEncryptionProvider; +import org.apache.nifi.security.encryption.ProviderSensitiveValueDecryptor; +import org.apache.nifi.security.encryption.SensitivePropertyContext; +import org.apache.nifi.security.encryption.SensitivePropertyContextFactory; import org.apache.nifi.services.FlowService; import org.apache.nifi.util.BundleUtils; import org.apache.nifi.util.FlowDifferenceFilters; @@ -192,7 +195,7 @@ public synchronized void sync(final FlowController controller, final DataFlow pr AffectedComponentSet activeSet = null; if (!existingFlowEmpty) { - flowComparison = compareFlows(existingDataFlow, proposedFlow, controller.getEncryptor()); + flowComparison = compareFlows(existingDataFlow, proposedFlow, controller.getPropertyEncryptionProvider()); final Set flowDifferences = flowComparison.getDifferences(); if (flowDifferences.isEmpty()) { @@ -439,7 +442,7 @@ private void synchronizeFlow(final FlowController controller, final DataFlow exi try { final VersionedDataflow versionedFlow = proposedFlow.getVersionedDataflow(); - final PropertyEncryptor encryptor = controller.getEncryptor(); + final PropertyEncryptionProvider propertyEncryptionProvider = controller.getPropertyEncryptionProvider(); if (versionedFlow != null) { controller.setMaxTimerDrivenThreadCount(versionedFlow.getMaxTimerDrivenThreadCount()); @@ -489,14 +492,14 @@ private void synchronizeFlow(final FlowController controller, final DataFlow exi .updateGroupSettings(true) .updateDescendantVersionedFlows(true) .updateRpgUrls(true) - .propertyDecryptor(encryptor::decrypt) + .propertyEncryptionProvider(propertyEncryptionProvider) .build(); final FlowMappingOptions flowMappingOptions = new FlowMappingOptions.Builder() .mapSensitiveConfiguration(true) .mapPropertyDescriptors(false) .stateLookup(stateLookup) - .sensitiveValueEncryptor(encryptor::encrypt) + .propertyEncryptionProvider(propertyEncryptionProvider) .componentIdLookup(ComponentIdLookup.VERSIONED_OR_GENERATE) .mapInstanceIdentifiers(true) .mapControllerServiceReferencesToVersionedId(false) @@ -517,7 +520,7 @@ private void synchronizeFlow(final FlowController controller, final DataFlow exi } } - private FlowComparison compareFlows(final DataFlow existingFlow, final DataFlow proposedFlow, final PropertyEncryptor encryptor) { + private FlowComparison compareFlows(final DataFlow existingFlow, final DataFlow proposedFlow, final PropertyEncryptionProvider propertyEncryptionProvider) { final DifferenceDescriptor differenceDescriptor = new StaticDifferenceDescriptor(); final VersionedDataflow clusterVersionedFlow = proposedFlow.getVersionedDataflow(); @@ -548,7 +551,8 @@ private FlowComparison compareFlows(final DataFlow existingFlow, final DataFlow ); final FlowComparator flowComparator = new StandardFlowComparator(localDataFlow, clusterDataFlow, - differenceDescriptor, encryptor::decrypt, VersionedComponent::getInstanceIdentifier, FlowComparatorVersionedStrategy.DEEP); + differenceDescriptor, new ProviderSensitiveValueDecryptor(propertyEncryptionProvider), + VersionedComponent::getInstanceIdentifier, FlowComparatorVersionedStrategy.DEEP); return flowComparator.compare(); } @@ -602,7 +606,7 @@ private void inheritRegistryClients(final FlowController controller, final Versi if (existing == null) { addFlowRegistryClient(controller, versionedFlowRegistryClient); } else if (affectedComponentSet.isFlowRegistryClientAffected(existing.getIdentifier())) { - final Map decryptedProperties = decryptProperties(versionedFlowRegistryClient.getProperties(), controller.getEncryptor()); + final Map decryptedProperties = decryptProperties(versionedFlowRegistryClient, versionedFlowRegistryClient.getProperties(), controller.getPropertyEncryptionProvider()); updateRegistry(existing, versionedFlowRegistryClient, decryptedProperties); } } @@ -628,7 +632,7 @@ private void addFlowRegistryClient(final FlowController flowController, final Ve final FlowRegistryClientNode flowRegistryClient = flowController.getFlowManager().createFlowRegistryClient( versionedFlowRegistryClient.getType(), versionedFlowRegistryClient.getIdentifier(), coordinate, Collections.emptySet(), false, true, null); - final Map decryptedProperties = decryptProperties(versionedFlowRegistryClient.getProperties(), flowController.getEncryptor()); + final Map decryptedProperties = decryptProperties(versionedFlowRegistryClient, versionedFlowRegistryClient.getProperties(), flowController.getPropertyEncryptionProvider()); updateRegistry(flowRegistryClient, versionedFlowRegistryClient, decryptedProperties); final ControllerServiceFactory serviceFactory = new StandardControllerServiceFactory(flowController.getExtensionManager(), flowController.getFlowManager(), @@ -669,7 +673,7 @@ private void addReportingTask(final FlowController controller, final VersionedRe final ReportingTaskNode taskNode = controller.createReportingTask(reportingTask.getType(), reportingTask.getInstanceIdentifier(), coordinate, false); - final Map decryptedProperties = decryptProperties(reportingTask.getProperties(), controller.getEncryptor()); + final Map decryptedProperties = decryptProperties(reportingTask, reportingTask.getProperties(), controller.getPropertyEncryptionProvider()); configureReportingTask(taskNode, reportingTask, decryptedProperties); final ControllerServiceFactory serviceFactory = new StandardControllerServiceFactory(controller.getExtensionManager(), controller.getFlowManager(), @@ -719,7 +723,7 @@ private void startReportingTask(final ReportingTaskNode taskNode, final Versione } private void updateReportingTask(final ReportingTaskNode taskNode, final VersionedReportingTask reportingTask, final FlowController controller) { - final Map decryptedProperties = decryptProperties(reportingTask.getProperties(), controller.getEncryptor()); + final Map decryptedProperties = decryptProperties(reportingTask, reportingTask.getProperties(), controller.getPropertyEncryptionProvider()); configureReportingTask(taskNode, reportingTask, decryptedProperties); startReportingTask(taskNode, reportingTask, controller); } @@ -763,7 +767,7 @@ private void updateFlowAnalysisRule(final FlowAnalysisRuleNode ruleNode, final V ruleNode.setEnforcementPolicy(flowAnalysisRule.getEnforcementPolicy()); final Set sensitiveDynamicPropertyNames = getSensitiveDynamicPropertyNames(ruleNode, flowAnalysisRule); - final Map decryptedProperties = decryptProperties(flowAnalysisRule.getProperties(), controller.getEncryptor()); + final Map decryptedProperties = decryptProperties(flowAnalysisRule, flowAnalysisRule.getProperties(), controller.getPropertyEncryptionProvider()); ruleNode.setProperties(decryptedProperties, false, sensitiveDynamicPropertyNames); switch (flowAnalysisRule.getScheduledState()) { @@ -793,9 +797,9 @@ private void inheritParameterProviders(final FlowController controller, final Ve final ParameterProviderNode existing = flowManager.getParameterProvider(versionedParameterProvider.getInstanceIdentifier()); if (existing == null) { - addParameterProvider(controller, versionedParameterProvider, controller.getEncryptor()); + addParameterProvider(controller, versionedParameterProvider, controller.getPropertyEncryptionProvider()); } else if (affectedComponentSet.isParameterProviderAffected(existing.getIdentifier())) { - final Map decryptedProperties = decryptProperties(versionedParameterProvider.getProperties(), controller.getEncryptor()); + final Map decryptedProperties = decryptProperties(versionedParameterProvider, versionedParameterProvider.getProperties(), controller.getPropertyEncryptionProvider()); updateParameterProvider(existing, versionedParameterProvider, decryptedProperties); } } @@ -807,13 +811,14 @@ private void inheritParameterProviders(final FlowController controller, final Ve } } - private void addParameterProvider(final FlowController controller, final VersionedParameterProvider parameterProvider, final PropertyEncryptor encryptor) { + private void addParameterProvider(final FlowController controller, final VersionedParameterProvider parameterProvider, + final PropertyEncryptionProvider propertyEncryptionProvider) { final BundleCoordinate coordinate = createBundleCoordinate(extensionManager, parameterProvider.getBundle(), parameterProvider.getType()); final ParameterProviderNode parameterProviderNode = controller.getFlowManager() .createParameterProvider(parameterProvider.getType(), parameterProvider.getInstanceIdentifier(), coordinate, false); - final Map decryptedProperties = decryptProperties(parameterProvider.getProperties(), encryptor); + final Map decryptedProperties = decryptProperties(parameterProvider, parameterProvider.getProperties(), propertyEncryptionProvider); updateParameterProvider(parameterProviderNode, parameterProvider, decryptedProperties); final ControllerServiceFactory serviceFactory = new StandardControllerServiceFactory(controller.getExtensionManager(), controller.getFlowManager(), @@ -865,7 +870,7 @@ private void inheritParameterContexts(final FlowController controller, final Ver parameterContexts.forEach(context -> namedParameterContexts.put(context.getName(), context)); for (final VersionedParameterContext versionedParameterContext : parameterContexts) { - inheritParameterContext(versionedParameterContext, controller.getFlowManager(), namedParameterContexts, controller.getEncryptor(), controller.getAssetManager()); + inheritParameterContext(versionedParameterContext, controller.getFlowManager(), namedParameterContexts, controller.getPropertyEncryptionProvider(), controller.getAssetManager()); } }); } @@ -874,15 +879,15 @@ private void inheritParameterContext( final VersionedParameterContext versionedParameterContext, final FlowManager flowManager, final Map namedParameterContexts, - final PropertyEncryptor encryptor, + final PropertyEncryptionProvider propertyEncryptionProvider, final AssetManager assetManager ) { final ParameterContextManager contextManager = flowManager.getParameterContextManager(); final ParameterContext existingContext = contextManager.getParameterContextNameMapping().get(versionedParameterContext.getName()); if (existingContext == null) { - addParameterContext(versionedParameterContext, flowManager, namedParameterContexts, encryptor, assetManager); + addParameterContext(versionedParameterContext, flowManager, namedParameterContexts, propertyEncryptionProvider, assetManager); } else { - updateParameterContext(versionedParameterContext, existingContext, flowManager, namedParameterContexts, encryptor, assetManager); + updateParameterContext(versionedParameterContext, existingContext, flowManager, namedParameterContexts, propertyEncryptionProvider, assetManager); } } @@ -890,10 +895,10 @@ private void addParameterContext( final VersionedParameterContext versionedParameterContext, final FlowManager flowManager, final Map namedParameterContexts, - final PropertyEncryptor encryptor, + final PropertyEncryptionProvider propertyEncryptionProvider, final AssetManager assetManager ) { - final Map parameters = createParameterMap(flowManager, versionedParameterContext, encryptor, assetManager); + final Map parameters = createParameterMap(flowManager, versionedParameterContext, propertyEncryptionProvider, assetManager); final ParameterContextManager contextManager = flowManager.getParameterContextManager(); final List referenceIds = findReferencedParameterContextIds(versionedParameterContext, contextManager, namedParameterContexts); @@ -941,7 +946,7 @@ private List findReferencedParameterContextIds( private Map createParameterMap( final FlowManager flowManager, final VersionedParameterContext versionedParameterContext, - final PropertyEncryptor encryptor, + final PropertyEncryptionProvider propertyEncryptionProvider, final AssetManager assetManager ) { final Map providedParameters = getProvidedParameters(flowManager, versionedParameterContext); @@ -972,7 +977,8 @@ private Map createParameterMap( parameterValue = providedParameter.getValue(); } } else if (versioned.isSensitive()) { - parameterValue = decrypt(rawValue, encryptor); + final SensitivePropertyContext context = SensitivePropertyContextFactory.forParameter(versionedParameterContext.getName(), name); + parameterValue = decrypt(rawValue, context, propertyEncryptionProvider); } else { parameterValue = rawValue; } @@ -1018,10 +1024,10 @@ private void updateParameterContext( final ParameterContext parameterContext, final FlowManager flowManager, final Map namedParameterContexts, - final PropertyEncryptor encryptor, + final PropertyEncryptionProvider propertyEncryptionProvider, final AssetManager assetManager ) { - final Map parameters = createParameterMap(flowManager, versionedParameterContext, encryptor, assetManager); + final Map parameters = createParameterMap(flowManager, versionedParameterContext, propertyEncryptionProvider, assetManager); final Map currentValues = new HashMap<>(); final Map> currentAssetReferences = new HashMap<>(); @@ -1151,7 +1157,7 @@ private void inheritControllerServices(final FlowController controller, final Ve final ControllerServiceNode serviceNode = flowManager.getRootControllerService(versionedControllerService.getInstanceIdentifier()); if (controllerServicesAddedAndProperties.containsKey(serviceNode) || affectedComponentSet.isControllerServiceAffected(serviceNode.getIdentifier())) { // Set Decrypted Properties for subsequent migrate configuration using actual values - final Map decryptedProperties = decryptProperties(versionedControllerService.getProperties(), controller.getEncryptor()); + final Map decryptedProperties = decryptProperties(versionedControllerService, versionedControllerService.getProperties(), controller.getPropertyEncryptionProvider()); controllerServicesAddedAndProperties.put(serviceNode, decryptedProperties); updateRootControllerService(serviceNode, versionedControllerService, decryptedProperties); } diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/encrypt/PropertyEncryptorFactory.java b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/encrypt/PropertyEncryptorFactory.java deleted file mode 100644 index 8a2e8a70b0cf..000000000000 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/encrypt/PropertyEncryptorFactory.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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.encrypt; - -import org.apache.nifi.util.NiFiProperties; - -import java.util.Objects; - -/** - * Property Encryptor Factory for encapsulating instantiation of Property Encryptors based on various parameters - */ -public class PropertyEncryptorFactory { - private static final String KEY_REQUIRED = String.format("NiFi Sensitive Properties Key [%s] is required", NiFiProperties.SENSITIVE_PROPS_KEY); - - /** - * Get Property Encryptor using NiFi Properties - * - * @param properties NiFi Properties - * @return Property Encryptor - */ - public static PropertyEncryptor getPropertyEncryptor(final NiFiProperties properties) { - Objects.requireNonNull(properties, "NiFi Properties is required"); - final String algorithm = properties.getProperty(NiFiProperties.SENSITIVE_PROPS_ALGORITHM); - String password = properties.getProperty(NiFiProperties.SENSITIVE_PROPS_KEY); - - if (password == null || password.isEmpty()) { - throw new IllegalArgumentException(KEY_REQUIRED); - } - - return new PropertyEncryptorBuilder(password).setAlgorithm(algorithm).build(); - } -} diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/framework/configuration/FlowControllerConfiguration.java b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/framework/configuration/FlowControllerConfiguration.java index a73c625ea92f..4ea40e13b604 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/framework/configuration/FlowControllerConfiguration.java +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/framework/configuration/FlowControllerConfiguration.java @@ -47,14 +47,11 @@ import org.apache.nifi.controller.status.history.VolatileComponentStatusRepository; import org.apache.nifi.diagnostics.DiagnosticsFactory; import org.apache.nifi.diagnostics.bootstrap.BootstrapDiagnosticsFactory; -import org.apache.nifi.encrypt.PropertyEncryptor; -import org.apache.nifi.encrypt.PropertyEncryptorFactory; import org.apache.nifi.extension.manifest.parser.ExtensionManifestParser; import org.apache.nifi.extension.manifest.parser.jaxb.JAXBExtensionManifestParser; import org.apache.nifi.manifest.RuntimeManifestService; import org.apache.nifi.manifest.StandardRuntimeManifestService; import org.apache.nifi.nar.ExtensionDiscoveringManager; -import org.apache.nifi.nar.NarCloseable; import org.apache.nifi.nar.NarComponentManager; import org.apache.nifi.nar.NarLoader; import org.apache.nifi.nar.NarLoaderHolder; @@ -66,9 +63,7 @@ import org.apache.nifi.nar.StandardNarManager; import org.apache.nifi.reporting.BulletinRepository; import org.apache.nifi.security.encryption.PropertyEncryptionProvider; -import org.apache.nifi.security.encryption.PropertyEncryptionProviderInitializationContext; -import org.apache.nifi.security.encryption.SensitivePropertyContext; -import org.apache.nifi.security.encryption.StandardPropertyEncryptionProviderInitializationContext; +import org.apache.nifi.security.encryption.PropertyEncryptionProviderFactory; import org.apache.nifi.services.FlowService; import org.apache.nifi.util.FormatUtils; import org.apache.nifi.util.NiFiProperties; @@ -84,12 +79,9 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import java.io.IOException; import java.time.Duration; -import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; import javax.net.ssl.SSLContext; import javax.net.ssl.X509KeyManager; import javax.net.ssl.X509TrustManager; @@ -221,7 +213,7 @@ public FlowController flowController() throws Exception { authorizer, auditService, componentMetricReporter(), - propertyEncryptor(), + propertyEncryptionProvider(), bulletinRepository, extensionManager, statusHistoryRepository(), @@ -237,7 +229,7 @@ public FlowController flowController() throws Exception { authorizer, auditService, componentMetricReporter(), - propertyEncryptor(), + propertyEncryptionProvider(), nodeProtocolSender, bulletinRepository, clusterCoordinator, @@ -312,16 +304,6 @@ public RuleViolationsManager ruleViolationsManager() { return new StandardRuleViolationsManager(); } - /** - * Property Encryptor configured using Application Properties - * - * @return Property Encryptor - */ - @Bean - public PropertyEncryptor propertyEncryptor() { - return PropertyEncryptorFactory.getPropertyEncryptor(properties); - } - /** * Status History Repository configured from NiFi Application Properties * @@ -558,88 +540,13 @@ public ComponentMetricReporter componentMetricReporter() { } /** - * Property Encryption Provider configured from NiFi Application Properties + * Property Encryption Provider configured from NiFi Application Properties. Installations that do not configure an + * implementation class use the password-based provider, which derives a secret key from the sensitive properties key. * * @return Property Encryption Provider */ @Bean public PropertyEncryptionProvider propertyEncryptionProvider() { - final PropertyEncryptionProvider propertyEncryptionProvider; - - final String configuredClassName = properties.getProperty(NiFiProperties.PROPERTY_ENCRYPTION_PROVIDER_IMPLEMENTATION); - if (configuredClassName == null || configuredClassName.isBlank()) { - // Set null implementation for initial configuration pending wiring to framework components - propertyEncryptionProvider = null; - } else { - try { - final PropertyEncryptionProvider provider = NarThreadContextClassLoader.createInstance( - extensionManager, configuredClassName, PropertyEncryptionProvider.class, properties - ); - propertyEncryptionProvider = initializePropertyEncryptionProvider(provider); - } catch (final Exception e) { - throw new IllegalStateException("Failed to create PropertyEncryptionProvider with class [%s]".formatted(configuredClassName), e); - } - } - - return propertyEncryptionProvider; - } - - private PropertyEncryptionProvider initializePropertyEncryptionProvider(final PropertyEncryptionProvider propertyEncryptionProvider) { - final PropertyEncryptionProvider wrappedPropertyEncryptionProvider = wrapWithComponentNarLoader(propertyEncryptionProvider); - try { - final PropertyEncryptionProviderInitializationContext initializationContext = new StandardPropertyEncryptionProviderInitializationContext( - getPropertyEncryptionProviderProperties(), sslContext, trustManager - ); - wrappedPropertyEncryptionProvider.initialize(initializationContext); - return wrappedPropertyEncryptionProvider; - } catch (final RuntimeException e) { - try { - wrappedPropertyEncryptionProvider.close(); - } catch (final Exception closeException) { - e.addSuppressed(closeException); - } - throw e; - } - } - - private PropertyEncryptionProvider wrapWithComponentNarLoader(final PropertyEncryptionProvider propertyEncryptionProvider) { - final ClassLoader componentClassLoader = propertyEncryptionProvider.getClass().getClassLoader(); - return new PropertyEncryptionProvider() { - @Override - public void initialize(final PropertyEncryptionProviderInitializationContext context) { - try (final NarCloseable ignored = NarCloseable.withComponentNarLoader(componentClassLoader)) { - propertyEncryptionProvider.initialize(context); - } - } - - @Override - public byte[] encrypt(final byte[] property, final SensitivePropertyContext context) { - try (final NarCloseable ignored = NarCloseable.withComponentNarLoader(componentClassLoader)) { - return propertyEncryptionProvider.encrypt(property, context); - } - } - - @Override - public byte[] decrypt(final byte[] encryptedProperty, final SensitivePropertyContext context) { - try (final NarCloseable ignored = NarCloseable.withComponentNarLoader(componentClassLoader)) { - return propertyEncryptionProvider.decrypt(encryptedProperty, context); - } - } - - @Override - public void close() throws IOException { - try (final NarCloseable ignored = NarCloseable.withComponentNarLoader(componentClassLoader)) { - propertyEncryptionProvider.close(); - } - } - }; - } - - private Map getPropertyEncryptionProviderProperties() { - final String prefix = NiFiProperties.PROPERTY_ENCRYPTION_PROVIDER_PREFIX; - return properties.getPropertiesWithPrefix(prefix) - .entrySet() - .stream() - .collect(Collectors.toMap(entry -> entry.getKey().substring(prefix.length()), Map.Entry::getValue)); + return PropertyEncryptionProviderFactory.getPropertyEncryptionProvider(extensionManager, properties, sslContext, trustManager); } } diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/serialization/VersionedFlowSynchronizerTest.java b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/serialization/VersionedFlowSynchronizerTest.java index 221f730de0fc..82a041e97576 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/serialization/VersionedFlowSynchronizerTest.java +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/serialization/VersionedFlowSynchronizerTest.java @@ -34,7 +34,6 @@ import org.apache.nifi.controller.parameter.ParameterProviderLookup; import org.apache.nifi.controller.service.ControllerServiceNode; import org.apache.nifi.controller.service.ControllerServiceProvider; -import org.apache.nifi.encrypt.PropertyEncryptor; import org.apache.nifi.flow.Bundle; import org.apache.nifi.flow.ScheduledState; import org.apache.nifi.flow.VersionedConnector; @@ -58,6 +57,8 @@ import org.apache.nifi.parameter.StandardParameterProviderConfiguration; import org.apache.nifi.persistence.FlowConfigurationArchiveManager; import org.apache.nifi.registry.flow.mapping.VersionedComponentStateLookup; +import org.apache.nifi.security.encryption.PropertyEncryptionEncoder; +import org.apache.nifi.security.encryption.PropertyEncryptionProvider; import org.apache.nifi.services.FlowService; import org.apache.nifi.util.NiFiProperties; import org.junit.jupiter.api.BeforeEach; @@ -109,10 +110,12 @@ class VersionedFlowSynchronizerTest { private static final String SENSITIVE_PROPERTY_NAME = "Protected"; - private static final String ENCRYPTED_PROPERTY_VALUE = "enc{encoded}"; + private static final String ENCRYPTED_PROPERTY_VALUE = PropertyEncryptionEncoder.getEncoded("656e636f646564"); private static final String DECRYPTED_PROPERTY_VALUE = "decoded"; + private static final byte[] DECRYPTED_PROPERTY_BYTES = DECRYPTED_PROPERTY_VALUE.getBytes(StandardCharsets.UTF_8); + private static final String REPORTING_TASK_INSTANCE_ID = "reporting-task-instance-id"; private static final String REPORTING_TASK_TYPE = "org.apache.nifi.reporting.TestReportingTask"; @@ -145,7 +148,7 @@ class VersionedFlowSynchronizerTest { private SnippetManager snippetManager; @Mock - private PropertyEncryptor encryptor; + private PropertyEncryptionProvider propertyEncryptionProvider; @Mock private VersionedComponentStateLookup stateLookup; @@ -197,7 +200,7 @@ void testSyncInheritControllerServicesMigrateConfiguration() { // Mock Property Descriptor for sensitive Property with decrypted value final PropertyDescriptor sensitivePropertyDescriptor = mock(PropertyDescriptor.class); when(controllerServiceNode.getPropertyDescriptor(eq(SENSITIVE_PROPERTY_NAME))).thenReturn(sensitivePropertyDescriptor); - when(encryptor.decrypt(any())).thenReturn(DECRYPTED_PROPERTY_VALUE); + when(propertyEncryptionProvider.decrypt(any(), any())).thenReturn(DECRYPTED_PROPERTY_BYTES); // Return created Controller Service Node as a result of null returned for initial lookup method when(flowManager.createControllerService(any(), any(), any(), any(), eq(true), eq(true), any())).thenReturn(controllerServiceNode); @@ -232,7 +235,7 @@ void testSyncInheritReportingTasksMigrateConfigurationBeforeStart() { // Mock Property Descriptor for sensitive Property with decrypted value final PropertyDescriptor sensitivePropertyDescriptor = mock(PropertyDescriptor.class); when(reportingTaskNode.getPropertyDescriptor(eq(SENSITIVE_PROPERTY_NAME))).thenReturn(sensitivePropertyDescriptor); - when(encryptor.decrypt(any())).thenReturn(DECRYPTED_PROPERTY_VALUE); + when(propertyEncryptionProvider.decrypt(any(), any())).thenReturn(DECRYPTED_PROPERTY_BYTES); // Return created Reporting Task Node when(flowController.createReportingTask(any(), eq(REPORTING_TASK_INSTANCE_ID), any(), eq(false))).thenReturn(reportingTaskNode); @@ -478,7 +481,7 @@ private void setFlowController(final ConnectorRepository connectorRepository) { when(dataFlow.getVersionedDataflow()).thenReturn(versionedDataflow); when(dataFlow.getFlow()).thenReturn("{}".getBytes(StandardCharsets.UTF_8)); when(versionedDataflow.getRootGroup()).thenReturn(versionedRootGroup); - when(flowController.getEncryptor()).thenReturn(encryptor); + when(flowController.getPropertyEncryptionProvider()).thenReturn(propertyEncryptionProvider); when(flowController.createVersionedComponentStateLookup(any())).thenReturn(stateLookup); when(flowController.getControllerServiceProvider()).thenReturn(controllerServiceProvider); diff --git a/nifi-framework-bundle/nifi-framework/nifi-headless-server/pom.xml b/nifi-framework-bundle/nifi-framework/nifi-headless-server/pom.xml index 998a4bffc34b..60937abe5750 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-headless-server/pom.xml +++ b/nifi-framework-bundle/nifi-framework/nifi-headless-server/pom.xml @@ -57,11 +57,6 @@ nifi-framework-core-api 2.12.0-SNAPSHOT - - org.apache.nifi - nifi-property-encryptor - 2.12.0-SNAPSHOT - org.apache.nifi nifi-framework-nar-utils diff --git a/nifi-framework-bundle/nifi-framework/nifi-headless-server/src/main/java/org/apache/nifi/headless/HeadlessNiFiServer.java b/nifi-framework-bundle/nifi-framework/nifi-headless-server/src/main/java/org/apache/nifi/headless/HeadlessNiFiServer.java index 08419a64e0e2..c38afaa0391c 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-headless-server/src/main/java/org/apache/nifi/headless/HeadlessNiFiServer.java +++ b/nifi-framework-bundle/nifi-framework/nifi-headless-server/src/main/java/org/apache/nifi/headless/HeadlessNiFiServer.java @@ -47,8 +47,6 @@ import org.apache.nifi.diagnostics.DiagnosticsFactory; import org.apache.nifi.diagnostics.ThreadDumpTask; import org.apache.nifi.diagnostics.bootstrap.BootstrapDiagnosticsFactory; -import org.apache.nifi.encrypt.PropertyEncryptor; -import org.apache.nifi.encrypt.PropertyEncryptorBuilder; import org.apache.nifi.events.VolatileBulletinRepository; import org.apache.nifi.framework.ssl.FrameworkSslContextProvider; import org.apache.nifi.nar.ExtensionManager; @@ -63,6 +61,8 @@ import org.apache.nifi.nar.StandardNarLoader; import org.apache.nifi.parameter.ParameterLookup; import org.apache.nifi.reporting.BulletinRepository; +import org.apache.nifi.security.encryption.PropertyEncryptionProvider; +import org.apache.nifi.security.encryption.PropertyEncryptionProviderFactory; import org.apache.nifi.services.FlowService; import org.apache.nifi.util.FlowParser; import org.apache.nifi.util.NiFiProperties; @@ -129,15 +129,14 @@ public void preDestruction() throws AuthorizerDestructionException { } }; - final String propertiesKey = props.getProperty(NiFiProperties.SENSITIVE_PROPS_KEY); - final String propertiesAlgorithm = props.getProperty(NiFiProperties.SENSITIVE_PROPS_ALGORITHM); - final PropertyEncryptor encryptor = new PropertyEncryptorBuilder(propertiesKey).setAlgorithm(propertiesAlgorithm).build(); final BulletinRepository bulletinRepository = new VolatileBulletinRepository(); final StatusHistoryRepository statusHistoryRepository = getStatusHistoryRepository(extensionManager); final FrameworkSslContextProvider sslContextProvider = new FrameworkSslContextProvider(props); final SSLContext sslContext = sslContextProvider.loadSslContext().orElse(null); final StateManagerProvider stateManagerProvider = StandardStateManagerProvider.create(props, sslContext, extensionManager, ParameterLookup.EMPTY); + final PropertyEncryptionProvider propertyEncryptionProvider = + PropertyEncryptionProviderFactory.getPropertyEncryptionProvider(extensionManager, props, sslContext, null); flowController = FlowController.createStandaloneInstance( flowFileEventRepository, @@ -146,7 +145,7 @@ public void preDestruction() throws AuthorizerDestructionException { authorizer, auditService, componentMetricReporter, - encryptor, + propertyEncryptionProvider, bulletinRepository, extensionManager, statusHistoryRepository, diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java index 90db89eea4db..b178d39d6769 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java @@ -6233,7 +6233,7 @@ public CopyResponseEntity copyComponents(final String groupId, final CopyRequest final ProcessGroup processGroup = processGroupDAO.getProcessGroup(groupId); final FlowMappingOptions mappingOptions = new FlowMappingOptions.Builder() - .sensitiveValueEncryptor(null) + .propertyEncryptionProvider(null) .stateLookup(VersionedComponentStateLookup.ENABLED_OR_DISABLED) .componentIdLookup(ComponentIdLookup.VERSIONED_OR_GENERATE) .mapPropertyDescriptors(true) @@ -6374,7 +6374,7 @@ public RegisteredFlowSnapshot getCurrentFlowSnapshotByGroupId(final String proce } final FlowMappingOptions mappingOptions = new FlowMappingOptions.Builder() - .sensitiveValueEncryptor(null) + .propertyEncryptionProvider(null) .stateLookup(VersionedComponentStateLookup.ENABLED_OR_DISABLED) .componentIdLookup(ComponentIdLookup.VERSIONED_OR_GENERATE) .mapPropertyDescriptors(true) diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/pom.xml b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/pom.xml index 306edc48fd83..ef23c1e085c6 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/pom.xml +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/pom.xml @@ -97,11 +97,6 @@ nifi-framework-nar-utils 2.12.0-SNAPSHOT - - org.apache.nifi - nifi-property-encryptor - 2.12.0-SNAPSHOT - org.apache.nifi nifi-framework-core-api diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/OidcSecurityConfiguration.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/OidcSecurityConfiguration.java index e62f7c3d7149..a8b763866301 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/OidcSecurityConfiguration.java +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/OidcSecurityConfiguration.java @@ -21,7 +21,7 @@ import org.apache.nifi.authorization.util.IdentityMappingUtil; import org.apache.nifi.components.state.StateManager; import org.apache.nifi.components.state.StateManagerProvider; -import org.apache.nifi.encrypt.PropertyEncryptor; +import org.apache.nifi.security.encryption.PropertyEncryptionProvider; import org.apache.nifi.util.FormatUtils; import org.apache.nifi.util.NiFiProperties; import org.apache.nifi.web.security.StandardAuthenticationEntryPoint; @@ -99,7 +99,7 @@ public class OidcSecurityConfiguration { private final StateManagerProvider stateManagerProvider; - private final PropertyEncryptor propertyEncryptor; + private final PropertyEncryptionProvider propertyEncryptionProvider; private final BearerTokenProvider bearerTokenProvider; @@ -122,7 +122,7 @@ public OidcSecurityConfiguration( final NiFiProperties properties, final TaskScheduler taskScheduler, final StateManagerProvider stateManagerProvider, - final PropertyEncryptor propertyEncryptor, + final PropertyEncryptionProvider propertyEncryptionProvider, final BearerTokenProvider bearerTokenProvider, final BearerTokenResolver bearerTokenResolver, final ClientRegistrationRepository clientRegistrationRepository, @@ -137,7 +137,7 @@ public OidcSecurityConfiguration( this.properties = Objects.requireNonNull(properties, "Properties required"); this.taskScheduler = Objects.requireNonNull(taskScheduler, "Task Scheduler required"); this.stateManagerProvider = Objects.requireNonNull(stateManagerProvider, "State Manager Provider required"); - this.propertyEncryptor = Objects.requireNonNull(propertyEncryptor, "Property Encryptor required"); + this.propertyEncryptionProvider = Objects.requireNonNull(propertyEncryptionProvider, "Property Encryption Provider required"); this.bearerTokenProvider = Objects.requireNonNull(bearerTokenProvider, "Bearer Token Provider required"); this.bearerTokenResolver = Objects.requireNonNull(bearerTokenResolver, "Bearer Token Resolver required"); this.clientRegistrationRepository = Objects.requireNonNull(clientRegistrationRepository, "Registration Repository required"); @@ -328,7 +328,7 @@ public AuthorizedClientExpirationCommand authorizedClientExpirationCommand() { */ @Bean public AuthorizedClientConverter authorizedClientConverter() { - return new StandardAuthorizedClientConverter(propertyEncryptor, clientRegistrationRepository); + return new StandardAuthorizedClientConverter(propertyEncryptionProvider, clientRegistrationRepository); } /** diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/client/web/converter/StandardAuthorizedClientConverter.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/client/web/converter/StandardAuthorizedClientConverter.java index b92e5a1720fe..6169fbff9a35 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/client/web/converter/StandardAuthorizedClientConverter.java +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/oidc/client/web/converter/StandardAuthorizedClientConverter.java @@ -18,7 +18,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import org.apache.nifi.encrypt.PropertyEncryptor; +import org.apache.nifi.security.encryption.PropertyEncryptionProvider; +import org.apache.nifi.security.encryption.SensitivePropertyCodec; +import org.apache.nifi.security.encryption.SensitivePropertyContext; +import org.apache.nifi.security.encryption.SensitivePropertyContextFactory; import org.apache.nifi.web.security.jwt.provider.SupportedClaim; import org.apache.nifi.web.security.oidc.OidcConfigurationException; import org.apache.nifi.web.security.oidc.client.web.OidcAuthorizedClient; @@ -43,15 +46,17 @@ public class StandardAuthorizedClientConverter implements AuthorizedClientConver private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().registerModules(new JavaTimeModule()); - private final PropertyEncryptor propertyEncryptor; + private static final SensitivePropertyContext AUTHORIZATION_TOKEN_CONTEXT = SensitivePropertyContextFactory.forAuthorizationToken(); + + private final PropertyEncryptionProvider propertyEncryptionProvider; private final ClientRegistrationRepository clientRegistrationRepository; public StandardAuthorizedClientConverter( - final PropertyEncryptor propertyEncryptor, + final PropertyEncryptionProvider propertyEncryptionProvider, final ClientRegistrationRepository clientRegistrationRepository ) { - this.propertyEncryptor = Objects.requireNonNull(propertyEncryptor, "Property Encryptor required"); + this.propertyEncryptionProvider = Objects.requireNonNull(propertyEncryptionProvider, "Property Encryption Provider required"); this.clientRegistrationRepository = Objects.requireNonNull(clientRegistrationRepository, "Client Registry Repository required"); } @@ -68,7 +73,7 @@ public String getEncoded(final OidcAuthorizedClient oidcAuthorizedClient) { try { final AuthorizedClient authorizedClient = writeAuthorizedClient(oidcAuthorizedClient); final String serialized = OBJECT_MAPPER.writeValueAsString(authorizedClient); - return propertyEncryptor.encrypt(serialized); + return SensitivePropertyCodec.encrypt(propertyEncryptionProvider, serialized, AUTHORIZATION_TOKEN_CONTEXT); } catch (final Exception e) { throw new OidcConfigurationException("OIDC Authorized Client serialization failed", e); } @@ -85,7 +90,7 @@ public OidcAuthorizedClient getDecoded(final String encoded) { Objects.requireNonNull(encoded, "Encoded representation required"); try { - final String decrypted = propertyEncryptor.decrypt(encoded); + final String decrypted = SensitivePropertyCodec.decrypt(propertyEncryptionProvider, encoded, AUTHORIZATION_TOKEN_CONTEXT); final AuthorizedClient authorizedClient = OBJECT_MAPPER.readValue(decrypted, AuthorizedClient.class); return readAuthorizedClient(authorizedClient); } catch (final Exception e) { diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/oidc/client/web/converter/StandardAuthorizedClientConverterTest.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/oidc/client/web/converter/StandardAuthorizedClientConverterTest.java index d0ae0e6e19a5..d7ecc2f6a8d8 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/oidc/client/web/converter/StandardAuthorizedClientConverterTest.java +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/oidc/client/web/converter/StandardAuthorizedClientConverterTest.java @@ -16,7 +16,9 @@ */ package org.apache.nifi.web.security.oidc.client.web.converter; -import org.apache.nifi.encrypt.PropertyEncryptor; +import org.apache.nifi.security.encryption.PropertyEncryptionProvider; +import org.apache.nifi.security.encryption.PropertyEncryptionProviderInitializationContext; +import org.apache.nifi.security.encryption.SensitivePropertyContext; import org.apache.nifi.web.security.jwt.provider.SupportedClaim; import org.apache.nifi.web.security.logout.LogoutRequest; import org.apache.nifi.web.security.oidc.client.web.OidcAuthorizedClient; @@ -68,7 +70,7 @@ class StandardAuthorizedClientConverterTest { @BeforeEach void setConverter() { - converter = new StandardAuthorizedClientConverter(new StringPropertyEncryptor(), clientRegistrationRepository); + converter = new StandardAuthorizedClientConverter(new PassThroughPropertyEncryptionProvider(), clientRegistrationRepository); } @Test @@ -168,15 +170,19 @@ ClientRegistration getClientRegistration() { .build(); } - private static class StringPropertyEncryptor implements PropertyEncryptor { + private static class PassThroughPropertyEncryptionProvider implements PropertyEncryptionProvider { @Override - public String encrypt(String property) { + public void initialize(final PropertyEncryptionProviderInitializationContext context) { + } + + @Override + public byte[] encrypt(final byte[] property, final SensitivePropertyContext context) { return property; } @Override - public String decrypt(String encryptedProperty) { + public byte[] decrypt(final byte[] encryptedProperty, final SensitivePropertyContext context) { return encryptedProperty; } } diff --git a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/SensitiveValueDecryptor.java b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/SensitiveValueDecryptor.java new file mode 100644 index 000000000000..15251bd21e93 --- /dev/null +++ b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/SensitiveValueDecryptor.java @@ -0,0 +1,38 @@ +/* + * 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.registry.flow.diff; + +import org.apache.nifi.flow.VersionedComponent; + +/** + * Decrypts a sensitive value so that two flows can be compared on the decrypted values. + * + *

The owning component and the name of the value are supplied because an implementation may need to describe the + * location of a value in order to decrypt it.

+ */ +@FunctionalInterface +public interface SensitiveValueDecryptor { + /** + * Decrypt a sensitive value + * + * @param owner Component that owns the value, which is the Parameter Context for a Parameter value + * @param valueName Name of the property or Parameter that holds the value + * @param encryptedValue Encrypted value without the surrounding encryption markers + * @return Decrypted value + */ + String decrypt(VersionedComponent owner, String valueName, String encryptedValue); +} diff --git a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardFlowComparator.java b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardFlowComparator.java index 6ed621456526..021185369532 100644 --- a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardFlowComparator.java +++ b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardFlowComparator.java @@ -39,8 +39,6 @@ import org.apache.nifi.flow.VersionedRemoteGroupPort; import org.apache.nifi.flow.VersionedRemoteProcessGroup; import org.apache.nifi.flow.VersionedReportingTask; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.Collection; import java.util.Collections; @@ -55,8 +53,6 @@ import java.util.stream.Stream; public class StandardFlowComparator implements FlowComparator { - private static final Logger logger = LoggerFactory.getLogger(StandardFlowComparator.class); - private static final String ENCRYPTED_VALUE_PREFIX = "enc{"; private static final String ENCRYPTED_VALUE_SUFFIX = "}"; private static final String FLOW_VERSION = "Flow Version"; @@ -72,13 +68,30 @@ public class StandardFlowComparator implements FlowComparator { private final ComparableDataFlow flowA; private final ComparableDataFlow flowB; private final DifferenceDescriptor differenceDescriptor; - private final Function propertyDecryptor; + private final SensitiveValueDecryptor propertyDecryptor; private final Function idLookup; private final FlowComparatorVersionedStrategy flowComparatorVersionedStrategy; + /** + * Create a comparator with a decryptor that does not require the location of a sensitive value + * + * @param propertyDecryptor Decryptor that receives only the encrypted value + */ public StandardFlowComparator(final ComparableDataFlow flowA, final ComparableDataFlow flowB, final DifferenceDescriptor differenceDescriptor, final Function propertyDecryptor, final Function idLookup, final FlowComparatorVersionedStrategy flowComparatorVersionedStrategy) { + this(flowA, flowB, differenceDescriptor, (owner, valueName, encryptedValue) -> propertyDecryptor.apply(encryptedValue), + idLookup, flowComparatorVersionedStrategy); + } + + /** + * Create a comparator with a decryptor that receives the component and value name that locate each sensitive value + * + * @param propertyDecryptor Decryptor that receives the owning component and the name of the value + */ + public StandardFlowComparator(final ComparableDataFlow flowA, final ComparableDataFlow flowB, + final DifferenceDescriptor differenceDescriptor, final SensitiveValueDecryptor propertyDecryptor, + final Function idLookup, final FlowComparatorVersionedStrategy flowComparatorVersionedStrategy) { this.flowA = flowA; this.flowB = flowB; this.differenceDescriptor = differenceDescriptor; @@ -267,8 +280,8 @@ void compare(final VersionedParameterContext contextA, final VersionedParameterC continue; } - final String decryptedValueA = decryptValue(parameterA); - final String decryptedValueB = decryptValue(parameterB); + final String decryptedValueA = decryptValue(contextA, parameterA); + final String decryptedValueB = decryptValue(contextB, parameterB); if (!Objects.equals(decryptedValueA, decryptedValueB)) { final String valueA = parameterA.isSensitive() ? "" : parameterA.getValue(); final String valueB = parameterB.isSensitive() ? "" : parameterB.getValue(); @@ -354,32 +367,50 @@ private void compare(final VersionedControllerService serviceA, final VersionedC addIfDifferent(differences, DifferenceType.BULLETIN_LEVEL_CHANGED, serviceA, serviceB, VersionedControllerService::getBulletinLevel, true, "WARN"); } - private String decrypt(final String value, final VersionedPropertyDescriptor descriptor) { - if (value == null) { - return null; - } + private String decrypt(final String value, final VersionedComponent component, final String propertyName) { + final String decrypted; - final boolean sensitive = (descriptor == null || descriptor.isSensitive()) && value.startsWith(ENCRYPTED_VALUE_PREFIX) && value.endsWith(ENCRYPTED_VALUE_SUFFIX); - if (!sensitive) { - return value; + if (isEncrypted(value)) { + decrypted = propertyDecryptor.decrypt(component, propertyName, getDecoded(value)); + } else { + decrypted = value; } - return propertyDecryptor.apply(value.substring(ENCRYPTED_VALUE_PREFIX.length(), value.length() - ENCRYPTED_VALUE_SUFFIX.length())); + return decrypted; } - private String decryptValue(final VersionedParameter parameter) { + private String decryptValue(final VersionedParameterContext parameterContext, final VersionedParameter parameter) { + final String decrypted; + final String rawValue = parameter.getValue(); - if (rawValue == null) { - return null; + if (isEncrypted(rawValue)) { + decrypted = propertyDecryptor.decrypt(parameterContext, parameter.getName(), getDecoded(rawValue)); + } else { + decrypted = rawValue; } - final boolean sensitive = parameter.isSensitive() && rawValue.startsWith(ENCRYPTED_VALUE_PREFIX) && rawValue.endsWith(ENCRYPTED_VALUE_SUFFIX); - if (!sensitive) { - logger.debug("Will not decrypt value for parameter {} because it is not encrypted", parameter.getName()); - return rawValue; - } + return decrypted; + } + + /** + * Determine whether a value is wrapped with the prefix and suffix that a serialized flow uses to mark an encrypted + * value. The wrapper distinguishes an encrypted value from a value stored as plaintext, such as a Parameter reference. + * + * @param value Value to be evaluated, which may be null + * @return Whether the value is wrapped with the encrypted value prefix and suffix + */ + private static boolean isEncrypted(final String value) { + return value != null && value.startsWith(ENCRYPTED_VALUE_PREFIX) && value.endsWith(ENCRYPTED_VALUE_SUFFIX); + } - return propertyDecryptor.apply(rawValue.substring(ENCRYPTED_VALUE_PREFIX.length(), rawValue.length() - ENCRYPTED_VALUE_SUFFIX.length())); + /** + * Get an encrypted value with the encrypted value prefix and suffix removed + * + * @param encodedValue Encrypted value wrapped with the prefix and suffix + * @return Encrypted value without the prefix and suffix + */ + private static String getDecoded(final String encodedValue) { + return encodedValue.substring(ENCRYPTED_VALUE_PREFIX.length(), encodedValue.length() - ENCRYPTED_VALUE_SUFFIX.length()); } private void compareProperties(final VersionedComponent componentA, final VersionedComponent componentB, @@ -389,8 +420,8 @@ private void compareProperties(final VersionedComponent componentA, final Versio propertiesA.forEach((key, rawValueA) -> { final String rawValueB = propertiesB.get(key); - final String valueB = decrypt(rawValueB, descriptorsB.get(key)); - final String valueA = decrypt(rawValueA, descriptorsA.get(key)); + final String valueB = decrypt(rawValueB, componentB, key); + final String valueA = decrypt(rawValueA, componentA, key); final VersionedPropertyDescriptor descriptorA = descriptorsA.get(key); final VersionedPropertyDescriptor descriptorB = descriptorsB.get(key); diff --git a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/test/java/org/apache/nifi/registry/flow/diff/TestStandardFlowComparator.java b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/test/java/org/apache/nifi/registry/flow/diff/TestStandardFlowComparator.java index a14a7cb7fe96..a404f3591d6e 100644 --- a/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/test/java/org/apache/nifi/registry/flow/diff/TestStandardFlowComparator.java +++ b/nifi-registry/nifi-registry-core/nifi-registry-flow-diff/src/test/java/org/apache/nifi/registry/flow/diff/TestStandardFlowComparator.java @@ -47,6 +47,10 @@ import static org.junit.jupiter.api.Assertions.assertTrue; public class TestStandardFlowComparator { + private static final String ENCRYPTED_VALUE_PREFIX = "enc{"; + + private static final String ENCRYPTED_VALUE_SUFFIX = "}"; + private Map decryptedToEncrypted; private Map encryptedToDecrypted; private StandardFlowComparator comparator; @@ -184,6 +188,46 @@ public void testMultipleParametersNamesChanged() { assertEquals(4, differences.size()); } + @Test + public void testSensitiveParameterReferenceComparedWithoutDecryption() { + final String propertyName = "Password"; + final String parameterReference = "#{secret.password}"; + + final VersionedProcessGroup groupA = new VersionedProcessGroup(); + groupA.setIdentifier("rootPG"); + groupA.getProcessors().add(createProcessorWithSensitiveProperty("processor", propertyName, parameterReference)); + + final VersionedProcessGroup groupB = new VersionedProcessGroup(); + groupB.setIdentifier("rootPG"); + groupB.getProcessors().add(createProcessorWithSensitiveProperty("processor", propertyName, parameterReference)); + + final SensitiveValueDecryptor failingDecryptor = (owner, valueName, encryptedValue) -> { + throw new IllegalArgumentException("Value is not encrypted: " + encryptedValue); + }; + + final StandardFlowComparator testComparator = new StandardFlowComparator( + new StandardComparableDataFlow("Flow A", groupA), + new StandardComparableDataFlow("Flow B", groupB), + new StaticDifferenceDescriptor(), + failingDecryptor, + VersionedComponent::getIdentifier, + FlowComparatorVersionedStrategy.SHALLOW); + + assertTrue(testComparator.compare().getDifferences().isEmpty()); + } + + private VersionedProcessor createProcessorWithSensitiveProperty(final String identifier, final String propertyName, final String propertyValue) { + final VersionedPropertyDescriptor descriptor = new VersionedPropertyDescriptor(); + descriptor.setName(propertyName); + descriptor.setSensitive(true); + + final VersionedProcessor processor = new VersionedProcessor(); + processor.setIdentifier(identifier); + processor.setProperties(Map.of(propertyName, propertyValue)); + processor.setPropertyDescriptors(Map.of(propertyName, descriptor)); + return processor; + } + @Test public void testDeepStrategyWithChildPGs() { final Function decryptor = encryptedToDecrypted::get; @@ -571,12 +615,23 @@ private VersionedParameter createParameter(final String name, final String value private VersionedParameter createParameter(final String name, final String value, final boolean sensitive, final List referencedAssets) { final VersionedParameter parameter = new VersionedParameter(); parameter.setName(name); - parameter.setValue(sensitive ? "enc{" + decryptedToEncrypted.get(value) + "}" : value); + parameter.setValue(sensitive ? encode(decryptedToEncrypted.get(value)) : value); parameter.setSensitive(sensitive); parameter.setReferencedAssets(referencedAssets); return parameter; } + /** + * Wrap an encrypted value with the prefix and suffix that a serialized flow uses to mark an encrypted value. Only + * values carrying this wrapper are decrypted during comparison. + * + * @param encryptedValue Encrypted value, which may be null + * @return Wrapped encrypted value, or null when no encrypted value is supplied + */ + private String encode(final String encryptedValue) { + return encryptedValue == null ? null : ENCRYPTED_VALUE_PREFIX + encryptedValue + ENCRYPTED_VALUE_SUFFIX; + } + private VersionedAsset createAsset(final String id, final String name) { final VersionedAsset asset = new VersionedAsset(); asset.setIdentifier(id); diff --git a/nifi-stateless/nifi-stateless-bundle/nifi-stateless-engine/pom.xml b/nifi-stateless/nifi-stateless-bundle/nifi-stateless-engine/pom.xml index 7937e9849ef4..17c6da5d5b75 100644 --- a/nifi-stateless/nifi-stateless-bundle/nifi-stateless-engine/pom.xml +++ b/nifi-stateless/nifi-stateless-bundle/nifi-stateless-engine/pom.xml @@ -43,11 +43,6 @@ nifi-expression-language 2.12.0-SNAPSHOT
- - org.apache.nifi - nifi-property-encryptor - 2.12.0-SNAPSHOT - org.apache.commons commons-lang3 diff --git a/nifi-stateless/nifi-stateless-bundle/nifi-stateless-engine/src/main/java/org/apache/nifi/stateless/engine/StandardStatelessEngine.java b/nifi-stateless/nifi-stateless-bundle/nifi-stateless-engine/src/main/java/org/apache/nifi/stateless/engine/StandardStatelessEngine.java index 29b94d45bf25..d1ee3d289954 100644 --- a/nifi-stateless/nifi-stateless-bundle/nifi-stateless-engine/src/main/java/org/apache/nifi/stateless/engine/StandardStatelessEngine.java +++ b/nifi-stateless/nifi-stateless-bundle/nifi-stateless-engine/src/main/java/org/apache/nifi/stateless/engine/StandardStatelessEngine.java @@ -45,7 +45,6 @@ import org.apache.nifi.controller.repository.metrics.tracking.StatsTracker; import org.apache.nifi.controller.scheduling.LifecycleStateManager; import org.apache.nifi.controller.service.ControllerServiceProvider; -import org.apache.nifi.encrypt.PropertyEncryptor; import org.apache.nifi.engine.FlowEngine; import org.apache.nifi.extensions.ExtensionRepository; import org.apache.nifi.flow.VersionedExternalFlowMetadata; @@ -111,7 +110,6 @@ public class StandardStatelessEngine implements StatelessEngine { private final ExtensionManager extensionManager; private final BulletinRepository bulletinRepository; private final StatelessStateManagerProvider stateManagerProvider; - private final PropertyEncryptor propertyEncryptor; private final ProcessScheduler processScheduler; private final AssetManager assetManager; private final KerberosConfig kerberosConfig; @@ -139,7 +137,6 @@ private StandardStatelessEngine(final Builder builder) { this.extensionManager = requireNonNull(builder.extensionManager, "Extension Manager must be provided"); this.bulletinRepository = requireNonNull(builder.bulletinRepository, "Bulletin Repository must be provided"); this.stateManagerProvider = requireNonNull(builder.stateManagerProvider, "State Manager Provider must be provided"); - this.propertyEncryptor = requireNonNull(builder.propertyEncryptor, "Encryptor must be provided"); this.processScheduler = requireNonNull(builder.processScheduler, "Process Scheduler must be provided"); this.kerberosConfig = requireNonNull(builder.kerberosConfig, "Kerberos Configuration must be provided"); this.flowFileEventRepository = requireNonNull(builder.flowFileEventRepository, "FlowFile Event Repository must be provided"); @@ -614,11 +611,6 @@ public StateManagerProvider getStateManagerProvider() { return stateManagerProvider; } - @Override - public PropertyEncryptor getPropertyEncryptor() { - return propertyEncryptor; - } - @Override public ProcessScheduler getProcessScheduler() { return processScheduler; @@ -683,7 +675,6 @@ public static class Builder { private ExtensionManager extensionManager = null; private BulletinRepository bulletinRepository = null; private StatelessStateManagerProvider stateManagerProvider = null; - private PropertyEncryptor propertyEncryptor = null; private ProcessScheduler processScheduler = null; private KerberosConfig kerberosConfig = null; private FlowFileEventRepository flowFileEventRepository = null; @@ -710,10 +701,6 @@ public Builder stateManagerProvider(final StatelessStateManagerProvider stateMan return this; } - public Builder encryptor(final PropertyEncryptor propertyEncryptor) { - this.propertyEncryptor = propertyEncryptor; - return this; - } public Builder processScheduler(final ProcessScheduler processScheduler) { this.processScheduler = processScheduler; diff --git a/nifi-stateless/nifi-stateless-bundle/nifi-stateless-engine/src/main/java/org/apache/nifi/stateless/engine/StatelessEngine.java b/nifi-stateless/nifi-stateless-bundle/nifi-stateless-engine/src/main/java/org/apache/nifi/stateless/engine/StatelessEngine.java index 16ba936c4673..5d518ace34c5 100644 --- a/nifi-stateless/nifi-stateless-bundle/nifi-stateless-engine/src/main/java/org/apache/nifi/stateless/engine/StatelessEngine.java +++ b/nifi-stateless/nifi-stateless-bundle/nifi-stateless-engine/src/main/java/org/apache/nifi/stateless/engine/StatelessEngine.java @@ -28,7 +28,6 @@ import org.apache.nifi.controller.repository.CounterRepository; import org.apache.nifi.controller.repository.FlowFileEventRepository; import org.apache.nifi.controller.service.ControllerServiceProvider; -import org.apache.nifi.encrypt.PropertyEncryptor; import org.apache.nifi.nar.ExtensionManager; import org.apache.nifi.provenance.ProvenanceRepository; import org.apache.nifi.registry.EnvironmentVariables; @@ -50,7 +49,6 @@ public interface StatelessEngine { StateManagerProvider getStateManagerProvider(); - PropertyEncryptor getPropertyEncryptor(); FlowManager getFlowManager(); diff --git a/nifi-stateless/nifi-stateless-bundle/nifi-stateless-engine/src/main/java/org/apache/nifi/stateless/engine/StatelessFlowManager.java b/nifi-stateless/nifi-stateless-bundle/nifi-stateless-engine/src/main/java/org/apache/nifi/stateless/engine/StatelessFlowManager.java index cc5c78688e01..bc2a2b242337 100644 --- a/nifi-stateless/nifi-stateless-bundle/nifi-stateless-engine/src/main/java/org/apache/nifi/stateless/engine/StatelessFlowManager.java +++ b/nifi-stateless/nifi-stateless-bundle/nifi-stateless-engine/src/main/java/org/apache/nifi/stateless/engine/StatelessFlowManager.java @@ -242,7 +242,9 @@ public Port createLocalOutputPort(final String id, final String name) { public ProcessGroup createProcessGroup(final String id, final String connectorId) { final ProcessGroup created = new StandardProcessGroup(id, statelessEngine.getControllerServiceProvider(), statelessEngine.getProcessScheduler(), - statelessEngine.getPropertyEncryptor(), + // The Stateless runtime neither restores a persisted flow nor places a group under version control, so no + // sensitive value passes through the group and no Property Encryption Provider is required + null, statelessEngine.getExtensionManager(), statelessEngine.getStateManagerProvider(), this, diff --git a/nifi-stateless/nifi-stateless-bundle/nifi-stateless-engine/src/main/java/org/apache/nifi/stateless/flow/StandardStatelessDataflowFactory.java b/nifi-stateless/nifi-stateless-bundle/nifi-stateless-engine/src/main/java/org/apache/nifi/stateless/flow/StandardStatelessDataflowFactory.java index 2aac3ced7131..44f7b16b1f3b 100644 --- a/nifi-stateless/nifi-stateless-bundle/nifi-stateless-engine/src/main/java/org/apache/nifi/stateless/flow/StandardStatelessDataflowFactory.java +++ b/nifi-stateless/nifi-stateless-bundle/nifi-stateless-engine/src/main/java/org/apache/nifi/stateless/flow/StandardStatelessDataflowFactory.java @@ -41,9 +41,6 @@ import org.apache.nifi.controller.scheduling.StatelessProcessSchedulerInitializationContext; import org.apache.nifi.controller.service.ControllerServiceProvider; import org.apache.nifi.controller.service.StandardControllerServiceProvider; -import org.apache.nifi.encrypt.PropertyEncryptionMethod; -import org.apache.nifi.encrypt.PropertyEncryptor; -import org.apache.nifi.encrypt.PropertyEncryptorBuilder; import org.apache.nifi.engine.FlowEngine; import org.apache.nifi.events.BulletinFactory; import org.apache.nifi.events.EventReporter; @@ -183,31 +180,6 @@ public NodeTypeProvider getNodeTypeProvider() { final ExtensionRepository extensionRepository = new FileSystemExtensionRepository(extensionManager, engineConfiguration, narClassLoaders, extensionClients); extensionRepository.initialize(); - final PropertyEncryptor lazyInitializedEncryptor = new PropertyEncryptor() { - private PropertyEncryptor created = null; - - @Override - public String encrypt(final String property) { - return getEncryptor().encrypt(property); - } - - @Override - public String decrypt(final String encryptedProperty) { - return getEncryptor().decrypt(encryptedProperty); - } - - private synchronized PropertyEncryptor getEncryptor() { - if (created != null) { - return created; - } - - created = new PropertyEncryptorBuilder(engineConfiguration.getSensitivePropsKey()) - .setAlgorithm(PropertyEncryptionMethod.NIFI_PBKDF2_AES_GCM_256.toString()) - .build(); - return created; - } - }; - final CounterRepository counterRepo = new StandardCounterRepository(); final File krb5File = engineConfiguration.getKrb5File(); @@ -219,7 +191,6 @@ private synchronized PropertyEncryptor getEncryptor() { final StatelessEngine statelessEngine = new StandardStatelessEngine.Builder() .bulletinRepository(bulletinRepository) - .encryptor(lazyInitializedEncryptor) .extensionManager(extensionManager) .assetManager(assetManager) .stateManagerProvider(stateManagerProvider) diff --git a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/encryption/PropertyEncryptionProviderIT.java b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/encryption/PropertyEncryptionProviderIT.java new file mode 100644 index 000000000000..103b1c992199 --- /dev/null +++ b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/encryption/PropertyEncryptionProviderIT.java @@ -0,0 +1,158 @@ +/* + * 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.tests.system.encryption; + +import org.apache.nifi.tests.system.NiFiSystemIT; +import org.apache.nifi.toolkit.client.NiFiClientException; +import org.apache.nifi.util.NiFiProperties; +import org.apache.nifi.web.api.entity.ConnectionEntity; +import org.apache.nifi.web.api.entity.ParameterContextEntity; +import org.apache.nifi.web.api.entity.ProcessorEntity; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Map; +import java.util.zip.GZIPInputStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +/** + * Verifies that sensitive component properties and sensitive Parameters protected by a Property Encryption Provider are + * written to the flow configuration in encrypted form and recovered after a restart. + * + *

The Provider is configured explicitly rather than relying on the default, so that the configured implementation is + * loaded from its NAR and used for both flow serialization and flow synchronization.

+ */ +class PropertyEncryptionProviderIT extends NiFiSystemIT { + private static final String PROVIDER_IMPLEMENTATION = "org.apache.nifi.security.encryption.password.PasswordBasedPropertyEncryptionProvider"; + + private static final String SENSITIVE_PROPERTY_VALUE = "Sensitive Property Value 4c1cf47b"; + + private static final String SENSITIVE_PARAMETER_VALUE = "Sensitive Parameter Value 9a2be830"; + + private static final String PARAMETER_CONTEXT_NAME = "Property Encryption Provider Context"; + + private static final String PARAMETER_NAME = "sensitiveParameter"; + + private static final String PARAMETER_REFERENCE = "#{%s}".formatted(PARAMETER_NAME); + + private static final String SENSITIVE_CONTENT_PROPERTY = "Sensitive Content"; + + private static final String UPDATE_STRATEGY_PROPERTY = "Update Strategy"; + + private static final String REPLACE_STRATEGY = "Replace"; + + private static final String SUCCESS_RELATIONSHIP = "success"; + + private static final String FLOW_CONFIGURATION_FILENAME = "conf/flow.json.gz"; + + @Override + protected Map getNifiPropertiesOverrides() { + return Map.of(NiFiProperties.PROPERTY_ENCRYPTION_PROVIDER_IMPLEMENTATION, PROVIDER_IMPLEMENTATION); + } + + @Override + protected boolean isAllowFactoryReuse() { + return false; + } + + @Override + protected boolean isDestroyEnvironmentAfterEachTest() { + return true; + } + + @Test + void testSensitivePropertyAndParameterRecoveredAfterRestart() throws NiFiClientException, IOException, InterruptedException { + final ParameterContextEntity parameterContext = getClientUtil().createParameterContext(PARAMETER_CONTEXT_NAME, PARAMETER_NAME, SENSITIVE_PARAMETER_VALUE, true); + getClientUtil().setParameterContext("root", parameterContext); + + final ProcessorEntity createdGenerate = getClientUtil().createProcessor("GenerateFlowFile"); + final ProcessorEntity generate = getClientUtil().updateProcessorProperties(createdGenerate, Map.of("Max FlowFiles", "1")); + + final ProcessorEntity propertyUpdateContent = createUpdateContent(SENSITIVE_PROPERTY_VALUE); + final ProcessorEntity parameterUpdateContent = createUpdateContent(PARAMETER_REFERENCE); + + final ConnectionEntity propertyConnection = createSensitiveContentFlow(generate, propertyUpdateContent); + final ConnectionEntity parameterConnection = createSensitiveContentFlow(generate, parameterUpdateContent); + + restart(); + + final String flowConfiguration = readFlowConfiguration(); + assertFalse(flowConfiguration.contains(SENSITIVE_PROPERTY_VALUE), "Sensitive property value written to flow configuration without encryption"); + assertFalse(flowConfiguration.contains(SENSITIVE_PARAMETER_VALUE), "Sensitive Parameter value written to flow configuration without encryption"); + + startProcessor(propertyUpdateContent.getId()); + startProcessor(parameterUpdateContent.getId()); + startProcessor(generate.getId()); + + waitForQueueCount(propertyConnection.getId(), getNumberOfNodes()); + waitForQueueCount(parameterConnection.getId(), getNumberOfNodes()); + + assertEquals(SENSITIVE_PROPERTY_VALUE, getClientUtil().getFlowFileContentAsUtf8(propertyConnection.getId(), 0)); + assertEquals(SENSITIVE_PARAMETER_VALUE, getClientUtil().getFlowFileContentAsUtf8(parameterConnection.getId(), 0)); + } + + private ProcessorEntity createUpdateContent(final String sensitiveContent) throws NiFiClientException, IOException { + final ProcessorEntity updateContent = getClientUtil().createProcessor("UpdateContent"); + return getClientUtil().updateProcessorProperties(updateContent, Map.of(SENSITIVE_CONTENT_PROPERTY, sensitiveContent, UPDATE_STRATEGY_PROPERTY, REPLACE_STRATEGY)); + } + + /** + * Connect the source Processor to the UpdateContent Processor and connect UpdateContent to a TerminateFlowFile + * Processor that is left stopped, so that the FlowFile written by UpdateContent stays queued for inspection + * + * @param generate Source Processor + * @param updateContent Processor that writes the sensitive value to the FlowFile + * @return Connection holding the FlowFiles written by UpdateContent + */ + private ConnectionEntity createSensitiveContentFlow(final ProcessorEntity generate, final ProcessorEntity updateContent) throws NiFiClientException, IOException { + final ProcessorEntity terminate = getClientUtil().createProcessor("TerminateFlowFile"); + + getClientUtil().createConnection(generate, updateContent, SUCCESS_RELATIONSHIP); + return getClientUtil().createConnection(updateContent, terminate, SUCCESS_RELATIONSHIP); + } + + private void restart() throws IOException { + getNiFiInstance().stop(); + getNiFiInstance().start(true); + setupClient(); + } + + private void startProcessor(final String processorId) throws NiFiClientException, IOException, InterruptedException { + getClientUtil().waitForValidProcessor(processorId); + getClientUtil().startProcessor(getNifiClient().getProcessorClient().getProcessor(processorId)); + } + + private String readFlowConfiguration() throws IOException { + final File flowConfiguration = new File(getNiFiInstance().getInstanceDirectory(), FLOW_CONFIGURATION_FILENAME); + + try ( + InputStream inputStream = Files.newInputStream(flowConfiguration.toPath()); + InputStream compressed = new GZIPInputStream(inputStream); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream() + ) { + compressed.transferTo(outputStream); + return outputStream.toString(StandardCharsets.UTF_8); + } + } +}