NIFI-8782 Added Rate-Limiting for Access Token Requests
- Added Jetty DoSFilter configured for /access/token
- Added nifi.web.max.access.token.requests.per.second property with default value of 25
Signed-off-by: Nathan Gough <thenatog@gmail.com>
This closes #5215.
diff --git a/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java b/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java
index 91b9f73..a7bda62 100644
--- a/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java
+++ b/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java
@@ -223,6 +223,7 @@
public static final String WEB_PROXY_HOST = "nifi.web.proxy.host";
public static final String WEB_MAX_CONTENT_SIZE = "nifi.web.max.content.size";
public static final String WEB_MAX_REQUESTS_PER_SECOND = "nifi.web.max.requests.per.second";
+ public static final String WEB_MAX_ACCESS_TOKEN_REQUESTS_PER_SECOND = "nifi.web.max.access.token.requests.per.second";
public static final String WEB_REQUEST_TIMEOUT = "nifi.web.request.timeout";
public static final String WEB_REQUEST_IP_WHITELIST = "nifi.web.request.ip.whitelist";
public static final String WEB_SHOULD_SEND_SERVER_VERSION = "nifi.web.should.send.server.version";
@@ -312,7 +313,8 @@
public static final String DEFAULT_WEB_MAX_HEADER_SIZE = "16 KB";
public static final String DEFAULT_WEB_WORKING_DIR = "./work/jetty";
public static final String DEFAULT_WEB_MAX_CONTENT_SIZE = "20 MB";
- public static final String DEFAULT_WEB_MAX_REQUESTS_PER_SECOND = "30000";
+ public static final int DEFAULT_WEB_MAX_REQUESTS_PER_SECOND = 30000;
+ public static final int DEFAULT_WEB_MAX_ACCESS_TOKEN_REQUESTS_PER_SECOND = 25;
public static final String DEFAULT_WEB_REQUEST_TIMEOUT = "60 secs";
public static final String DEFAULT_NAR_WORKING_DIR = "./work/nar";
public static final String DEFAULT_COMPONENT_DOCS_DIRECTORY = "./work/docs/components";
@@ -673,8 +675,12 @@
return getProperty(WEB_MAX_CONTENT_SIZE);
}
- public String getMaxWebRequestsPerSecond() {
- return getProperty(WEB_MAX_REQUESTS_PER_SECOND, DEFAULT_WEB_MAX_REQUESTS_PER_SECOND);
+ public Integer getMaxWebRequestsPerSecond() {
+ return getIntegerProperty(WEB_MAX_REQUESTS_PER_SECOND, DEFAULT_WEB_MAX_REQUESTS_PER_SECOND);
+ }
+
+ public Integer getMaxWebAccessTokenRequestsPerSecond() {
+ return getIntegerProperty(WEB_MAX_ACCESS_TOKEN_REQUESTS_PER_SECOND, DEFAULT_WEB_MAX_ACCESS_TOKEN_REQUESTS_PER_SECOND);
}
public String getWebRequestTimeout() {
diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc b/nifi-docs/src/main/asciidoc/administration-guide.adoc
index 38e5765..fa6d859 100644
--- a/nifi-docs/src/main/asciidoc/administration-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc
@@ -3599,6 +3599,7 @@
blank meaning all requests containing a proxy context path are rejected. Configuring this property would allow requests where the proxy path is contained in this listing.
|`nifi.web.max.content.size`|The maximum size (HTTP `Content-Length`) for PUT and POST requests. No default value is set for backward compatibility. Providing a value for this property enables the `Content-Length` filter on all incoming API requests (except Site-to-Site and cluster communications). A suggested value is `20 MB`.
|`nifi.web.max.requests.per.second`|The maximum number of requests from a connection per second. Requests in excess of this are first delayed, then throttled.
+|`nifi.web.max.access.token.requests.per.second`|The maximum number of requests for login Access Tokens from a connection per second. Requests in excess of this are rejected with HTTP 429.
|`nifi.web.request.ip.whitelist`|A comma separated list of IP addresses. Used to specify the IP addresses of clients which can exceed the maximum requests per second (`nifi.web.max.requests.per.second`). Does not apply to web request timeout.
|`nifi.web.request.timeout`|The request timeout for web requests. Requests running longer than this time will be forced to end with a HTTP 503 Service Unavailable response. Default value is `60 secs`.
|====
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/pom.xml
index 15cf537..6f43822 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/pom.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/pom.xml
@@ -145,6 +145,7 @@
<nifi.web.proxy.host />
<nifi.web.max.content.size />
<nifi.web.max.requests.per.second>30000</nifi.web.max.requests.per.second>
+ <nifi.web.max.access.token.requests.per.second>25</nifi.web.max.access.token.requests.per.second>
<nifi.web.request.timeout>60 secs</nifi.web.request.timeout>
<nifi.web.request.ip.whitelist />
<nifi.web.should.send.server.version>true</nifi.web.should.send.server.version>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/nifi.properties b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/nifi.properties
index cf0c531..b5d9a6a 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/nifi.properties
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/nifi.properties
@@ -166,6 +166,7 @@
nifi.web.proxy.host=${nifi.web.proxy.host}
nifi.web.max.content.size=${nifi.web.max.content.size}
nifi.web.max.requests.per.second=${nifi.web.max.requests.per.second}
+nifi.web.max.access.token.requests.per.second=${nifi.web.max.access.token.requests.per.second}
nifi.web.request.timeout=${nifi.web.request.timeout}
nifi.web.request.ip.whitelist=${nifi.web.request.ip.whitelist}
nifi.web.should.send.server.version=${nifi.web.should.send.server.version}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java
index 5beb7b5..c70f6b9 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java
@@ -136,6 +136,16 @@
private static final String CONTAINER_INCLUDE_PATTERN_KEY = "org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern";
private static final String CONTAINER_INCLUDE_PATTERN_VALUE = ".*/[^/]*servlet-api-[^/]*\\.jar$|.*/javax.servlet.jsp.jstl-.*\\\\.jar$|.*/[^/]*taglibs.*\\.jar$";
+ private static final String CONTEXT_PATH_ALL = "/*";
+ private static final String CONTEXT_PATH_ROOT = "/";
+ private static final String CONTEXT_PATH_NIFI = "/nifi";
+ private static final String CONTEXT_PATH_NIFI_API = "/nifi-api";
+ private static final String CONTEXT_PATH_NIFI_CONTENT_VIEWER = "/nifi-content-viewer";
+ private static final String CONTEXT_PATH_NIFI_DOCS = "/nifi-docs";
+ private static final String RELATIVE_PATH_ACCESS_TOKEN = "/access/token";
+
+ private static final int DOS_FILTER_REJECT_REQUEST = -1;
+
private static final FileFilter WAR_FILTER = pathname -> {
final String nameToTest = pathname.getName().toLowerCase();
return nameToTest.endsWith(".war") && pathname.isFile();
@@ -197,8 +207,8 @@
// Only restrict the host header if running in HTTPS mode
if (props.isHTTPSConfigured()) {
// Create a handler for the host header and add it to the server
- HostHeaderHandler hostHeaderHandler = new HostHeaderHandler(props);
- logger.info("Created HostHeaderHandler [" + hostHeaderHandler.toString() + "]");
+ final HostHeaderHandler hostHeaderHandler = new HostHeaderHandler(props);
+ logger.info("Created HostHeaderHandler [{}}]", hostHeaderHandler);
// Add this before the WAR handlers
allHandlers.addHandler(hostHeaderHandler);
@@ -283,7 +293,7 @@
final ClassLoader frameworkClassLoader = getClass().getClassLoader();
// load the web ui app
- final WebAppContext webUiContext = loadWar(webUiWar, "/nifi", frameworkClassLoader);
+ final WebAppContext webUiContext = loadWar(webUiWar, CONTEXT_PATH_NIFI, frameworkClassLoader);
webUiContext.getInitParams().put("oidc-supported", String.valueOf(props.isOidcEnabled()));
webUiContext.getInitParams().put("knox-supported", String.valueOf(props.isKnoxSsoEnabled()));
webUiContext.getInitParams().put("saml-supported", String.valueOf(props.isSamlEnabled()));
@@ -292,19 +302,16 @@
webAppContextHandlers.addHandler(webUiContext);
// load the web api app
- webApiContext = loadWar(webApiWar, "/nifi-api", frameworkClassLoader);
+ webApiContext = loadWar(webApiWar, CONTEXT_PATH_NIFI_API, frameworkClassLoader);
webAppContextHandlers.addHandler(webApiContext);
// load the content viewer app
- webContentViewerContext = loadWar(webContentViewerWar, "/nifi-content-viewer", frameworkClassLoader);
+ webContentViewerContext = loadWar(webContentViewerWar, CONTEXT_PATH_NIFI_CONTENT_VIEWER, frameworkClassLoader);
webContentViewerContext.getInitParams().putAll(extensionUiInfo.getMimeMappings());
webAppContextHandlers.addHandler(webContentViewerContext);
- // create a web app for the docs
- final String docsContextPath = "/nifi-docs";
-
// load the documentation war
- webDocsContext = loadWar(webDocsWar, docsContextPath, frameworkClassLoader);
+ webDocsContext = loadWar(webDocsWar, CONTEXT_PATH_NIFI_DOCS, frameworkClassLoader);
// add the servlets which serve the HTML documentation within the documentation web app
addDocsServlets(webDocsContext);
@@ -312,7 +319,7 @@
webAppContextHandlers.addHandler(webDocsContext);
// load the web error app
- final WebAppContext webErrorContext = loadWar(webErrorWar, "/", frameworkClassLoader);
+ final WebAppContext webErrorContext = loadWar(webErrorWar, CONTEXT_PATH_ROOT, frameworkClassLoader);
webErrorContext.getInitParams().put("allowedContextPaths", props.getAllowedContextPaths());
webAppContextHandlers.addHandler(webErrorContext);
@@ -617,7 +624,6 @@
// add HTTP security headers to all responses
// TODO: Allow more granular path configuration (e.g. /nifi-api/site-to-site/ vs. /nifi-api/process-groups)
- final String ALL_PATHS = "/*";
ArrayList<Class<? extends Filter>> filters =
new ArrayList<>(Arrays.asList(
XFrameOptionsFilter.class,
@@ -628,8 +634,12 @@
if(props.isHTTPSConfigured()) {
filters.add(StrictTransportSecurityFilter.class);
}
- filters.forEach((filter) -> addFilters(filter, ALL_PATHS, webappContext));
- addDenialOfServiceFilters(ALL_PATHS, webappContext, props);
+ filters.forEach((filter) -> addFilters(filter, webappContext));
+ addDenialOfServiceFilters(webappContext, props);
+
+ if (CONTEXT_PATH_NIFI_API.equals(contextPath)) {
+ addAccessTokenRequestFilter(webappContext, props);
+ }
try {
// configure the class loader - webappClassLoader -> jetty nar -> web app's nar -> ...
@@ -642,10 +652,10 @@
return webappContext;
}
- private void addFilters(Class<? extends Filter> clazz, String path, WebAppContext webappContext) {
+ private void addFilters(Class<? extends Filter> clazz, WebAppContext webappContext) {
FilterHolder holder = new FilterHolder(clazz);
holder.setName(clazz.getSimpleName());
- webappContext.addFilter(holder, path, EnumSet.allOf(DispatcherType.class));
+ webappContext.addFilter(holder, CONTEXT_PATH_ALL, EnumSet.allOf(DispatcherType.class));
}
private void addDocsServlets(WebAppContext docsContext) {
@@ -695,40 +705,23 @@
* Currently, this implementation adds
* {@link org.eclipse.jetty.servlets.DoSFilter} and {@link ContentLengthFilter} filters.
*
- * @param path path spec for filters ({@code /*} by convention in this class)
* @param webAppContext context to which filters will be added
* @param props the {@link NiFiProperties}
*/
- private static void addDenialOfServiceFilters(String path, WebAppContext webAppContext, NiFiProperties props) {
- // Add the requests rate limiting filter to all requests
- final int maxWebRequestsPerSecond = determineMaxWebRequestsPerSecond(props);
- final long requestTimeoutInMilliseconds = determineRequestTimeoutInMilliseconds(props);
- final String ipWhitelist = props.getWebRequestIpWhitelist();
- addWebRequestLimitingFilter(path, webAppContext, maxWebRequestsPerSecond, ipWhitelist, requestTimeoutInMilliseconds);
+ private static void addDenialOfServiceFilters(final WebAppContext webAppContext, final NiFiProperties props) {
+ addWebRequestLimitingFilter(webAppContext, props.getMaxWebRequestsPerSecond(), getWebRequestTimeoutMs(props), props.getWebRequestIpWhitelist());
// Only add the ContentLengthFilter if the property is explicitly set (empty by default)
- int maxRequestSize = determineMaxRequestSize(props);
+ final int maxRequestSize = determineMaxRequestSize(props);
if (maxRequestSize > 0) {
- addContentLengthFilter(path, webAppContext, maxRequestSize);
+ addContentLengthFilter(webAppContext, maxRequestSize);
} else {
logger.debug("Not adding content-length filter because {} is not set in nifi.properties", NiFiProperties.WEB_MAX_CONTENT_SIZE);
}
}
- private static int determineMaxWebRequestsPerSecond(final NiFiProperties props) {
- int defaultMaxRequestsPerSecond = Integer.parseInt(NiFiProperties.DEFAULT_WEB_MAX_REQUESTS_PER_SECOND);
- int configuredMaxRequestsPerSecond = 0;
- try {
- configuredMaxRequestsPerSecond = Integer.parseInt(props.getMaxWebRequestsPerSecond());
- } catch (final NumberFormatException e) {
- logger.warn("Exception parsing property [{}]; using default value: [{}]", NiFiProperties.WEB_MAX_REQUESTS_PER_SECOND, defaultMaxRequestsPerSecond);
- }
-
- return configuredMaxRequestsPerSecond > 0 ? configuredMaxRequestsPerSecond : defaultMaxRequestsPerSecond;
- }
-
- private static long determineRequestTimeoutInMilliseconds(final NiFiProperties props) {
- long defaultRequestTimeout = Math.round(FormatUtils.getPreciseTimeDuration(NiFiProperties.DEFAULT_WEB_REQUEST_TIMEOUT, TimeUnit.MILLISECONDS));
+ private static long getWebRequestTimeoutMs(final NiFiProperties props) {
+ final long defaultRequestTimeout = Math.round(FormatUtils.getPreciseTimeDuration(NiFiProperties.DEFAULT_WEB_REQUEST_TIMEOUT, TimeUnit.MILLISECONDS));
long configuredRequestTimeout = 0L;
try {
configuredRequestTimeout = Math.round(FormatUtils.getPreciseTimeDuration(props.getWebRequestTimeout(), TimeUnit.MILLISECONDS));
@@ -744,24 +737,41 @@
* In order to allow clients to make more requests than the maximum rate, clients can be added to the {@code ipWhitelist}.
* The {@code requestTimeoutInMilliseconds} value limits requests to the given request timeout amount, and will close connections that run longer than this time.
*
- * @param path the path to apply this filter
- * @param webAppContext the context to apply this filter
- * @param maxWebRequestsPerSecond the maximum number of allowed requests per second
- * @param ipWhitelist a whitelist of IP addresses that should not be rate limited. Does not apply to request timeout
- * @param requestTimeoutInMilliseconds the amount of time before a connection will be automatically closed
+ * @param webAppContext Web Application Context where Filter will be added
+ * @param maxRequestsPerSec Maximum number of allowed requests per second
+ * @param maxRequestMs Maximum amount of time in milliseconds before a connection will be automatically closed
+ * @param allowed Comma-separated string of IP addresses that should not be rate limited. Does not apply to request timeout
*/
- private static void addWebRequestLimitingFilter(String path, WebAppContext webAppContext, int maxWebRequestsPerSecond, final String ipWhitelist, long requestTimeoutInMilliseconds) {
- FilterHolder holder = new FilterHolder(DoSFilter.class);
+ private static void addWebRequestLimitingFilter(final WebAppContext webAppContext, final int maxRequestsPerSec, final long maxRequestMs, final String allowed) {
+ final FilterHolder holder = new FilterHolder(DoSFilter.class);
holder.setInitParameters(new HashMap<String, String>() {{
- put("maxRequestsPerSec", String.valueOf(maxWebRequestsPerSecond));
- put("maxRequestMs", String.valueOf(requestTimeoutInMilliseconds));
- put("ipWhitelist", String.valueOf(ipWhitelist));
+ put("maxRequestsPerSec", Integer.toString(maxRequestsPerSec));
+ put("maxRequestMs", Long.toString(maxRequestMs));
+ put("ipWhitelist", allowed);
}});
holder.setName(DoSFilter.class.getSimpleName());
- logger.debug("Adding DoSFilter to context at path: [{}] with max req/sec: [{}], request timeout: [{}] ms. Whitelisted IPs not subject to filter: [{}]",
- path, maxWebRequestsPerSecond, requestTimeoutInMilliseconds, StringUtils.defaultIfEmpty(ipWhitelist, "none"));
- webAppContext.addFilter(holder, path, EnumSet.allOf(DispatcherType.class));
+ webAppContext.addFilter(holder, CONTEXT_PATH_ALL, EnumSet.allOf(DispatcherType.class));
+ logger.debug("Added DoSFilter Path [{}] Max Requests Per Second [{}] Request Timeout [{} ms] Allowed [{}]", CONTEXT_PATH_ALL, maxRequestsPerSec, maxRequestMs, allowed);
+ }
+
+ private static void addAccessTokenRequestFilter(final WebAppContext webAppContext, final NiFiProperties properties) {
+ final int maxRequestsPerSec = properties.getMaxWebAccessTokenRequestsPerSecond();
+ final long maxRequestMs = getWebRequestTimeoutMs(properties);
+
+ final String webRequestAllowed = properties.getWebRequestIpWhitelist();
+ final FilterHolder holder = new FilterHolder(DoSFilter.class);
+ holder.setInitParameters(new HashMap<String, String>() {{
+ put("maxRequestsPerSec", Integer.toString(maxRequestsPerSec));
+ put("maxRequestMs", Long.toString(maxRequestMs));
+ put("ipWhitelist", webRequestAllowed);
+ put("maxWaitMs", Integer.toString(DOS_FILTER_REJECT_REQUEST));
+ put("delayMs", Integer.toString(DOS_FILTER_REJECT_REQUEST));
+ }});
+ holder.setName("AccessTokenRequest-DoSFilter");
+
+ webAppContext.addFilter(holder, RELATIVE_PATH_ACCESS_TOKEN, EnumSet.allOf(DispatcherType.class));
+ logger.debug("Added DoSFilter Path [{}] Max Requests Per Second [{}] Request Timeout [{} ms] Allowed [{}]", RELATIVE_PATH_ACCESS_TOKEN, maxRequestsPerSec, maxRequestMs, webRequestAllowed);
}
private static int determineMaxRequestSize(NiFiProperties props) {
@@ -782,14 +792,14 @@
return -1;
}
- private static void addContentLengthFilter(String path, WebAppContext webAppContext, int maxContentLength) {
- FilterHolder holder = new FilterHolder(ContentLengthFilter.class);
+ private static void addContentLengthFilter(final WebAppContext webAppContext, int maxContentLength) {
+ final FilterHolder holder = new FilterHolder(ContentLengthFilter.class);
holder.setInitParameters(new HashMap<String, String>() {{
put("maxContentLength", String.valueOf(maxContentLength));
}});
holder.setName(ContentLengthFilter.class.getSimpleName());
- logger.debug("Adding ContentLengthFilter to context at path: " + path + " with max request size: " + maxContentLength + "B");
- webAppContext.addFilter(holder, path, EnumSet.allOf(DispatcherType.class));
+ logger.debug("Adding ContentLengthFilter to Path [{}] with Maximum Content Length [{}B]", CONTEXT_PATH_ALL, maxContentLength);
+ webAppContext.addFilter(holder, CONTEXT_PATH_ALL, EnumSet.allOf(DispatcherType.class));
}
/**
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/groovy/org/apache/nifi/web/server/JettyServerGroovyTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/groovy/org/apache/nifi/web/server/JettyServerGroovyTest.groovy
index dc296a7..fe688ab 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/groovy/org/apache/nifi/web/server/JettyServerGroovyTest.groovy
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/groovy/org/apache/nifi/web/server/JettyServerGroovyTest.groovy
@@ -38,9 +38,7 @@
import org.eclipse.jetty.util.ssl.SslContextFactory
import org.eclipse.jetty.webapp.WebAppContext
import org.junit.After
-import org.junit.AfterClass
import org.junit.Assume
-import org.junit.Before
import org.junit.BeforeClass
import org.junit.Rule
import org.junit.Test
@@ -116,16 +114,6 @@
TestAppender.reset()
}
- @AfterClass
- static void tearDownOnce() throws Exception {
-
- }
-
- @Before
- void setUp() throws Exception {
-
- }
-
@After
void tearDown() throws Exception {
// Cleans up the EMH so it can be reinitialized when a new Jetty server starts
@@ -483,12 +471,10 @@
JettyServer jettyServer = new JettyServer(new Server(), mockProps)
logger.info("Created JettyServer: ${jettyServer.dump()}")
- String path = "/mock"
-
final int MAX_CONTENT_LENGTH_BYTES = DataUnit.parseDataSize(defaultProps[NiFiProperties.WEB_MAX_CONTENT_SIZE], DataUnit.B).intValue()
// Act
- jettyServer.addDenialOfServiceFilters(path, mockWebContext, mockProps)
+ jettyServer.addDenialOfServiceFilters(mockWebContext, mockProps)
// Assert
assert filters.size() == 2
@@ -526,10 +512,8 @@
JettyServer jettyServer = new JettyServer(new Server(), mockProps)
logger.info("Created JettyServer: ${jettyServer.dump()}")
- String path = "/mock"
-
// Act
- jettyServer.addDenialOfServiceFilters(path, mockWebContext, mockProps)
+ jettyServer.addDenialOfServiceFilters(mockWebContext, mockProps)
// Assert
assert filters.size() == 1