This is an automated email from the ASF dual-hosted git repository.
pvillard31 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new b6a04999e8a NIFI-16319 Fixed reloading of symlinked Security Stores
(#11653)
b6a04999e8a is described below
commit b6a04999e8aea33a44dde95299cc04456567b66c
Author: David Handermann <[email protected]>
AuthorDate: Wed Sep 9 06:36:45 2026 -0500
NIFI-16319 Fixed reloading of symlinked Security Stores (#11653)
- Replaced WatchService approach with file digest comparison
---
.../src/main/asciidoc/administration-guide.adoc | 10 +-
.../configuration/SslContextConfiguration.java | 123 ++--------------
.../ssl/SecurityStoreChangedPathListener.java | 89 ------------
.../framework/ssl/SecurityStoreMonitorCommand.java | 133 +++++++++++++++++
.../framework/ssl/WatchServiceMonitorCommand.java | 107 --------------
.../ssl/SecurityStoreMonitorCommandTest.java | 159 +++++++++++++++++++++
.../ssl/WatchServiceMonitorCommandTest.java | 80 -----------
7 files changed, 312 insertions(+), 389 deletions(-)
diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc
b/nifi-docs/src/main/asciidoc/administration-guide.adoc
index 5836a0d8b94..a0bf4f587e4 100644
--- a/nifi-docs/src/main/asciidoc/administration-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc
@@ -342,11 +342,11 @@ Automatic refreshing of NiFi's web SSL context factory
can be enabled using the
[options="header,footer"]
|==================================================================================================================================================
| Property Name | Description
-|`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`.
+|`nifi.security.autoreload.enabled`|Specifies whether the SSL context factory
should be automatically reloaded if updates to the keystore and truststore are
detected. NiFi polls the configured store paths and compares content digests,
resolving symbolic links at read time so Kubernetes Secret and projected-volume
mounts are detected. By default, it is set to `false`.
+|`nifi.security.autoreload.interval`|Specifies the interval at which the
configured keystore and truststore paths are digested and compared for updates.
Only applies if `nifi.security.autoreload.enabled` is set to `true`. The
default value is `10 secs`.
|==================================================================================================================================================
-Once the `nifi.security.autoreload.enabled` property is set to `true`, any
valid changes to the configured keystore and truststore will cause NiFi's SSL
context factory to be reloaded, allowing clients to pick up the changes. This
is intended to allow expired certificates to be updated in the keystore and new
trusted certificates to be added in the truststore, all without having to
restart the NiFi server.
+Once the `nifi.security.autoreload.enabled` property is set to `true`, NiFi
polls the configured keystore and truststore paths at the configured interval
and reloads the SSL context factory when a content digest changes. Symbolic
links are resolved when the files are read, so updates delivered through
Kubernetes Secret or projected-volume mounts are detected. This is intended to
allow expired certificates to be updated in the keystore and new trusted
certificates to be added in the trust [...]
NOTE: Changes to any of the `nifi.security.keystore*` or
`nifi.security.truststore*` properties will not be picked up by the
auto-refreshing logic, which assumes the passwords and store paths will remain
the same.
@@ -3624,8 +3624,8 @@ These properties pertain to various security features in
NiFi. Many of these pro
|`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 <<property-encryption-providers>> 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`.
+|`nifi.security.autoreload.enabled`|Specifies whether the SSL context factory
should be automatically reloaded if updates to the keystore and truststore are
detected. NiFi polls the configured store paths and compares content digests,
resolving symbolic links at read time so Kubernetes Secret and projected-volume
mounts are detected. By default, it is set to `false`.
+|`nifi.security.autoreload.interval`|Specifies the interval at which the
configured keystore and truststore paths are digested and compared for updates.
Only applies if `nifi.security.autoreload.enabled` is set to `true`. The
default value is `10 secs`.
|`nifi.security.keystore`*|The full path and name of the keystore. The default
value is `./conf/keystore.p12`.
|`nifi.security.keystoreType`|The keystore type. The default value is `PKCS12`.
|`nifi.security.keystorePasswd`|The keystore password. It is blank by default.
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/framework/configuration/SslContextConfiguration.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/framework/configuration/SslContextConfiguration.java
index d7599085e0e..e3b739c94cc 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/framework/configuration/SslContextConfiguration.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/framework/configuration/SslContextConfiguration.java
@@ -17,8 +17,7 @@
package org.apache.nifi.framework.configuration;
import org.apache.nifi.framework.ssl.FrameworkSslContextHolder;
-import org.apache.nifi.framework.ssl.SecurityStoreChangedPathListener;
-import org.apache.nifi.framework.ssl.WatchServiceMonitorCommand;
+import org.apache.nifi.framework.ssl.SecurityStoreMonitorCommand;
import org.apache.nifi.security.ssl.KeyManagerListener;
import org.apache.nifi.security.ssl.PemCertificateKeyStoreBuilder;
import org.apache.nifi.security.ssl.PemPrivateKeyCertificateKeyStoreBuilder;
@@ -36,17 +35,12 @@ import org.springframework.scheduling.TaskScheduler;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.io.UncheckedIOException;
-import java.nio.file.FileSystem;
-import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
-import java.nio.file.StandardWatchEventKinds;
-import java.nio.file.WatchService;
import java.security.KeyStore;
import java.time.Duration;
-import java.util.HashSet;
+import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;
@@ -107,10 +101,11 @@ public class SslContextConfiguration {
}
@Bean
- public WatchServiceMonitorCommand watchServiceMonitorCommand() {
- final WatchServiceMonitorCommand command;
+ public SecurityStoreMonitorCommand securityStoreMonitorCommand() {
+ final SecurityStoreMonitorCommand command;
if (isReloadEnabled()) {
+ final Set<Path> storePaths = getStorePaths();
final String reloadIntervalProperty =
properties.getProperty(SECURITY_AUTO_RELOAD_INTERVAL,
NiFiProperties.DEFAULT_SECURITY_AUTO_RELOAD_INTERVAL);
final long reloadIntervalSeconds =
Math.round(FormatUtils.getPreciseTimeDuration(reloadIntervalProperty,
TimeUnit.SECONDS));
final Duration reloadDuration =
Duration.ofSeconds(reloadIntervalSeconds);
@@ -118,18 +113,14 @@ public class SslContextConfiguration {
final X509ExtendedKeyManager keyManager = keyManager();
final X509ExtendedTrustManager trustManager = trustManager();
if (keyManager instanceof final KeyManagerListener
keyManagerListener && trustManager instanceof final TrustManagerListener
trustManagerListener) {
- final Set<Path> storeFileNames = getStoreFileNames();
- final SecurityStoreChangedPathListener changedPathListener =
new SecurityStoreChangedPathListener(
- storeFileNames,
+ command = new SecurityStoreMonitorCommand(
+ storePaths,
keyManagerListener,
FrameworkSslContextHolder.getKeyManagerBuilder(),
trustManagerListener,
FrameworkSslContextHolder.getTrustManagerBuilder()
);
- final WatchService watchService = storeWatchService();
- command = new WatchServiceMonitorCommand(watchService,
changedPathListener);
-
taskScheduler.scheduleAtFixedRate(command, reloadDuration);
logger.info("Scheduled Security Store Monitor with Duration
[{}]", reloadDuration);
} else {
@@ -141,33 +132,6 @@ public class SslContextConfiguration {
return command;
}
- @Bean
- public WatchService storeWatchService() {
- final WatchService watchService;
-
- final String keyStoreProperty =
properties.getProperty(SECURITY_KEYSTORE);
- final String keyStorePrivateKeyProperty =
properties.getProperty(SECURITY_KEYSTORE_PRIVATE_KEY);
- if ((keyStoreProperty == null || keyStoreProperty.isBlank()) &&
(keyStorePrivateKeyProperty == null || keyStorePrivateKeyProperty.isBlank())) {
- watchService = null;
- } else if (isReloadEnabled()) {
- final Set<Path> storeDirectories = getStoreDirectories();
- final FileSystem fileSystem = FileSystems.getDefault();
- try {
- watchService = fileSystem.newWatchService();
-
- for (final Path storeDirectory : storeDirectories) {
- storeDirectory.register(watchService,
StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY);
- }
- } catch (final IOException e) {
- throw new UncheckedIOException("Store Watch Service creation
failed", e);
- }
- } else {
- watchService = null;
- }
-
- return watchService;
- }
-
@Bean
public KeyStore keyStore() {
final KeyStore keyStore;
@@ -240,80 +204,23 @@ public class SslContextConfiguration {
return trustStore;
}
- private Set<Path> getStoreFileNames() {
- final Set<Path> storeFileNames = new HashSet<>();
-
- if (isPemStoreType(SECURITY_KEYSTORE_TYPE)) {
- final Path keyStorePrivateKeyPath =
getPropertyPath(SECURITY_KEYSTORE_PRIVATE_KEY);
- addStoreFileName(keyStorePrivateKeyPath, storeFileNames);
- final Path keyStoreCertificatePath =
getPropertyPath(SECURITY_KEYSTORE_CERTIFICATE);
- addStoreFileName(keyStoreCertificatePath, storeFileNames);
- } else {
- final Path keyStorePath = getPropertyPath(SECURITY_KEYSTORE);
- addStoreFileName(keyStorePath, storeFileNames);
- }
-
- if (isPemStoreType(SECURITY_TRUSTSTORE_TYPE)) {
- final Path trustStoreCertificatePath =
getPropertyPath(SECURITY_TRUSTSTORE_CERTIFICATE);
- addStoreFileName(trustStoreCertificatePath, storeFileNames);
- } else {
- final Path trustStorePath = getPropertyPath(SECURITY_TRUSTSTORE);
- addStoreFileName(trustStorePath, storeFileNames);
- }
-
- return storeFileNames;
- }
-
- private void addStoreFileName(final Path storePath, final Set<Path>
storeFileNames) {
- storeFileNames.add(storePath.getFileName());
-
- if (Files.isSymbolicLink(storePath)) {
- try {
- final Path realStorePath = storePath.toRealPath();
- storeFileNames.add(realStorePath.getFileName());
- } catch (final IOException e) {
- throw new UncheckedIOException("Failed to resolve Store Path
Link [%s]".formatted(storePath), e);
- }
- }
- }
-
- private Set<Path> getStoreDirectories() {
- final Set<Path> storeDirectories = new HashSet<>();
+ private Set<Path> getStorePaths() {
+ final Set<Path> storePaths = new LinkedHashSet<>();
if (isPemStoreType(SECURITY_KEYSTORE_TYPE)) {
- final Path keyStorePrivateKeyPath =
getPropertyPath(SECURITY_KEYSTORE_PRIVATE_KEY);
- addStorePath(keyStorePrivateKeyPath, storeDirectories);
- final Path keyStoreCertificatePath =
getPropertyPath(SECURITY_KEYSTORE_CERTIFICATE);
- addStorePath(keyStoreCertificatePath, storeDirectories);
+ storePaths.add(getPropertyPath(SECURITY_KEYSTORE_PRIVATE_KEY));
+ storePaths.add(getPropertyPath(SECURITY_KEYSTORE_CERTIFICATE));
} else {
- final Path keyStorePath = getPropertyPath(SECURITY_KEYSTORE);
- addStorePath(keyStorePath, storeDirectories);
+ storePaths.add(getPropertyPath(SECURITY_KEYSTORE));
}
if (isPemStoreType(SECURITY_TRUSTSTORE_TYPE)) {
- final Path trustStoreCertificatePath =
getPropertyPath(SECURITY_TRUSTSTORE_CERTIFICATE);
- addStorePath(trustStoreCertificatePath, storeDirectories);
+ storePaths.add(getPropertyPath(SECURITY_TRUSTSTORE_CERTIFICATE));
} else {
- final Path trustStorePath = getPropertyPath(SECURITY_TRUSTSTORE);
- addStorePath(trustStorePath, storeDirectories);
+ storePaths.add(getPropertyPath(SECURITY_TRUSTSTORE));
}
- return storeDirectories;
- }
-
- private void addStorePath(final Path storePath, final Set<Path>
storeDirectories) {
- final Path storeDirectory = storePath.getParent();
- storeDirectories.add(storeDirectory);
-
- if (Files.isSymbolicLink(storePath)) {
- try {
- final Path realStorePath = storePath.toRealPath();
- final Path realStoreDirectory = realStorePath.getParent();
- storeDirectories.add(realStoreDirectory);
- } catch (final IOException e) {
- throw new UncheckedIOException("Failed to resolve Store Path
Link [%s]".formatted(storePath), e);
- }
- }
+ return storePaths;
}
private Path getPropertyPath(final String propertyName) {
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/framework/ssl/SecurityStoreChangedPathListener.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/framework/ssl/SecurityStoreChangedPathListener.java
deleted file mode 100644
index e7defc8e910..00000000000
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/framework/ssl/SecurityStoreChangedPathListener.java
+++ /dev/null
@@ -1,89 +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.framework.ssl;
-
-import org.apache.nifi.security.ssl.KeyManagerBuilder;
-import org.apache.nifi.security.ssl.KeyManagerListener;
-import org.apache.nifi.security.ssl.TrustManagerBuilder;
-import org.apache.nifi.security.ssl.TrustManagerListener;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.nio.file.Path;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import javax.net.ssl.X509ExtendedKeyManager;
-import javax.net.ssl.X509ExtendedTrustManager;
-
-/**
- * Changed Path Listener loads new Key Manager and Trust Manager
implementations from Path resources
- */
-public class SecurityStoreChangedPathListener implements
WatchServiceMonitorCommand.ChangedPathListener {
- private static final Logger logger =
LoggerFactory.getLogger(SecurityStoreChangedPathListener.class);
-
- private final Set<Path> storeFileNames;
-
- private final KeyManagerListener keyManagerListener;
-
- private final KeyManagerBuilder keyManagerBuilder;
-
- private final TrustManagerListener trustManagerListener;
-
- private final TrustManagerBuilder trustManagerBuilder;
-
- /**
- * Security Store Changed Path Listener reloads Key Manager and Trust
Manager when changed paths contain files match store file names
- *
- * @param storeFileNames Key Store and Trust Store File Names that must be
matched
- * @param keyManagerListener Key Manager Listener for handling updated Key
Manager
- * @param keyManagerBuilder Key Manager Builder for creating new Key
Manager instances
- * @param trustManagerListener Trust Manager Listener for handling updated
Trust Manager
- * @param trustManagerBuilder Trust Manager Builder for creating new Trust
Manager instances
- */
- public SecurityStoreChangedPathListener(
- final Set<Path> storeFileNames,
- final KeyManagerListener keyManagerListener,
- final KeyManagerBuilder keyManagerBuilder,
- final TrustManagerListener trustManagerListener,
- final TrustManagerBuilder trustManagerBuilder
- ) {
- this.storeFileNames = Objects.requireNonNull(storeFileNames, "Store
File Names required");
- this.keyManagerListener = Objects.requireNonNull(keyManagerListener,
"Key Manager Listener required");
- this.keyManagerBuilder = Objects.requireNonNull(keyManagerBuilder,
"Key Manager Builder required");
- this.trustManagerListener =
Objects.requireNonNull(trustManagerListener, "Trust Manager Listener required");
- this.trustManagerBuilder = Objects.requireNonNull(trustManagerBuilder,
"Trust Manager Builder required");
- }
-
- @Override
- public void onChanged(final List<Path> changedPaths) {
- final Optional<Path> storeFileNameFound = changedPaths.stream()
- .map(Path::getFileName)
- .filter(storeFileNames::contains)
- .findFirst();
-
- if (storeFileNameFound.isPresent()) {
- final X509ExtendedKeyManager keyManager =
keyManagerBuilder.build();
- final X509ExtendedTrustManager trustManager =
trustManagerBuilder.build();
-
- keyManagerListener.setKeyManager(keyManager);
- trustManagerListener.setTrustManager(trustManager);
- logger.info("Key Manager and Trust Manager Reloaded from Changed
Path [{}]", storeFileNameFound.get().toAbsolutePath());
- }
- }
-}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/framework/ssl/SecurityStoreMonitorCommand.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/framework/ssl/SecurityStoreMonitorCommand.java
new file mode 100644
index 00000000000..e9aae0d5f3e
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/framework/ssl/SecurityStoreMonitorCommand.java
@@ -0,0 +1,133 @@
+/*
+ * 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.framework.ssl;
+
+import org.apache.nifi.security.ssl.KeyManagerBuilder;
+import org.apache.nifi.security.ssl.KeyManagerListener;
+import org.apache.nifi.security.ssl.TrustManagerBuilder;
+import org.apache.nifi.security.ssl.TrustManagerListener;
+import org.apache.nifi.util.security.MessageDigestUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import javax.net.ssl.X509ExtendedKeyManager;
+import javax.net.ssl.X509ExtendedTrustManager;
+
+/**
+ * Runnable command that digests configured Key Store and Trust Store paths
and reloads managers when content changes
+ */
+public class SecurityStoreMonitorCommand implements Runnable {
+ private static final Logger logger =
LoggerFactory.getLogger(SecurityStoreMonitorCommand.class);
+
+ private final Set<Path> storePaths;
+
+ private final Map<Path, byte[]> storeDigests;
+
+ private final KeyManagerListener keyManagerListener;
+
+ private final KeyManagerBuilder keyManagerBuilder;
+
+ private final TrustManagerListener trustManagerListener;
+
+ private final TrustManagerBuilder trustManagerBuilder;
+
+ /**
+ * Security Store Monitor Command reloads Key Manager and Trust Manager
when a configured store path digest changes
+ *
+ * @param storePaths Key Store and Trust Store Paths to digest
+ * @param keyManagerListener Key Manager Listener for handling updated Key
Manager
+ * @param keyManagerBuilder Key Manager Builder for creating new Key
Manager instances
+ * @param trustManagerListener Trust Manager Listener for handling updated
Trust Manager
+ * @param trustManagerBuilder Trust Manager Builder for creating new Trust
Manager instances
+ */
+ public SecurityStoreMonitorCommand(
+ final Set<Path> storePaths,
+ final KeyManagerListener keyManagerListener,
+ final KeyManagerBuilder keyManagerBuilder,
+ final TrustManagerListener trustManagerListener,
+ final TrustManagerBuilder trustManagerBuilder
+ ) {
+ this.storePaths = Set.copyOf(Objects.requireNonNull(storePaths, "Store
Paths required"));
+ this.keyManagerListener = Objects.requireNonNull(keyManagerListener,
"Key Manager Listener required");
+ this.keyManagerBuilder = Objects.requireNonNull(keyManagerBuilder,
"Key Manager Builder required");
+ this.trustManagerListener =
Objects.requireNonNull(trustManagerListener, "Trust Manager Listener required");
+ this.trustManagerBuilder = Objects.requireNonNull(trustManagerBuilder,
"Trust Manager Builder required");
+ this.storeDigests = new HashMap<>();
+
+ for (final Path storePath : this.storePaths) {
+ try {
+ storeDigests.put(storePath, getDigest(storePath));
+ } catch (final IOException e) {
+ logger.warn("Digest calculation failed for Security Store
[{}]", storePath, e);
+ }
+ }
+ }
+
+ /**
+ * Digest configured store paths and reload Key Manager and Trust Manager
when content changes
+ */
+ @Override
+ public void run() {
+ final List<Path> changedPaths = new ArrayList<>();
+
+ for (final Path storePath : storePaths) {
+ try {
+ final byte[] currentDigest = getDigest(storePath);
+ // Set current digest regardless of potential issues with
reading other paths
+ final byte[] previousDigest = storeDigests.put(storePath,
currentDigest);
+ if (Arrays.equals(previousDigest, currentDigest)) {
+ logger.debug("Digest not changed for Store Path [{}]",
storePath);
+ } else {
+ changedPaths.add(storePath);
+ }
+ } catch (final IOException e) {
+ logger.warn("Digest calculation failed for Security Store
[{}]", storePath, e);
+ }
+ }
+
+ if (changedPaths.isEmpty()) {
+ logger.debug("Changed Security Store Paths not found");
+ } else {
+ try {
+ final X509ExtendedKeyManager keyManager =
keyManagerBuilder.build();
+ final X509ExtendedTrustManager trustManager =
trustManagerBuilder.build();
+ keyManagerListener.setKeyManager(keyManager);
+ trustManagerListener.setTrustManager(trustManager);
+ logger.info("Key Manager and Trust Manager reloaded for
changed Security Store Paths {}", changedPaths);
+ } catch (final RuntimeException e) {
+ logger.warn("Key Manager and Trust Manager reload failed for
changed Security Store Paths {}", changedPaths, e);
+ }
+ }
+ }
+
+ private byte[] getDigest(final Path storePath) throws IOException {
+ try (InputStream inputStream = Files.newInputStream(storePath)) {
+ return MessageDigestUtils.getDigest(inputStream);
+ }
+ }
+}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/framework/ssl/WatchServiceMonitorCommand.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/framework/ssl/WatchServiceMonitorCommand.java
deleted file mode 100644
index 64433b9d0eb..00000000000
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/framework/ssl/WatchServiceMonitorCommand.java
+++ /dev/null
@@ -1,107 +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.framework.ssl;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.nio.file.Path;
-import java.nio.file.StandardWatchEventKinds;
-import java.nio.file.WatchEvent;
-import java.nio.file.WatchKey;
-import java.nio.file.WatchService;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-
-/**
- * Runnable command to poll a configured File Watch Service and notify
registered listeners
- */
-public class WatchServiceMonitorCommand implements Runnable {
- private static final Logger logger =
LoggerFactory.getLogger(WatchServiceMonitorCommand.class);
-
- private final WatchService watchService;
-
- private final ChangedPathListener changedPathListener;
-
- public WatchServiceMonitorCommand(final WatchService watchService, final
ChangedPathListener changedPathListener) {
- this.watchService = Objects.requireNonNull(watchService, "Watch
Service required");
- this.changedPathListener = Objects.requireNonNull(changedPathListener,
"Changed Path Listener required");
- }
-
- /**
- * Poll Watch Service and process events
- */
- @Override
- public void run() {
- final WatchKey watchKey = watchService.poll();
- if (watchKey == null) {
- logger.debug("Watch Key not found");
- } else {
- try {
- processWatchKey(watchKey);
- } finally {
- if (watchKey.reset()) {
- logger.debug("Watch Key reset completed");
- } else {
- logger.warn("Watch Key reset failed: Watch Service no
longer valid");
- }
- }
- }
- }
-
- private void processWatchKey(final WatchKey watchKey) {
- final List<WatchEvent<?>> events = watchKey.pollEvents();
- final List<Path> changedPaths = getChangedPaths(events);
- if (changedPaths.isEmpty()) {
- logger.debug("Changed Paths not found");
- } else {
- logger.debug("Changed Paths found {}", changedPaths);
- changedPathListener.onChanged(changedPaths);
- }
- }
-
- private List<Path> getChangedPaths(final List<WatchEvent<?>> events) {
- final List<Path> changedPaths = new ArrayList<>();
-
- for (final WatchEvent<?> event : events) {
- final WatchEvent.Kind<?> kind = event.kind();
- if (StandardWatchEventKinds.OVERFLOW == kind) {
- continue;
- }
-
- final Object context = event.context();
- if (context instanceof final Path path) {
- changedPaths.add(path);
- }
- }
-
- return changedPaths;
- }
-
- /**
- * Changed Path Listener for handling file watch events
- */
- public interface ChangedPathListener {
- /**
- * Handle Changed Paths
- *
- * @param changedPaths Changed Paths
- */
- void onChanged(List<Path> changedPaths);
- }
-}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/framework/ssl/SecurityStoreMonitorCommandTest.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/framework/ssl/SecurityStoreMonitorCommandTest.java
new file mode 100644
index 00000000000..c7d44b2db37
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/framework/ssl/SecurityStoreMonitorCommandTest.java
@@ -0,0 +1,159 @@
+/*
+ * 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.framework.ssl;
+
+import org.apache.nifi.security.ssl.KeyManagerBuilder;
+import org.apache.nifi.security.ssl.KeyManagerListener;
+import org.apache.nifi.security.ssl.TrustManagerBuilder;
+import org.apache.nifi.security.ssl.TrustManagerListener;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Set;
+import javax.net.ssl.X509ExtendedKeyManager;
+import javax.net.ssl.X509ExtendedTrustManager;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class SecurityStoreMonitorCommandTest {
+ private static final String KEY_STORE_FILE_NAME = "keystore.p12";
+
+ private static final String INITIAL_CONTENT = "initial-store";
+
+ private static final String UPDATED_CONTENT = "updated-store";
+
+ private static final String RELOADED_CONTENT = "reloaded-store";
+
+ @TempDir
+ private Path tempDir;
+
+ @Mock
+ private KeyManagerListener keyManagerListener;
+
+ @Mock
+ private KeyManagerBuilder keyManagerBuilder;
+
+ @Mock
+ private TrustManagerListener trustManagerListener;
+
+ @Mock
+ private TrustManagerBuilder trustManagerBuilder;
+
+ @Mock
+ private X509ExtendedKeyManager keyManager;
+
+ @Mock
+ private X509ExtendedTrustManager trustManager;
+
+ @Test
+ void testRunStoreUnchanged() throws IOException {
+ final Path keyStorePath = writeStore(INITIAL_CONTENT);
+ final SecurityStoreMonitorCommand command = newCommand(keyStorePath);
+
+ command.run();
+
+ verifyNoInteractions(keyManagerBuilder, keyManagerListener,
trustManagerBuilder, trustManagerListener);
+ }
+
+ @Test
+ void testRunStoreRewrittenReloadsManagers() throws IOException {
+ final Path keyStorePath = writeStore(INITIAL_CONTENT);
+ final SecurityStoreMonitorCommand command = newCommand(keyStorePath);
+ setManagers();
+
+ writeStore(UPDATED_CONTENT);
+ command.run();
+
+ verify(keyManagerListener).setKeyManager(keyManager);
+ verify(trustManagerListener).setTrustManager(trustManager);
+ }
+
+ @Test
+ void testRunStoreRewrittenWithIdenticalContent() throws IOException {
+ final Path keyStorePath = writeStore(INITIAL_CONTENT);
+ final SecurityStoreMonitorCommand command = newCommand(keyStorePath);
+
+ writeStore(INITIAL_CONTENT);
+ command.run();
+
+ verifyNoInteractions(keyManagerBuilder, keyManagerListener,
trustManagerBuilder, trustManagerListener);
+ }
+
+ @Test
+ void testRunReloadFailureDoesNotPropagate() throws IOException {
+ final Path keyStorePath = writeStore(INITIAL_CONTENT);
+ final SecurityStoreMonitorCommand command = newCommand(keyStorePath);
+
+ doThrow(new IllegalStateException("Key Store loading
failed")).when(keyManagerBuilder).build();
+ writeStore(UPDATED_CONTENT);
+ command.run();
+
+ verifyNoInteractions(keyManagerListener, trustManagerListener);
+
+ doReturn(keyManager).when(keyManagerBuilder).build();
+ doReturn(trustManager).when(trustManagerBuilder).build();
+ writeStore(RELOADED_CONTENT);
+ command.run();
+
+ verify(keyManagerListener).setKeyManager(keyManager);
+ verify(trustManagerListener).setTrustManager(trustManager);
+ }
+
+ @Test
+ void testRunDeletedStoreDoesNotPropagate() throws IOException {
+ final Path keyStorePath = writeStore(INITIAL_CONTENT);
+ final SecurityStoreMonitorCommand command = newCommand(keyStorePath);
+
+ Files.delete(keyStorePath);
+ command.run();
+
+ verifyNoInteractions(keyManagerBuilder, keyManagerListener,
trustManagerBuilder, trustManagerListener);
+ }
+
+ private SecurityStoreMonitorCommand newCommand(final Path storePath) {
+ return new SecurityStoreMonitorCommand(
+ Set.of(storePath),
+ keyManagerListener,
+ keyManagerBuilder,
+ trustManagerListener,
+ trustManagerBuilder
+ );
+ }
+
+ private void setManagers() {
+ when(keyManagerBuilder.build()).thenReturn(keyManager);
+ when(trustManagerBuilder.build()).thenReturn(trustManager);
+ }
+
+ private Path writeStore(final String content) throws IOException {
+ final Path storePath = tempDir.resolve(KEY_STORE_FILE_NAME);
+ Files.writeString(storePath, content, StandardCharsets.UTF_8);
+ return storePath;
+ }
+}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/framework/ssl/WatchServiceMonitorCommandTest.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/framework/ssl/WatchServiceMonitorCommandTest.java
deleted file mode 100644
index a6774512589..00000000000
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/framework/ssl/WatchServiceMonitorCommandTest.java
+++ /dev/null
@@ -1,80 +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.framework.ssl;
-
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.Timeout;
-import org.junit.jupiter.api.io.TempDir;
-
-import java.io.IOException;
-import java.nio.file.FileSystem;
-import java.nio.file.FileSystems;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.StandardWatchEventKinds;
-import java.nio.file.WatchService;
-import java.util.ArrayList;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-class WatchServiceMonitorCommandTest {
- @TempDir
- private Path tempDir;
-
- @Test
- void testRunChangedPathsNotFound() throws IOException {
- final FileSystem fileSystem = FileSystems.getDefault();
- try (WatchService watchService = fileSystem.newWatchService()) {
- registerTempDir(watchService);
-
- final List<Path> changedPaths = new ArrayList<>();
-
- final WatchServiceMonitorCommand command = new
WatchServiceMonitorCommand(watchService, changedPaths::addAll);
-
- command.run();
-
- assertTrue(changedPaths.isEmpty());
- }
- }
-
- @Timeout(5)
- @Test
- void testRunChangedPathsFound() throws IOException {
- final FileSystem fileSystem = FileSystems.getDefault();
- try (WatchService watchService = fileSystem.newWatchService()) {
- registerTempDir(watchService);
-
- final List<Path> changedPaths = new ArrayList<>();
-
- final WatchServiceMonitorCommand command = new
WatchServiceMonitorCommand(watchService, changedPaths::addAll);
-
- final Path tempFile = Files.createTempFile(tempDir,
WatchServiceMonitorCommandTest.class.getSimpleName(), null);
- while (changedPaths.isEmpty()) {
- command.run();
- }
-
- final Path firstChangedPath = changedPaths.getFirst();
- assertEquals(tempFile.getFileName(),
firstChangedPath.getFileName());
- }
- }
-
- private void registerTempDir(final WatchService watchService) throws
IOException {
- tempDir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY);
- }
-}