NIFIREG-320 - Added jetty header filters to set security headers. Setting security headers for the registry-api using spring security configuration.
NIFIREG-320 - Fixed rest-api docs loading. Headers confirmed to be applied for docs.
diff --git a/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/JettyServer.java b/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/JettyServer.java
index 45619f7..387857f 100644
--- a/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/JettyServer.java
+++ b/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/JettyServer.java
@@ -17,6 +17,10 @@
 package org.apache.nifi.registry.jetty;
 
 import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.jetty.headers.ContentSecurityPolicyFilter;
+import org.apache.nifi.registry.jetty.headers.StrictTransportSecurityFilter;
+import org.apache.nifi.registry.jetty.headers.XFrameOptionsFilter;
+import org.apache.nifi.registry.jetty.headers.XSSProtectionFilter;
 import org.apache.nifi.registry.properties.NiFiRegistryProperties;
 import org.apache.nifi.registry.security.crypto.CryptoKeyProvider;
 import org.eclipse.jetty.annotations.AnnotationConfiguration;
@@ -28,11 +32,10 @@
 import org.eclipse.jetty.server.Server;
 import org.eclipse.jetty.server.ServerConnector;
 import org.eclipse.jetty.server.SslConnectionFactory;
-import org.eclipse.jetty.server.handler.ContextHandler;
 import org.eclipse.jetty.server.handler.HandlerCollection;
-import org.eclipse.jetty.server.handler.ResourceHandler;
-import org.eclipse.jetty.util.resource.Resource;
-import org.eclipse.jetty.util.resource.ResourceCollection;
+import org.eclipse.jetty.servlet.DefaultServlet;
+import org.eclipse.jetty.servlet.FilterHolder;
+import org.eclipse.jetty.servlet.ServletHolder;
 import org.eclipse.jetty.util.ssl.SslContextFactory;
 import org.eclipse.jetty.util.thread.QueuedThreadPool;
 import org.eclipse.jetty.webapp.Configuration;
@@ -42,6 +45,8 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import javax.servlet.DispatcherType;
+import javax.servlet.Filter;
 import java.io.File;
 import java.io.FileFilter;
 import java.io.IOException;
@@ -55,6 +60,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.EnumSet;
 import java.util.Enumeration;
 import java.util.HashSet;
 import java.util.LinkedList;
@@ -106,6 +112,37 @@
         }
     }
 
+    /**
+     * Returns a File object for the directory containing NIFI documentation.
+     * <p>
+     * Formerly, if the docsDirectory did not exist NIFI would fail to start
+     * with an IllegalStateException and a rather unhelpful log message.
+     * NIFI-2184 updates the process such that if the docsDirectory does not
+     * exist an attempt will be made to create the directory. If that is
+     * successful NIFI will no longer fail and will start successfully barring
+     * any other errors. The side effect of the docsDirectory not being present
+     * is that the documentation links under the 'General' portion of the help
+     * page will not be accessible, but at least the process will be running.
+     *
+     * @param docsDirectory Name of documentation directory in installation directory.
+     * @return A File object to the documentation directory; else startUpFailure called.
+     */
+    private File getDocsDir(final String docsDirectory) {
+        File docsDir;
+        try {
+            docsDir = Paths.get(docsDirectory).toRealPath().toFile();
+        } catch (IOException ex) {
+            logger.info("Directory '" + docsDirectory + "' is missing. Some documentation will be unavailable.");
+            docsDir = new File(docsDirectory).getAbsoluteFile();
+            final boolean made = docsDir.mkdirs();
+            if (!made) {
+                logger.error("Failed to create 'docs' directory!");
+                startUpFailure(new IOException(docsDir.getAbsolutePath() + " could not be created"));
+            }
+        }
+        return docsDir;
+    }
+
     private void configureConnectors() {
         // create the http configuration
         final HttpConfiguration httpConfiguration = new HttpConfiguration();
@@ -254,11 +291,11 @@
 
         final String docsContextPath = "/nifi-registry-docs";
         webDocsContext = loadWar(webDocsWar, docsContextPath);
+        addDocsServlets(webDocsContext);
 
         final HandlerCollection handlers = new HandlerCollection();
         handlers.addHandler(webUiContext);
         handlers.addHandler(webApiContext);
-        handlers.addHandler(createDocsWebApp(docsContextPath));
         handlers.addHandler(webDocsContext);
         server.setHandler(handlers);
     }
@@ -301,6 +338,15 @@
         // configure the max form size (3x the default)
         webappContext.setMaxFormContentSize(600000);
 
+        // add HTTP security headers to all responses
+        final String ALL_PATHS = "/*";
+        ArrayList<Class<? extends Filter>> filters = new ArrayList<>(Arrays.asList(XFrameOptionsFilter.class, ContentSecurityPolicyFilter.class, XSSProtectionFilter.class));
+        if(properties.isHTTPSConfigured()) {
+            filters.add(StrictTransportSecurityFilter.class);
+        }
+
+        filters.forEach( (filter) -> addFilters(filter, ALL_PATHS, webappContext));
+
         // start out assuming the system ClassLoader will be the parent, but if additional resources were specified then
         // inject a new ClassLoader in between the system and webapp ClassLoaders that contains the additional resources
         ClassLoader parentClassLoader = ClassLoader.getSystemClassLoader();
@@ -315,6 +361,12 @@
         return webappContext;
     }
 
+    private void addFilters(Class<? extends Filter> clazz, String path, WebAppContext webappContext) {
+        FilterHolder holder = new FilterHolder(clazz);
+        holder.setName(clazz.getSimpleName());
+        webappContext.addFilter(holder, path, EnumSet.allOf(DispatcherType.class));
+    }
+
     private URL[] getWebApiAdditionalClasspath() {
         final String dbDriverDir = properties.getDatabaseDriverDirectory();
 
@@ -370,51 +422,43 @@
         return resources.toArray(new URL[resources.size()]);
     }
 
-    private ContextHandler createDocsWebApp(final String contextPath) throws IOException {
-        final ResourceHandler resourceHandler = new ResourceHandler();
-        resourceHandler.setDirectoriesListed(false);
-
-        // load the docs directory
-        String docsDirectory = docsLocation;
-        if (StringUtils.isBlank(docsDirectory)) {
-            docsDirectory = "docs";
-        }
-
-        File docsDir;
+    private void addDocsServlets(WebAppContext docsContext) {
         try {
-            docsDir = Paths.get(docsDirectory).toRealPath().toFile();
-        } catch (IOException ex) {
-            logger.warn("Directory '" + docsDirectory + "' is missing. Some documentation will be unavailable.");
-            docsDir = new File(docsDirectory).getAbsoluteFile();
-            final boolean made = docsDir.mkdirs();
-            if (!made) {
-                logger.error("Failed to create 'docs' directory!");
-                startUpFailure(new IOException(docsDir.getAbsolutePath() + " could not be created"));
+            // Load the nifi-registry/docs directory
+            final File docsDir = getDocsDir(docsLocation);
+
+            // Create the servlet which will serve the static resources
+            ServletHolder defaultHolder = new ServletHolder("default", DefaultServlet.class);
+            defaultHolder.setInitParameter("dirAllowed", "false");
+
+            ServletHolder docs = new ServletHolder("docs", DefaultServlet.class);
+            docs.setInitParameter("resourceBase", docsDir.getPath());
+            docs.setInitParameter("dirAllowed", "false");
+
+            docsContext.addServlet(docs, "/html/*");
+            docsContext.addServlet(defaultHolder, "/");
+
+            // load the rest documentation
+            final File webApiDocsDir = new File(webApiContext.getTempDirectory(), "webapp/docs");
+            if (!webApiDocsDir.exists()) {
+                final boolean made = webApiDocsDir.mkdirs();
+                if (!made) {
+                    throw new RuntimeException(webApiDocsDir.getAbsolutePath() + " could not be created");
+                }
             }
+
+            ServletHolder apiDocs = new ServletHolder("apiDocs", DefaultServlet.class);
+            apiDocs.setInitParameter("resourceBase", webApiDocsDir.getPath());
+            apiDocs.setInitParameter("dirAllowed", "false");
+
+            docsContext.addServlet(apiDocs, "/rest-api/*");
+
+            logger.info("Loading documents web app with context path set to " + docsContext.getContextPath());
+
+        } catch (Exception ex) {
+            logger.error("Unhandled Exception in createDocsWebApp: " + ex.getMessage());
+            startUpFailure(ex);
         }
-
-        final Resource docsResource = Resource.newResource(docsDir);
-
-        // load the rest documentation
-        final File webApiDocsDir = new File(webApiContext.getTempDirectory(), "webapp/docs");
-        if (!webApiDocsDir.exists()) {
-            final boolean made = webApiDocsDir.mkdirs();
-            if (!made) {
-                throw new RuntimeException(webApiDocsDir.getAbsolutePath() + " could not be created");
-            }
-        }
-        final Resource webApiDocsResource = Resource.newResource(webApiDocsDir);
-
-        // create resources for both docs locations
-        final ResourceCollection resources = new ResourceCollection(docsResource, webApiDocsResource);
-        resourceHandler.setBaseResource(resources);
-
-        // create the context handler
-        final ContextHandler handler = new ContextHandler(contextPath);
-        handler.setHandler(resourceHandler);
-
-        logger.info("Loading documents web app with context path set to " + contextPath);
-        return handler;
     }
 
     public void start() {
diff --git a/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/ContentSecurityPolicyFilter.java b/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/ContentSecurityPolicyFilter.java
new file mode 100644
index 0000000..758e939
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/ContentSecurityPolicyFilter.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.registry.jetty.headers;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * A filter to apply the Content Security Policy header.
+ *
+ */
+public class ContentSecurityPolicyFilter implements Filter {
+    private static final String HEADER = "Content-Security-Policy";
+    private static final String POLICY = "frame-ancestors 'self'";
+
+    private static final Logger logger = LoggerFactory.getLogger(ContentSecurityPolicyFilter.class);
+
+    @Override
+    public void doFilter(final ServletRequest req, final ServletResponse resp, final FilterChain filterChain)
+            throws IOException, ServletException {
+
+        final HttpServletResponse response = (HttpServletResponse) resp;
+        response.setHeader(HEADER, POLICY);
+
+        filterChain.doFilter(req, resp);
+    }
+
+    @Override
+    public void init(final FilterConfig config) {
+    }
+
+    @Override
+    public void destroy() {
+    }
+}
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/StrictTransportSecurityFilter.java b/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/StrictTransportSecurityFilter.java
new file mode 100644
index 0000000..7f0f913
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/StrictTransportSecurityFilter.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.registry.jetty.headers;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * A filter to apply the HTTP Strict Transport Security (HSTS) HTTP header. This forces the browser to use HTTPS for
+ * all
+ */
+public class StrictTransportSecurityFilter implements Filter {
+    private static final String HEADER = "Strict-Transport-Security";
+    private static final String POLICY = "max-age=31540000";
+
+    private static final Logger logger = LoggerFactory.getLogger(StrictTransportSecurityFilter.class);
+
+    @Override
+    public void doFilter(final ServletRequest req, final ServletResponse resp, final FilterChain filterChain)
+            throws IOException, ServletException {
+
+        final HttpServletResponse response = (HttpServletResponse) resp;
+        response.setHeader(HEADER, POLICY);
+
+        filterChain.doFilter(req, resp);
+    }
+
+    @Override
+    public void init(final FilterConfig config) {
+    }
+
+    @Override
+    public void destroy() {
+    }
+}
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/XFrameOptionsFilter.java b/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/XFrameOptionsFilter.java
new file mode 100644
index 0000000..fad5bbc
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/XFrameOptionsFilter.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.registry.jetty.headers;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * A filter to apply the X-Frame-Options header.
+ *
+ */
+public class XFrameOptionsFilter implements Filter {
+    private static final String HEADER = "X-Frame-Options";
+    private static final String POLICY = "SAMEORIGIN";
+
+    private static final Logger logger = LoggerFactory.getLogger(XFrameOptionsFilter.class);
+
+    @Override
+    public void doFilter(final ServletRequest req, final ServletResponse resp, final FilterChain filterChain)
+            throws IOException, ServletException {
+
+        final HttpServletResponse response = (HttpServletResponse) resp;
+        response.setHeader(HEADER, POLICY);
+
+        filterChain.doFilter(req, resp);
+    }
+
+    @Override
+    public void init(final FilterConfig config) {
+    }
+
+    @Override
+    public void destroy() {
+    }
+}
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/XSSProtectionFilter.java b/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/XSSProtectionFilter.java
new file mode 100644
index 0000000..62792f1
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/XSSProtectionFilter.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.registry.jetty.headers;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * A filter to apply the Cross Site Scripting (XSS) HTTP header. Protects against reflected cross-site scripting attacks.
+ * The browser will prevent rendering of the page if an attack is detected.
+ */
+
+public class XSSProtectionFilter implements Filter {
+    private static final String HEADER = "X-XSS-Protection";
+    private static final String POLICY = "1; mode=block";
+
+    private static final Logger logger = LoggerFactory.getLogger(XSSProtectionFilter.class);
+
+    @Override
+    public void doFilter(final ServletRequest req, final ServletResponse resp, final FilterChain filterChain)
+            throws IOException, ServletException {
+
+        final HttpServletResponse response = (HttpServletResponse) resp;
+        response.setHeader(HEADER, POLICY);
+
+        filterChain.doFilter(req, resp);
+    }
+
+    @Override
+    public void init(final FilterConfig config) {
+    }
+
+    @Override
+    public void destroy() {
+    }
+}
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java
index 31133eb..e1e9a39 100644
--- a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java
@@ -118,6 +118,10 @@
         return getPropertyAsInteger(WEB_HTTPS_PORT);
     }
 
+    public boolean isHTTPSConfigured() {
+        return getSslPort() != null;
+    }
+
     public String getHttpsHost() {
         return getProperty(WEB_HTTPS_HOST);
     }
diff --git a/nifi-registry-core/nifi-registry-web-api/pom.xml b/nifi-registry-core/nifi-registry-web-api/pom.xml
index 2df5caf..e03c69d 100644
--- a/nifi-registry-core/nifi-registry-web-api/pom.xml
+++ b/nifi-registry-core/nifi-registry-web-api/pom.xml
@@ -445,5 +445,11 @@
             <version>${jetty.version}</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-jetty</artifactId>
+            <version>1.0.0-SNAPSHOT</version>
+            <scope>compile</scope>
+        </dependency>
     </dependencies>
 </project>
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java
index 20a6e0d..b792830 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java
@@ -100,6 +100,12 @@
                 .sessionManagement()
                     .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
 
+        // Apply security headers for registry API. Security headers for docs and UI are applied with Jetty filters in registry-core.
+        http.headers().xssProtection();
+        http.headers().contentSecurityPolicy("frame-ancestors 'self'");
+        http.headers().httpStrictTransportSecurity().maxAgeInSeconds(31540000);
+        http.headers().frameOptions().sameOrigin();
+
         // x509
         http.addFilterBefore(x509AuthenticationFilter(), AnonymousAuthenticationFilter.class);