exceptionfactory commented on a change in pull request #4991:
URL: https://github.com/apache/nifi/pull/4991#discussion_r621736609
##########
File path: nifi-docs/src/main/asciidoc/administration-guide.adoc
##########
@@ -199,6 +199,26 @@ Now that the User Interface has been secured, we can
easily secure Site-to-Site
accomplished by setting the `nifi.remote.input.secure` and
`nifi.cluster.protocol.is.secure` properties, respectively, to `true`. These
communications
will always REQUIRE two way SSL as the nodes will use their configured
keystore/truststore for authentication.
+Automatic refreshing of NiFi's web SSL context factory can be enabled using
the following properties:
+
+[options="header,footer"]
+|==================================================================================================================================================
+| Property Name | Description
+|`nifi.security.autorefresh.enabled`|Specifies whether the SSL context factory
should be automatically refreshed if updates to the keystore and truststore are
detected. By default, it is set to `false`.
+|`nifi.security.autorefresh.interval`|Specifies the interval at which the
keystore and truststore are checked for updates. Only applies if
`nifi.security.autorefresh.enabled` is set to `true`. The default value is `10
secs`.
+|==================================================================================================================================================
+
+Once the `nifi.security.autorefresh.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.
+
+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.
+
+There are no restrictions on updates to certificates in the trust store, but
for security reasons, changes to the keystore are considered "valid" only if:
+
+* No entries are added to the keystore
+* There are no changes to the aliases of existing entries
+* There are no changes to the subject principal of existing entries
+* There are no changes to the issuer principal or serial number of existing
entries
Review comment:
On further review, do you think it is necessary to ensure the continuity
both the alias and issuer? Checking the subject ensures that the identity
doesn't change, but the alias does not seem like it impacts functionality. The
issuer should generally remain the same, but it is possible that the subject
could stay the same and a new Certificate Authority issued the certificate with
the same subject.
##########
File path:
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/main/java/org/apache/nifi/autoload/SSLContextFactoryAutoLoader.java
##########
@@ -0,0 +1,148 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.SSLContextFactoryReloadable;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.FormatUtils;
+import org.apache.nifi.util.NiFiProperties;
+import org.apache.nifi.util.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchService;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Starts a thread to monitor the keystore and truststore configured in
nifi.properties.
+ */
+public class SSLContextFactoryAutoLoader {
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(SSLContextFactoryAutoLoader.class);
+
+ private File keystore = null;
+ private File truststore = null;
+ private final SSLContextFactoryReloadable sslContextFactoryReloadable;
+
+ private boolean isApplicable = false;
+
+ private long pollIntervalMS;
+ private final ScheduledThreadPoolExecutor executor;
+
+ private final NiFiProperties niFiProperties;
+
+ private volatile SSLContextFactoryAutoLoaderTask
sslContextFactoryAutoLoaderTask;
+ private volatile boolean started = false;
+
+ public SSLContextFactoryAutoLoader(final NiFiProperties nifiProperties,
SSLContextFactoryReloadable sslContextFactoryReloadable) {
+ this.niFiProperties = nifiProperties;
+ if
(nifiProperties.getProperty(NiFiProperties.SECURITY_AUTO_REFRESH_ENABLED,
"false").equals("true")) {
+ LOGGER.info("Auto-refreshing of keystore and truststore is
enabled.");
Review comment:
This informational log seems unnecessary since another log message is
written at the end of this block based on whether or not the loader is enabled.
##########
File path:
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/main/java/org/apache/nifi/autoload/SSLContextFactoryAutoLoader.java
##########
@@ -0,0 +1,148 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.SSLContextFactoryReloadable;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.FormatUtils;
+import org.apache.nifi.util.NiFiProperties;
+import org.apache.nifi.util.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchService;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Starts a thread to monitor the keystore and truststore configured in
nifi.properties.
+ */
+public class SSLContextFactoryAutoLoader {
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(SSLContextFactoryAutoLoader.class);
+
+ private File keystore = null;
+ private File truststore = null;
+ private final SSLContextFactoryReloadable sslContextFactoryReloadable;
+
+ private boolean isApplicable = false;
+
+ private long pollIntervalMS;
+ private final ScheduledThreadPoolExecutor executor;
+
+ private final NiFiProperties niFiProperties;
+
+ private volatile SSLContextFactoryAutoLoaderTask
sslContextFactoryAutoLoaderTask;
+ private volatile boolean started = false;
Review comment:
Could this tracking variable be replaced by calling
`ThreadPoolExecutor.isShutdown()`
##########
File path:
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/main/java/org/apache/nifi/autoload/SSLContextFactoryAutoLoader.java
##########
@@ -0,0 +1,148 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.SSLContextFactoryReloadable;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.FormatUtils;
+import org.apache.nifi.util.NiFiProperties;
+import org.apache.nifi.util.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchService;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Starts a thread to monitor the keystore and truststore configured in
nifi.properties.
+ */
+public class SSLContextFactoryAutoLoader {
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(SSLContextFactoryAutoLoader.class);
+
+ private File keystore = null;
+ private File truststore = null;
+ private final SSLContextFactoryReloadable sslContextFactoryReloadable;
+
+ private boolean isApplicable = false;
+
+ private long pollIntervalMS;
+ private final ScheduledThreadPoolExecutor executor;
+
+ private final NiFiProperties niFiProperties;
+
+ private volatile SSLContextFactoryAutoLoaderTask
sslContextFactoryAutoLoaderTask;
+ private volatile boolean started = false;
+
+ public SSLContextFactoryAutoLoader(final NiFiProperties nifiProperties,
SSLContextFactoryReloadable sslContextFactoryReloadable) {
+ this.niFiProperties = nifiProperties;
+ if
(nifiProperties.getProperty(NiFiProperties.SECURITY_AUTO_REFRESH_ENABLED,
"false").equals("true")) {
+ LOGGER.info("Auto-refreshing of keystore and truststore is
enabled.");
+ pollIntervalMS =
Double.valueOf(FormatUtils.getPreciseTimeDuration(nifiProperties.getSecurityAutoRefreshInterval(),
TimeUnit.MILLISECONDS))
+ .longValue();
+
+ String keystorePath =
nifiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE);
+ if (StringUtils.isNotBlank(keystorePath)) {
+ this.keystore = new File(keystorePath);
+ if (keystore.exists() && keystore.canRead()) {
+ String truststorePath =
nifiProperties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE);
+ if (StringUtils.isNotBlank(truststorePath)) {
+ this.truststore = new File(truststorePath);
+
+ if (truststore.exists() && truststore.canRead()) {
+ isApplicable = true;
+ }
+ }
+ }
+ }
+ if (isApplicable) {
+ LOGGER.info("Keystore and truststore will auto-refresh when
applicable changes are made.");
+ } else {
+ LOGGER.info("No keystore and truststore detected: nothing to
auto-refresh.");
+ }
+ } else {
+ LOGGER.info("Auto-refreshing of keystore and truststore is
disabled.");
+ }
+ this.executor = new ScheduledThreadPoolExecutor(1, new ThreadFactory()
{
+ @Override
+ public Thread newThread(Runnable r) {
+ final Thread thread = new Thread(r);
+ thread.setName("SSL Context Factory Auto-Loader");
+ thread.setDaemon(true);
+ return thread;
+ }
+ });
+ this.sslContextFactoryReloadable = sslContextFactoryReloadable;
+ }
+
+ protected SSLContextFactoryReloadable getSslContextFactoryReloadable() {
+ return sslContextFactoryReloadable;
+ }
+
+ public synchronized void start() throws IOException,
NoSuchAlgorithmException, UnrecoverableEntryException, KeyStoreException,
TlsException {
+ if (started || !isApplicable) {
+ return;
+ }
+
+ final WatchService keystoreWatcher =
FileSystems.getDefault().newWatchService();
+ final Path keystoreDirPath = keystore.toPath().getParent();
+ keystoreDirPath.register(keystoreWatcher,
StandardWatchEventKinds.ENTRY_MODIFY);
+
+ WatchService truststoreWatcher =
FileSystems.getDefault().newWatchService();
+ final Path truststoreDirPath = truststore.toPath().getParent();
+ truststoreDirPath.register(truststoreWatcher,
StandardWatchEventKinds.ENTRY_MODIFY);
+
+ // In this case, just watch the same directory, but the task will look
for both keystore and truststore file updates
+ if (truststoreDirPath.toString().equals(keystoreDirPath.toString())) {
+ truststoreWatcher = keystoreWatcher;
+ }
+
+ sslContextFactoryAutoLoaderTask = new
SSLContextFactoryAutoLoaderTask.Builder()
+ .keystorePath(keystore.toPath())
+ .truststorePath(truststore.toPath())
+ .keystoreWatchService(keystoreWatcher)
+ .truststoreWatchService(truststoreWatcher)
+ .autoLoader(this)
+ .nifiProperties(niFiProperties)
+ .build();
+
+ LOGGER.info("Starting keystore auto-refresh for {} ...", new
Object[]{keystore.toPath()});
+ LOGGER.info("Starting truststore auto-refresh for {} ...", new
Object[]{truststore.toPath()});
Review comment:
It seems like these messages would be better as debug, or perhaps
removed, since the keystore and truststore are known properties, and the
constructor would have already logged that fact the automatic refresh is
enabled.
##########
File path:
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/main/java/org/apache/nifi/autoload/SSLContextFactoryAutoLoader.java
##########
@@ -0,0 +1,148 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.SSLContextFactoryReloadable;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.FormatUtils;
+import org.apache.nifi.util.NiFiProperties;
+import org.apache.nifi.util.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchService;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Starts a thread to monitor the keystore and truststore configured in
nifi.properties.
+ */
+public class SSLContextFactoryAutoLoader {
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(SSLContextFactoryAutoLoader.class);
+
+ private File keystore = null;
+ private File truststore = null;
+ private final SSLContextFactoryReloadable sslContextFactoryReloadable;
+
+ private boolean isApplicable = false;
+
+ private long pollIntervalMS;
+ private final ScheduledThreadPoolExecutor executor;
+
+ private final NiFiProperties niFiProperties;
+
+ private volatile SSLContextFactoryAutoLoaderTask
sslContextFactoryAutoLoaderTask;
+ private volatile boolean started = false;
+
+ public SSLContextFactoryAutoLoader(final NiFiProperties nifiProperties,
SSLContextFactoryReloadable sslContextFactoryReloadable) {
+ this.niFiProperties = nifiProperties;
+ if
(nifiProperties.getProperty(NiFiProperties.SECURITY_AUTO_REFRESH_ENABLED,
"false").equals("true")) {
+ LOGGER.info("Auto-refreshing of keystore and truststore is
enabled.");
+ pollIntervalMS =
Double.valueOf(FormatUtils.getPreciseTimeDuration(nifiProperties.getSecurityAutoRefreshInterval(),
TimeUnit.MILLISECONDS))
+ .longValue();
+
+ String keystorePath =
nifiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE);
+ if (StringUtils.isNotBlank(keystorePath)) {
+ this.keystore = new File(keystorePath);
+ if (keystore.exists() && keystore.canRead()) {
+ String truststorePath =
nifiProperties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE);
+ if (StringUtils.isNotBlank(truststorePath)) {
+ this.truststore = new File(truststorePath);
+
+ if (truststore.exists() && truststore.canRead()) {
+ isApplicable = true;
+ }
+ }
+ }
+ }
+ if (isApplicable) {
+ LOGGER.info("Keystore and truststore will auto-refresh when
applicable changes are made.");
Review comment:
Although minor, recommend avoiding periods at the end of logging
statements, perhaps simplifying it to the following?
```suggestion
LOGGER.info("Automatic refresh of keystore and truststore
enabled");
```
##########
File path:
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/main/java/org/apache/nifi/autoload/SSLContextFactoryAutoLoaderTask.java
##########
@@ -0,0 +1,248 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.NiFiProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+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.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The runnable task that polls the WatchService for updates to the keystore
and truststore.
+ *
+ */
+public class SSLContextFactoryAutoLoaderTask implements Runnable {
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(SSLContextFactoryAutoLoaderTask.class);
+
+ private static final int MIN_FILE_AGE = 5000;
+
+ private final Path keystorePath;
+ private final Path truststorePath;
+ private final WatchService keystoreWatchService;
+ private final WatchService truststoreWatchService;
+ private final SSLContextFactoryAutoLoader autoLoader;
+ private final NiFiProperties nifiProperties;
+ private final List<File> candidateStores;
+
+ private final List<CertificateEntryDescription> existingKeystoreState;
+
+ private volatile boolean stopped = false;
+
+ private SSLContextFactoryAutoLoaderTask(final Builder builder) throws
NoSuchAlgorithmException, UnrecoverableEntryException,
+ KeyStoreException, TlsException {
+ this.keystorePath = builder.keystorePath;
+ this.truststorePath = builder.truststorePath;
+ this.keystoreWatchService = builder.keystoreWatchService;
+ this.truststoreWatchService = builder.truststoreWatchService;
+ this.autoLoader = builder.autoLoader;
+ this.nifiProperties = builder.niFiProperties;
+ this.existingKeystoreState = this.getKeystoreState();
+ this.candidateStores = new ArrayList<>();
+ }
+
+ private boolean poll(WatchService watchService, Collection<Path>
storePaths) {
+ if (storePaths == null || storePaths.isEmpty()) {
+ throw new RuntimeException("A polling directory must be
specified.");
+ }
+ WatchKey key = watchService.poll();
+
+ boolean storeChanged = false;
+
+ // Key comes back as null when there are no new create events, but we
still want to continue processing
+ // so we can consider files added to the candidateNars list in
previous iterations
+
+ if (key != null) {
+ for (WatchEvent<?> event : key.pollEvents()) {
+ final WatchEvent.Kind<?> kind = event.kind();
+ if (kind == StandardWatchEventKinds.OVERFLOW) {
+ continue;
+ }
+
+ final WatchEvent<Path> ev = (WatchEvent<Path>) event;
+ final Path filename = ev.context();
+
+ for(Path storePath : storePaths) {
+
+ final Path autoLoadFile =
storePath.getParent().resolve(filename);
+ final String autoLoadFilename =
autoLoadFile.toFile().getName();
+
+ if
(!storePath.getFileName().toString().equals(autoLoadFilename)) {
+ continue;
+ }
+
+ LOGGER.info("Found update to {}", new
Object[]{autoLoadFilename});
+ storeChanged = true;
+ }
+ }
+
+ final boolean valid = key.reset();
+ if (!valid) {
+ LOGGER.error("{} auto-refresh directory is no longer valid",
new Object[] {storePaths.iterator().next()});
+ autoLoader.stop();
+ }
+ return storeChanged;
+ }
+ return false;
+ }
+
+ @Override
+ public void run() {
+ Set<Path> bothPaths = new HashSet<>(Arrays.asList(keystorePath,
truststorePath));
+ try {
+ boolean storeChanged = false;
+ // Can we poll the same directory for updates?
+ if (keystoreWatchService == truststoreWatchService) {
+ LOGGER.debug("Polling for keystore updates at {} and
truststore updates at {}", new Object[]{keystorePath, truststorePath});
+ storeChanged = this.poll(keystoreWatchService, bothPaths);
+ } else {
+ // Otherwise, poll separate directories
+ LOGGER.debug("Polling for keystore updates at {}", new
Object[]{keystorePath});
+ storeChanged = this.poll(keystoreWatchService,
Arrays.asList(keystorePath));
+
+ LOGGER.debug("Polling for truststore updates at {}", new
Object[]{truststorePath});
+ storeChanged |= this.poll(truststoreWatchService,
Arrays.asList(truststorePath));
+ }
+
+ if (storeChanged) {
+ if (this.isReloadAllowed()) {
+
autoLoader.getSslContextFactoryReloadable().reloadSslContextFactory();
+ } else {
+ LOGGER.warn("For security reasons, the SSL Context Factory
could not be reloaded because the " +
+ "keystore {} changed in a way that is
disallowed.", new Object[] {keystorePath});
+ }
+ }
+
+ } catch (final Throwable t) {
+ LOGGER.error("Error reloading SSL context factory due to: " +
t.getMessage(), t);
Review comment:
String concatenation should be replaced with placeholder logging
statements.
```suggestion
LOGGER.error("Reloading SslContextFactory Failed: {}",
t.getMessage(), t);
```
##########
File path:
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/main/java/org/apache/nifi/autoload/SSLContextFactoryAutoLoaderTask.java
##########
@@ -0,0 +1,248 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.NiFiProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+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.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The runnable task that polls the WatchService for updates to the keystore
and truststore.
+ *
+ */
+public class SSLContextFactoryAutoLoaderTask implements Runnable {
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(SSLContextFactoryAutoLoaderTask.class);
+
+ private static final int MIN_FILE_AGE = 5000;
+
+ private final Path keystorePath;
+ private final Path truststorePath;
+ private final WatchService keystoreWatchService;
+ private final WatchService truststoreWatchService;
+ private final SSLContextFactoryAutoLoader autoLoader;
+ private final NiFiProperties nifiProperties;
+ private final List<File> candidateStores;
+
+ private final List<CertificateEntryDescription> existingKeystoreState;
+
+ private volatile boolean stopped = false;
+
+ private SSLContextFactoryAutoLoaderTask(final Builder builder) throws
NoSuchAlgorithmException, UnrecoverableEntryException,
+ KeyStoreException, TlsException {
+ this.keystorePath = builder.keystorePath;
+ this.truststorePath = builder.truststorePath;
+ this.keystoreWatchService = builder.keystoreWatchService;
+ this.truststoreWatchService = builder.truststoreWatchService;
+ this.autoLoader = builder.autoLoader;
+ this.nifiProperties = builder.niFiProperties;
+ this.existingKeystoreState = this.getKeystoreState();
+ this.candidateStores = new ArrayList<>();
+ }
+
+ private boolean poll(WatchService watchService, Collection<Path>
storePaths) {
+ if (storePaths == null || storePaths.isEmpty()) {
+ throw new RuntimeException("A polling directory must be
specified.");
+ }
+ WatchKey key = watchService.poll();
+
+ boolean storeChanged = false;
+
+ // Key comes back as null when there are no new create events, but we
still want to continue processing
+ // so we can consider files added to the candidateNars list in
previous iterations
+
+ if (key != null) {
+ for (WatchEvent<?> event : key.pollEvents()) {
+ final WatchEvent.Kind<?> kind = event.kind();
+ if (kind == StandardWatchEventKinds.OVERFLOW) {
+ continue;
+ }
+
+ final WatchEvent<Path> ev = (WatchEvent<Path>) event;
+ final Path filename = ev.context();
+
+ for(Path storePath : storePaths) {
+
+ final Path autoLoadFile =
storePath.getParent().resolve(filename);
+ final String autoLoadFilename =
autoLoadFile.toFile().getName();
+
+ if
(!storePath.getFileName().toString().equals(autoLoadFilename)) {
+ continue;
+ }
+
+ LOGGER.info("Found update to {}", new
Object[]{autoLoadFilename});
+ storeChanged = true;
+ }
+ }
+
+ final boolean valid = key.reset();
+ if (!valid) {
+ LOGGER.error("{} auto-refresh directory is no longer valid",
new Object[] {storePaths.iterator().next()});
+ autoLoader.stop();
+ }
+ return storeChanged;
+ }
+ return false;
+ }
+
+ @Override
+ public void run() {
+ Set<Path> bothPaths = new HashSet<>(Arrays.asList(keystorePath,
truststorePath));
+ try {
+ boolean storeChanged = false;
+ // Can we poll the same directory for updates?
+ if (keystoreWatchService == truststoreWatchService) {
Review comment:
Should this use `equals()` instead of `==`?
##########
File path:
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/main/java/org/apache/nifi/autoload/SSLContextFactoryAutoLoaderTask.java
##########
@@ -0,0 +1,248 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.NiFiProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+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.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The runnable task that polls the WatchService for updates to the keystore
and truststore.
+ *
+ */
+public class SSLContextFactoryAutoLoaderTask implements Runnable {
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(SSLContextFactoryAutoLoaderTask.class);
+
+ private static final int MIN_FILE_AGE = 5000;
+
+ private final Path keystorePath;
+ private final Path truststorePath;
+ private final WatchService keystoreWatchService;
+ private final WatchService truststoreWatchService;
+ private final SSLContextFactoryAutoLoader autoLoader;
+ private final NiFiProperties nifiProperties;
+ private final List<File> candidateStores;
+
+ private final List<CertificateEntryDescription> existingKeystoreState;
+
+ private volatile boolean stopped = false;
+
+ private SSLContextFactoryAutoLoaderTask(final Builder builder) throws
NoSuchAlgorithmException, UnrecoverableEntryException,
+ KeyStoreException, TlsException {
+ this.keystorePath = builder.keystorePath;
+ this.truststorePath = builder.truststorePath;
+ this.keystoreWatchService = builder.keystoreWatchService;
+ this.truststoreWatchService = builder.truststoreWatchService;
+ this.autoLoader = builder.autoLoader;
+ this.nifiProperties = builder.niFiProperties;
+ this.existingKeystoreState = this.getKeystoreState();
+ this.candidateStores = new ArrayList<>();
+ }
+
+ private boolean poll(WatchService watchService, Collection<Path>
storePaths) {
+ if (storePaths == null || storePaths.isEmpty()) {
+ throw new RuntimeException("A polling directory must be
specified.");
+ }
+ WatchKey key = watchService.poll();
+
+ boolean storeChanged = false;
+
+ // Key comes back as null when there are no new create events, but we
still want to continue processing
+ // so we can consider files added to the candidateNars list in
previous iterations
+
+ if (key != null) {
+ for (WatchEvent<?> event : key.pollEvents()) {
+ final WatchEvent.Kind<?> kind = event.kind();
+ if (kind == StandardWatchEventKinds.OVERFLOW) {
+ continue;
+ }
+
+ final WatchEvent<Path> ev = (WatchEvent<Path>) event;
+ final Path filename = ev.context();
+
+ for(Path storePath : storePaths) {
+
+ final Path autoLoadFile =
storePath.getParent().resolve(filename);
+ final String autoLoadFilename =
autoLoadFile.toFile().getName();
+
+ if
(!storePath.getFileName().toString().equals(autoLoadFilename)) {
+ continue;
+ }
+
+ LOGGER.info("Found update to {}", new
Object[]{autoLoadFilename});
+ storeChanged = true;
+ }
+ }
+
+ final boolean valid = key.reset();
+ if (!valid) {
+ LOGGER.error("{} auto-refresh directory is no longer valid",
new Object[] {storePaths.iterator().next()});
+ autoLoader.stop();
+ }
+ return storeChanged;
+ }
+ return false;
+ }
+
+ @Override
+ public void run() {
+ Set<Path> bothPaths = new HashSet<>(Arrays.asList(keystorePath,
truststorePath));
+ try {
+ boolean storeChanged = false;
+ // Can we poll the same directory for updates?
+ if (keystoreWatchService == truststoreWatchService) {
+ LOGGER.debug("Polling for keystore updates at {} and
truststore updates at {}", new Object[]{keystorePath, truststorePath});
Review comment:
The `Object[]` wrapper here and following is not necessary:
```suggestion
LOGGER.debug("Polling keystore [{}] truststore [{}] for
updates", keystorePath, truststorePath);
```
##########
File path:
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/main/java/org/apache/nifi/autoload/SSLContextFactoryAutoLoader.java
##########
@@ -0,0 +1,148 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.SSLContextFactoryReloadable;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.FormatUtils;
+import org.apache.nifi.util.NiFiProperties;
+import org.apache.nifi.util.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchService;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Starts a thread to monitor the keystore and truststore configured in
nifi.properties.
+ */
+public class SSLContextFactoryAutoLoader {
Review comment:
What do you think of renaming this to `SslContextFactoryMonitor`?
##########
File path:
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/main/java/org/apache/nifi/autoload/SSLContextFactoryAutoLoaderTask.java
##########
@@ -0,0 +1,248 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.NiFiProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+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.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The runnable task that polls the WatchService for updates to the keystore
and truststore.
+ *
+ */
+public class SSLContextFactoryAutoLoaderTask implements Runnable {
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(SSLContextFactoryAutoLoaderTask.class);
+
+ private static final int MIN_FILE_AGE = 5000;
+
+ private final Path keystorePath;
+ private final Path truststorePath;
+ private final WatchService keystoreWatchService;
+ private final WatchService truststoreWatchService;
+ private final SSLContextFactoryAutoLoader autoLoader;
+ private final NiFiProperties nifiProperties;
+ private final List<File> candidateStores;
+
+ private final List<CertificateEntryDescription> existingKeystoreState;
+
+ private volatile boolean stopped = false;
+
+ private SSLContextFactoryAutoLoaderTask(final Builder builder) throws
NoSuchAlgorithmException, UnrecoverableEntryException,
+ KeyStoreException, TlsException {
+ this.keystorePath = builder.keystorePath;
+ this.truststorePath = builder.truststorePath;
+ this.keystoreWatchService = builder.keystoreWatchService;
+ this.truststoreWatchService = builder.truststoreWatchService;
+ this.autoLoader = builder.autoLoader;
+ this.nifiProperties = builder.niFiProperties;
+ this.existingKeystoreState = this.getKeystoreState();
+ this.candidateStores = new ArrayList<>();
+ }
+
+ private boolean poll(WatchService watchService, Collection<Path>
storePaths) {
+ if (storePaths == null || storePaths.isEmpty()) {
+ throw new RuntimeException("A polling directory must be
specified.");
+ }
+ WatchKey key = watchService.poll();
+
+ boolean storeChanged = false;
+
+ // Key comes back as null when there are no new create events, but we
still want to continue processing
+ // so we can consider files added to the candidateNars list in
previous iterations
+
+ if (key != null) {
+ for (WatchEvent<?> event : key.pollEvents()) {
+ final WatchEvent.Kind<?> kind = event.kind();
+ if (kind == StandardWatchEventKinds.OVERFLOW) {
+ continue;
+ }
+
+ final WatchEvent<Path> ev = (WatchEvent<Path>) event;
+ final Path filename = ev.context();
+
+ for(Path storePath : storePaths) {
+
+ final Path autoLoadFile =
storePath.getParent().resolve(filename);
+ final String autoLoadFilename =
autoLoadFile.toFile().getName();
+
+ if
(!storePath.getFileName().toString().equals(autoLoadFilename)) {
+ continue;
+ }
+
+ LOGGER.info("Found update to {}", new
Object[]{autoLoadFilename});
Review comment:
This seems like a debug statement instead of info.
##########
File path:
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/main/java/org/apache/nifi/autoload/SSLContextFactoryAutoLoaderTask.java
##########
@@ -0,0 +1,248 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.NiFiProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+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.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The runnable task that polls the WatchService for updates to the keystore
and truststore.
+ *
+ */
+public class SSLContextFactoryAutoLoaderTask implements Runnable {
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(SSLContextFactoryAutoLoaderTask.class);
+
+ private static final int MIN_FILE_AGE = 5000;
+
+ private final Path keystorePath;
+ private final Path truststorePath;
+ private final WatchService keystoreWatchService;
+ private final WatchService truststoreWatchService;
+ private final SSLContextFactoryAutoLoader autoLoader;
+ private final NiFiProperties nifiProperties;
+ private final List<File> candidateStores;
+
+ private final List<CertificateEntryDescription> existingKeystoreState;
+
+ private volatile boolean stopped = false;
+
+ private SSLContextFactoryAutoLoaderTask(final Builder builder) throws
NoSuchAlgorithmException, UnrecoverableEntryException,
+ KeyStoreException, TlsException {
+ this.keystorePath = builder.keystorePath;
+ this.truststorePath = builder.truststorePath;
+ this.keystoreWatchService = builder.keystoreWatchService;
+ this.truststoreWatchService = builder.truststoreWatchService;
+ this.autoLoader = builder.autoLoader;
+ this.nifiProperties = builder.niFiProperties;
+ this.existingKeystoreState = this.getKeystoreState();
+ this.candidateStores = new ArrayList<>();
+ }
+
+ private boolean poll(WatchService watchService, Collection<Path>
storePaths) {
+ if (storePaths == null || storePaths.isEmpty()) {
Review comment:
Is it possible for `storePaths` to be empty since this is a private
method?
##########
File path:
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/main/java/org/apache/nifi/autoload/SSLContextFactoryAutoLoader.java
##########
@@ -0,0 +1,148 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.SSLContextFactoryReloadable;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.FormatUtils;
+import org.apache.nifi.util.NiFiProperties;
+import org.apache.nifi.util.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchService;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Starts a thread to monitor the keystore and truststore configured in
nifi.properties.
+ */
+public class SSLContextFactoryAutoLoader {
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(SSLContextFactoryAutoLoader.class);
+
+ private File keystore = null;
+ private File truststore = null;
+ private final SSLContextFactoryReloadable sslContextFactoryReloadable;
+
+ private boolean isApplicable = false;
+
+ private long pollIntervalMS;
+ private final ScheduledThreadPoolExecutor executor;
+
+ private final NiFiProperties niFiProperties;
+
+ private volatile SSLContextFactoryAutoLoaderTask
sslContextFactoryAutoLoaderTask;
Review comment:
Is it necessary to keep this as a member variable, or can it be changed
to method-local?
##########
File path:
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/main/java/org/apache/nifi/autoload/SSLContextFactoryAutoLoaderTask.java
##########
@@ -0,0 +1,248 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.NiFiProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+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.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The runnable task that polls the WatchService for updates to the keystore
and truststore.
+ *
+ */
+public class SSLContextFactoryAutoLoaderTask implements Runnable {
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(SSLContextFactoryAutoLoaderTask.class);
+
+ private static final int MIN_FILE_AGE = 5000;
+
+ private final Path keystorePath;
+ private final Path truststorePath;
+ private final WatchService keystoreWatchService;
+ private final WatchService truststoreWatchService;
+ private final SSLContextFactoryAutoLoader autoLoader;
+ private final NiFiProperties nifiProperties;
+ private final List<File> candidateStores;
+
+ private final List<CertificateEntryDescription> existingKeystoreState;
+
+ private volatile boolean stopped = false;
+
+ private SSLContextFactoryAutoLoaderTask(final Builder builder) throws
NoSuchAlgorithmException, UnrecoverableEntryException,
+ KeyStoreException, TlsException {
+ this.keystorePath = builder.keystorePath;
+ this.truststorePath = builder.truststorePath;
+ this.keystoreWatchService = builder.keystoreWatchService;
+ this.truststoreWatchService = builder.truststoreWatchService;
+ this.autoLoader = builder.autoLoader;
+ this.nifiProperties = builder.niFiProperties;
+ this.existingKeystoreState = this.getKeystoreState();
+ this.candidateStores = new ArrayList<>();
+ }
+
+ private boolean poll(WatchService watchService, Collection<Path>
storePaths) {
+ if (storePaths == null || storePaths.isEmpty()) {
+ throw new RuntimeException("A polling directory must be
specified.");
+ }
+ WatchKey key = watchService.poll();
+
+ boolean storeChanged = false;
+
+ // Key comes back as null when there are no new create events, but we
still want to continue processing
+ // so we can consider files added to the candidateNars list in
previous iterations
+
+ if (key != null) {
+ for (WatchEvent<?> event : key.pollEvents()) {
+ final WatchEvent.Kind<?> kind = event.kind();
+ if (kind == StandardWatchEventKinds.OVERFLOW) {
+ continue;
+ }
+
+ final WatchEvent<Path> ev = (WatchEvent<Path>) event;
+ final Path filename = ev.context();
+
+ for(Path storePath : storePaths) {
+
+ final Path autoLoadFile =
storePath.getParent().resolve(filename);
+ final String autoLoadFilename =
autoLoadFile.toFile().getName();
+
+ if
(!storePath.getFileName().toString().equals(autoLoadFilename)) {
+ continue;
+ }
+
+ LOGGER.info("Found update to {}", new
Object[]{autoLoadFilename});
+ storeChanged = true;
+ }
+ }
+
+ final boolean valid = key.reset();
+ if (!valid) {
+ LOGGER.error("{} auto-refresh directory is no longer valid",
new Object[] {storePaths.iterator().next()});
+ autoLoader.stop();
+ }
+ return storeChanged;
+ }
+ return false;
+ }
+
+ @Override
+ public void run() {
+ Set<Path> bothPaths = new HashSet<>(Arrays.asList(keystorePath,
truststorePath));
+ try {
+ boolean storeChanged = false;
+ // Can we poll the same directory for updates?
+ if (keystoreWatchService == truststoreWatchService) {
+ LOGGER.debug("Polling for keystore updates at {} and
truststore updates at {}", new Object[]{keystorePath, truststorePath});
+ storeChanged = this.poll(keystoreWatchService, bothPaths);
+ } else {
+ // Otherwise, poll separate directories
+ LOGGER.debug("Polling for keystore updates at {}", new
Object[]{keystorePath});
+ storeChanged = this.poll(keystoreWatchService,
Arrays.asList(keystorePath));
+
+ LOGGER.debug("Polling for truststore updates at {}", new
Object[]{truststorePath});
+ storeChanged |= this.poll(truststoreWatchService,
Arrays.asList(truststorePath));
+ }
+
+ if (storeChanged) {
+ if (this.isReloadAllowed()) {
+
autoLoader.getSslContextFactoryReloadable().reloadSslContextFactory();
+ } else {
+ LOGGER.warn("For security reasons, the SSL Context Factory
could not be reloaded because the " +
+ "keystore {} changed in a way that is
disallowed.", new Object[] {keystorePath});
+ }
+ }
+
+ } catch (final Throwable t) {
+ LOGGER.error("Error reloading SSL context factory due to: " +
t.getMessage(), t);
+ }
+ }
+
+ /**
+ * Returns a list representing the state of the current keystore at the
given path. This method uses the
+ * keystore properties from nifi.properties. The only state retrieved
will be the alias, subject DN,
+ * issuer DN of each PrivateKeyEntry, and issuer certificate serial number
if applicable, and the results
+ * will be a sorted list.
+ * @return A sorted list of information about each private key in the
keystore
+ * @throws TlsException If the keystore could not be loaded
+ * @throws KeyStoreException If the keystore password was incorrect
+ * @throws UnrecoverableEntryException If a private key entry could not be
recovered
+ * @throws NoSuchAlgorithmException If the default password algorithm is
not supported
Review comment:
Declaring all of these exceptions impacts several other methods, and
handling them separately doesn't provide much value. What do you think about
adjusting the signature of this and other methods to catch these checked
exceptions and wrap them in one checked exception, or perhaps a subclass of
`RuntimeException`?
##########
File path:
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/main/java/org/apache/nifi/autoload/SSLContextFactoryAutoLoaderTask.java
##########
@@ -0,0 +1,248 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.NiFiProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+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.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The runnable task that polls the WatchService for updates to the keystore
and truststore.
+ *
+ */
+public class SSLContextFactoryAutoLoaderTask implements Runnable {
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(SSLContextFactoryAutoLoaderTask.class);
+
+ private static final int MIN_FILE_AGE = 5000;
+
+ private final Path keystorePath;
+ private final Path truststorePath;
+ private final WatchService keystoreWatchService;
+ private final WatchService truststoreWatchService;
+ private final SSLContextFactoryAutoLoader autoLoader;
+ private final NiFiProperties nifiProperties;
+ private final List<File> candidateStores;
+
+ private final List<CertificateEntryDescription> existingKeystoreState;
+
+ private volatile boolean stopped = false;
+
+ private SSLContextFactoryAutoLoaderTask(final Builder builder) throws
NoSuchAlgorithmException, UnrecoverableEntryException,
+ KeyStoreException, TlsException {
+ this.keystorePath = builder.keystorePath;
+ this.truststorePath = builder.truststorePath;
+ this.keystoreWatchService = builder.keystoreWatchService;
+ this.truststoreWatchService = builder.truststoreWatchService;
+ this.autoLoader = builder.autoLoader;
+ this.nifiProperties = builder.niFiProperties;
+ this.existingKeystoreState = this.getKeystoreState();
+ this.candidateStores = new ArrayList<>();
+ }
+
+ private boolean poll(WatchService watchService, Collection<Path>
storePaths) {
+ if (storePaths == null || storePaths.isEmpty()) {
+ throw new RuntimeException("A polling directory must be
specified.");
+ }
+ WatchKey key = watchService.poll();
+
+ boolean storeChanged = false;
+
+ // Key comes back as null when there are no new create events, but we
still want to continue processing
+ // so we can consider files added to the candidateNars list in
previous iterations
+
+ if (key != null) {
+ for (WatchEvent<?> event : key.pollEvents()) {
+ final WatchEvent.Kind<?> kind = event.kind();
+ if (kind == StandardWatchEventKinds.OVERFLOW) {
+ continue;
+ }
+
+ final WatchEvent<Path> ev = (WatchEvent<Path>) event;
+ final Path filename = ev.context();
+
+ for(Path storePath : storePaths) {
Review comment:
Recommend adding spacing and `final` keyword:
```suggestion
for (final Path storePath : storePaths) {
```
##########
File path:
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java
##########
@@ -315,6 +319,15 @@ private Handler loadInitialWars(final Set<Bundle> bundles)
{
return gzip(webAppContextHandlers);
}
+ @Override
+ public void reloadSslContextFactory() throws Exception {
Review comment:
Instead of defining the new `SSLContextFactoryReloadable` and
implementing this method, why not just pass the Jetty `SslContextFactory` to
the loader class? That would reduce the impact on the JettyServer class. This
also follows the approach of the Jetty
[KeyStoreScanner](https://github.com/eclipse/jetty.project/blob/b56edf511ab4399122ea2c6162a4a5988870f479/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/KeyStoreScanner.java).
##########
File path:
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/main/java/org/apache/nifi/autoload/SSLContextFactoryAutoLoaderTask.java
##########
@@ -0,0 +1,248 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.NiFiProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+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.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The runnable task that polls the WatchService for updates to the keystore
and truststore.
+ *
+ */
+public class SSLContextFactoryAutoLoaderTask implements Runnable {
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(SSLContextFactoryAutoLoaderTask.class);
+
+ private static final int MIN_FILE_AGE = 5000;
+
+ private final Path keystorePath;
+ private final Path truststorePath;
+ private final WatchService keystoreWatchService;
+ private final WatchService truststoreWatchService;
+ private final SSLContextFactoryAutoLoader autoLoader;
+ private final NiFiProperties nifiProperties;
+ private final List<File> candidateStores;
+
+ private final List<CertificateEntryDescription> existingKeystoreState;
+
+ private volatile boolean stopped = false;
+
+ private SSLContextFactoryAutoLoaderTask(final Builder builder) throws
NoSuchAlgorithmException, UnrecoverableEntryException,
+ KeyStoreException, TlsException {
+ this.keystorePath = builder.keystorePath;
+ this.truststorePath = builder.truststorePath;
+ this.keystoreWatchService = builder.keystoreWatchService;
+ this.truststoreWatchService = builder.truststoreWatchService;
+ this.autoLoader = builder.autoLoader;
+ this.nifiProperties = builder.niFiProperties;
+ this.existingKeystoreState = this.getKeystoreState();
+ this.candidateStores = new ArrayList<>();
+ }
+
+ private boolean poll(WatchService watchService, Collection<Path>
storePaths) {
+ if (storePaths == null || storePaths.isEmpty()) {
+ throw new RuntimeException("A polling directory must be
specified.");
+ }
+ WatchKey key = watchService.poll();
+
+ boolean storeChanged = false;
+
+ // Key comes back as null when there are no new create events, but we
still want to continue processing
+ // so we can consider files added to the candidateNars list in
previous iterations
+
+ if (key != null) {
+ for (WatchEvent<?> event : key.pollEvents()) {
+ final WatchEvent.Kind<?> kind = event.kind();
+ if (kind == StandardWatchEventKinds.OVERFLOW) {
+ continue;
+ }
+
+ final WatchEvent<Path> ev = (WatchEvent<Path>) event;
+ final Path filename = ev.context();
+
+ for(Path storePath : storePaths) {
+
+ final Path autoLoadFile =
storePath.getParent().resolve(filename);
+ final String autoLoadFilename =
autoLoadFile.toFile().getName();
+
+ if
(!storePath.getFileName().toString().equals(autoLoadFilename)) {
+ continue;
+ }
+
+ LOGGER.info("Found update to {}", new
Object[]{autoLoadFilename});
+ storeChanged = true;
+ }
+ }
+
+ final boolean valid = key.reset();
+ if (!valid) {
+ LOGGER.error("{} auto-refresh directory is no longer valid",
new Object[] {storePaths.iterator().next()});
+ autoLoader.stop();
+ }
+ return storeChanged;
+ }
+ return false;
+ }
+
+ @Override
+ public void run() {
+ Set<Path> bothPaths = new HashSet<>(Arrays.asList(keystorePath,
truststorePath));
+ try {
+ boolean storeChanged = false;
+ // Can we poll the same directory for updates?
+ if (keystoreWatchService == truststoreWatchService) {
+ LOGGER.debug("Polling for keystore updates at {} and
truststore updates at {}", new Object[]{keystorePath, truststorePath});
+ storeChanged = this.poll(keystoreWatchService, bothPaths);
+ } else {
+ // Otherwise, poll separate directories
+ LOGGER.debug("Polling for keystore updates at {}", new
Object[]{keystorePath});
+ storeChanged = this.poll(keystoreWatchService,
Arrays.asList(keystorePath));
+
+ LOGGER.debug("Polling for truststore updates at {}", new
Object[]{truststorePath});
+ storeChanged |= this.poll(truststoreWatchService,
Arrays.asList(truststorePath));
+ }
+
+ if (storeChanged) {
+ if (this.isReloadAllowed()) {
+
autoLoader.getSslContextFactoryReloadable().reloadSslContextFactory();
+ } else {
+ LOGGER.warn("For security reasons, the SSL Context Factory
could not be reloaded because the " +
+ "keystore {} changed in a way that is
disallowed.", new Object[] {keystorePath});
+ }
+ }
+
+ } catch (final Throwable t) {
+ LOGGER.error("Error reloading SSL context factory due to: " +
t.getMessage(), t);
+ }
+ }
+
+ /**
+ * Returns a list representing the state of the current keystore at the
given path. This method uses the
+ * keystore properties from nifi.properties. The only state retrieved
will be the alias, subject DN,
+ * issuer DN of each PrivateKeyEntry, and issuer certificate serial number
if applicable, and the results
+ * will be a sorted list.
+ * @return A sorted list of information about each private key in the
keystore
+ * @throws TlsException If the keystore could not be loaded
+ * @throws KeyStoreException If the keystore password was incorrect
+ * @throws UnrecoverableEntryException If a private key entry could not be
recovered
+ * @throws NoSuchAlgorithmException If the default password algorithm is
not supported
+ */
+ private List<CertificateEntryDescription> getKeystoreState() throws
TlsException, KeyStoreException,
+ UnrecoverableEntryException, NoSuchAlgorithmException {
+ List<CertificateEntryDescription> state = new ArrayList<>();
+
+ KeyStore keyStore = KeyStoreUtils.loadKeyStore(keystorePath.toString(),
+
nifiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE_PASSWD).toCharArray(),
+
nifiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE_TYPE));
+
+ Enumeration<String> aliasesEnum = keyStore.aliases();
+ while(aliasesEnum.hasMoreElements()) {
+ String alias = aliasesEnum.nextElement();
+ if (keyStore.isKeyEntry(alias)) {
+ KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)
keyStore.getEntry(alias, new KeyStore.PasswordProtection(nifiProperties
+
.getProperty(NiFiProperties.SECURITY_KEY_PASSWD).toCharArray()));
+ X509Certificate cert = (X509Certificate)
entry.getCertificateChain()[0];
+ String issuerSerialNumber =
(entry.getCertificateChain().length > 1)
+ ? ((X509Certificate)
entry.getCertificateChain()[1]).getSerialNumber().toString()
+ : null;
+ state.add(new
CertificateEntryDescription(cert.getSubjectX500Principal(),
cert.getIssuerX500Principal(), alias, issuerSerialNumber));
+ }
+ }
+
+ Collections.sort(state);
+ return new ArrayList<>(state);
+ }
+
+ /**
+ * This returns false if there were any changes to the keystore other than
updating a PrivateKeyEntry with
+ * the same subject DN, issuer DN, alias, and issuer cert serial number if
applicable.
+ * @return True if a reload should be allowed, meaning the keystore has
not changed in a meaningful way
+ */
+ boolean isReloadAllowed() throws NoSuchAlgorithmException,
UnrecoverableEntryException, KeyStoreException, TlsException {
+ return existingKeystoreState.equals(this.getKeystoreState());
+ }
+
+ /**
+ * Builder for SSLContextFactoryAutoLoaderTask.
+ */
+ public static class Builder {
Review comment:
The builder pattern is useful for optional properties, but there it
looks like all of the properties should always be provided. Is there another
reason for using the builder pattern as opposed to passing everything as
constructor arguments?
##########
File path:
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/main/java/org/apache/nifi/autoload/SSLContextFactoryAutoLoader.java
##########
@@ -0,0 +1,148 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.SSLContextFactoryReloadable;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.FormatUtils;
+import org.apache.nifi.util.NiFiProperties;
+import org.apache.nifi.util.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchService;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Starts a thread to monitor the keystore and truststore configured in
nifi.properties.
+ */
+public class SSLContextFactoryAutoLoader {
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(SSLContextFactoryAutoLoader.class);
+
+ private File keystore = null;
+ private File truststore = null;
+ private final SSLContextFactoryReloadable sslContextFactoryReloadable;
+
+ private boolean isApplicable = false;
Review comment:
Perhaps changing this to `applicable` or simply `enabled` would be a bit
a cleaner.
##########
File path:
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-ssl-autoloading-utils/src/test/java/org/apache/nifi/autoload/TestSSLContextFactoryAutoLoaderTask.java
##########
@@ -0,0 +1,197 @@
+/*
+ * 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.autoload;
+
+import org.apache.nifi.SSLContextFactoryReloadable;
+import org.apache.nifi.security.util.KeyStoreUtils;
+import org.apache.nifi.security.util.StandardTlsConfiguration;
+import org.apache.nifi.security.util.TlsConfiguration;
+import org.apache.nifi.security.util.TlsException;
+import org.apache.nifi.util.NiFiProperties;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.security.GeneralSecurityException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableEntryException;
+import java.util.Arrays;
+
+public class TestSSLContextFactoryAutoLoaderTask {
+
+ private static final Path KEYSTORE = new
File("src/test/resources/keystore.jks").toPath();
+ private static final Path TRUSTSTORE = new
File("src/test/resources/truststore.jks").toPath();
+ private static final String KEYSTORE_PASSWORD = "testtesttest";
+ private static final String KEY_PASSWORD = "testtesttest";
+ private static final String KEYSTORE_TYPE = "JKS";
+
+ private static final String DEFAULT_KEY_ALIAS = "nifi-key";
+ private static final String DEFAULT_CERT_DN = "CN=localhost, OU=NIFI";
+
+ private SSLContextFactoryAutoLoaderTask task;
+ private NiFiProperties nifiProperties;
+ private SSLContextFactoryAutoLoader autoLoader;
+ private SSLContextFactoryReloadable sslContextFactoryReloadable;
+ private WatchService keystoreWatchService;
+ private WatchService truststoreWatchService;
+
+ @Before
+ public void init() throws GeneralSecurityException, IOException {
+ this.createKeystore(DEFAULT_KEY_ALIAS, DEFAULT_CERT_DN);
+
+ nifiProperties = Mockito.mock(NiFiProperties.class);
+
Mockito.when(nifiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE_PASSWD)).thenReturn(KEYSTORE_PASSWORD);
+
Mockito.when(nifiProperties.getProperty(NiFiProperties.SECURITY_KEY_PASSWD)).thenReturn(KEY_PASSWORD);
+
Mockito.when(nifiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE_TYPE)).thenReturn(KEYSTORE_TYPE);
+
+ keystoreWatchService = Mockito.mock(WatchService.class);
+ truststoreWatchService = Mockito.mock(WatchService.class);
+ autoLoader = Mockito.mock(SSLContextFactoryAutoLoader.class);
+ sslContextFactoryReloadable =
Mockito.mock(SSLContextFactoryReloadable.class);
+
Mockito.when(autoLoader.getSslContextFactoryReloadable()).thenReturn(sslContextFactoryReloadable);
+
+ task = new SSLContextFactoryAutoLoaderTask.Builder()
+ .keystorePath(KEYSTORE)
+ .keystoreWatchService(keystoreWatchService)
+ .truststorePath(TRUSTSTORE)
+ .truststoreWatchService(truststoreWatchService)
+ .autoLoader(autoLoader)
+ .nifiProperties(nifiProperties)
+ .build();
+ }
+
+ @Test
+ public void testIsReloadAllowed_true() throws IOException,
GeneralSecurityException {
+ this.createKeystore(DEFAULT_KEY_ALIAS, DEFAULT_CERT_DN); // Creates a
keystore with the same DNs and alias, so this is allowed
+ Assert.assertTrue(task.isReloadAllowed());
+ }
+
+ @Test
+ public void testIsReloadAllowed_differentAlias() throws IOException,
GeneralSecurityException {
+ this.createKeystore("different-alias", DEFAULT_CERT_DN);
+ Assert.assertFalse(task.isReloadAllowed());
+ }
+
+ @Test
+ public void testIsReloadAllowed_differentSubjectDN() throws IOException,
GeneralSecurityException {
+ this.createKeystore(DEFAULT_KEY_ALIAS, "CN=different");
+ Assert.assertFalse(task.isReloadAllowed());
+ }
+
+ @Test
+ public void testIsReloadAllowed_differentIssuerSerialNumber() throws
IOException, UnrecoverableEntryException, NoSuchAlgorithmException,
KeyStoreException, TlsException {
+ // This is a keystore with the same key alias and cert DN, but with a
different issuer cert in the cert chain,
+ // showing that we disallow cert updates with the same issuer DN but
different actual issuer serial number
+ Files.copy(new File("src/test/resources/test-keystore.jks").toPath(),
KEYSTORE, StandardCopyOption.REPLACE_EXISTING);
Review comment:
Instead of committing the `test-keystore.jks` binary, can this be
replaced with a generated keystore as used in other methods?
##########
File path:
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java
##########
@@ -315,6 +319,15 @@ private Handler loadInitialWars(final Set<Bundle> bundles)
{
return gzip(webAppContextHandlers);
}
+ @Override
+ public void reloadSslContextFactory() throws Exception {
+ if (sslContextFactory != null) {
+ this.sslContextFactory.reload(s -> {
+ logger.info("Successfully reloaded SSLContextFactory");
Review comment:
Recommend adjusting log message to follow the class name more precisely:
```suggestion
logger.info("SslContextFactory reloaded");
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
For queries about this service, please contact Infrastructure at:
[email protected]