gresockj commented on a change in pull request #4991:
URL: https://github.com/apache/nifi/pull/4991#discussion_r613155455



##########
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,270 @@
+/*
+ * 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;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 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 long pollIntervalMillis;
+    private final SSLContextFactoryAutoLoader autoLoader;
+    private final NiFiProperties nifiProperties;
+    private final List<File> candidateStores;
+
+    private final List<PrivateKeyInfo> 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.pollIntervalMillis = builder.pollIntervalMillis;
+        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;
+        try {
+            key = watchService.poll(pollIntervalMillis, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException x) {
+            LOGGER.info("WatchService interrupted, returning...");
+            return false;
+        }
+
+        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()});
+                stop();
+            }
+            return storeChanged;
+        }
+        return false;
+    }
+
+    @Override
+    public void run() {
+        Set<Path> bothPaths = new HashSet<>(Arrays.asList(keystorePath, 
truststorePath));
+        while (!stopped) {
+            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<PrivateKeyInfo> getKeystoreState() throws TlsException, 
KeyStoreException,
+            UnrecoverableEntryException, NoSuchAlgorithmException {
+        List<PrivateKeyInfo> 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 PrivateKeyInfo(cert.getSubjectDN().getName(), 
cert.getIssuerDN().getName(), 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());
+    }
+
+    public void stop() {
+        LOGGER.info("Stopping SSL Context Factory Auto-loader");
+        stopped = true;
+    }

Review comment:
       I like it, and this enabled me to write some additional unit tests.




-- 
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:
us...@infra.apache.org


Reply via email to