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);