Add QoSFilters first in the chain. (#12625)

* Add QoSFilters first in the chain.

When a request is suspended and later resumed due to QoS constraints,
its filter chain is restarted. Placing QoSFilters first in the chain
avoids double-execution of other filters.

Fixes an issue where requests deferred by QoS would report 403 Forbidden
due to double-execution of SecuritySanityCheckFilter.

* Smaller changes.

* Add QoS filters in BaseJettyTest.

* Remove unused parameter.
This commit is contained in:
Gian Merlino 2022-06-14 13:37:00 -07:00 committed by GitHub
parent ceb4ace118
commit 1f6e888472
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 32 additions and 20 deletions

View File

@ -22,7 +22,6 @@ package org.apache.druid.server.initialization.jetty;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.inject.Binder; import com.google.inject.Binder;
import com.google.inject.multibindings.Multibinder; import com.google.inject.multibindings.Multibinder;
import org.apache.druid.java.util.common.logger.Logger;
import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.servlets.QoSFilter; import org.eclipse.jetty.servlets.QoSFilter;
@ -33,22 +32,14 @@ import java.util.Map;
public class JettyBindings public class JettyBindings
{ {
private static final Logger log = new Logger(JettyBindings.class);
private JettyBindings() private JettyBindings()
{ {
// No instantiation. // No instantiation.
} }
public static void addQosFilter(Binder binder, String paths, int maxRequests) public static void addQosFilter(Binder binder, String path, int maxRequests)
{ {
if (maxRequests <= 0) { addQosFilter(binder, new String[]{path}, maxRequests);
return;
}
Multibinder.newSetBinder(binder, ServletFilterHolder.class)
.addBinding()
.toInstance(new QosFilterHolder(new String[]{paths}, maxRequests));
} }
public static void addQosFilter(Binder binder, String[] paths, int maxRequests) public static void addQosFilter(Binder binder, String[] paths, int maxRequests)
@ -57,7 +48,7 @@ public class JettyBindings
return; return;
} }
Multibinder.newSetBinder(binder, ServletFilterHolder.class) Multibinder.newSetBinder(binder, QosFilterHolder.class)
.addBinding() .addBinding()
.toInstance(new QosFilterHolder(paths, maxRequests)); .toInstance(new QosFilterHolder(paths, maxRequests));
} }
@ -69,7 +60,7 @@ public class JettyBindings
.to(handlerClass); .to(handlerClass);
} }
private static class QosFilterHolder implements ServletFilterHolder public static class QosFilterHolder implements ServletFilterHolder
{ {
private final String[] paths; private final String[] paths;
private final int maxRequests; private final int maxRequests;

View File

@ -54,11 +54,28 @@ public class JettyServerInitUtils
return gzipHandler; return gzipHandler;
} }
/**
* Add any filters that were registered with {@link JettyBindings#addQosFilter}. These must be added first in
* the filter chain, because when a request is suspended and later resumed due to QoS constraints, its filter
* chain is restarted. Placing QoSFilters first in the chain avoids double-execution of other filters.
*/
public static void addQosFilters(ServletContextHandler handler, Injector injector)
{
final Set<JettyBindings.QosFilterHolder> filters =
injector.getInstance(Key.get(new TypeLiteral<Set<JettyBindings.QosFilterHolder>>() {}));
addFilters(handler, filters);
}
public static void addExtensionFilters(ServletContextHandler handler, Injector injector) public static void addExtensionFilters(ServletContextHandler handler, Injector injector)
{ {
Set<ServletFilterHolder> extensionFilters = injector.getInstance(Key.get(new TypeLiteral<Set<ServletFilterHolder>>(){})); final Set<ServletFilterHolder> filters =
injector.getInstance(Key.get(new TypeLiteral<Set<ServletFilterHolder>>() {}));
addFilters(handler, filters);
}
for (ServletFilterHolder servletFilterHolder : extensionFilters) { public static void addFilters(ServletContextHandler handler, Set<? extends ServletFilterHolder> filterHolders)
{
for (ServletFilterHolder servletFilterHolder : filterHolders) {
// Check the Filter first to guard against people who don't read the docs and return the Class even // Check the Filter first to guard against people who don't read the docs and return the Class even
// when they have an instance. // when they have an instance.
FilterHolder holder; FilterHolder holder;

View File

@ -133,6 +133,7 @@ public class JettyServerModule extends JerseyServletModule
// Add empty binding for Handlers so that the injector returns an empty set if none are provided by extensions. // Add empty binding for Handlers so that the injector returns an empty set if none are provided by extensions.
Multibinder.newSetBinder(binder, Handler.class); Multibinder.newSetBinder(binder, Handler.class);
Multibinder.newSetBinder(binder, JettyBindings.QosFilterHolder.class);
Multibinder.newSetBinder(binder, ServletFilterHolder.class) Multibinder.newSetBinder(binder, ServletFilterHolder.class)
.addBinding() .addBinding()
.to(StandardResponseHeaderFilterHolder.class); .to(StandardResponseHeaderFilterHolder.class);

View File

@ -150,6 +150,7 @@ public abstract class BaseJettyTest
{ {
final ServletContextHandler root = new ServletContextHandler(ServletContextHandler.SESSIONS); final ServletContextHandler root = new ServletContextHandler(ServletContextHandler.SESSIONS);
root.addServlet(new ServletHolder(new DefaultServlet()), "/*"); root.addServlet(new ServletHolder(new DefaultServlet()), "/*");
JettyServerInitUtils.addQosFilters(root, injector);
JettyServerInitUtils.addExtensionFilters(root, injector); JettyServerInitUtils.addExtensionFilters(root, injector);
root.addFilter(GuiceFilter.class, "/*", null); root.addFilter(GuiceFilter.class, "/*", null);

View File

@ -396,6 +396,7 @@ public class CliOverlord extends ServerRunnable
final ObjectMapper jsonMapper = injector.getInstance(Key.get(ObjectMapper.class, Json.class)); final ObjectMapper jsonMapper = injector.getInstance(Key.get(ObjectMapper.class, Json.class));
final AuthenticatorMapper authenticatorMapper = injector.getInstance(AuthenticatorMapper.class); final AuthenticatorMapper authenticatorMapper = injector.getInstance(AuthenticatorMapper.class);
JettyServerInitUtils.addQosFilters(root, injector);
AuthenticationUtils.addSecuritySanityCheckFilter(root, jsonMapper); AuthenticationUtils.addSecuritySanityCheckFilter(root, jsonMapper);
// perform no-op authorization/authentication for these resources // perform no-op authorization/authentication for these resources

View File

@ -26,7 +26,6 @@ import com.google.inject.Injector;
import com.google.inject.Key; import com.google.inject.Key;
import com.google.inject.servlet.GuiceFilter; import com.google.inject.servlet.GuiceFilter;
import org.apache.druid.guice.annotations.Json; import org.apache.druid.guice.annotations.Json;
import org.apache.druid.server.coordinator.DruidCoordinatorConfig;
import org.apache.druid.server.http.OverlordProxyServlet; import org.apache.druid.server.http.OverlordProxyServlet;
import org.apache.druid.server.http.RedirectFilter; import org.apache.druid.server.http.RedirectFilter;
import org.apache.druid.server.initialization.ServerConfig; import org.apache.druid.server.initialization.ServerConfig;
@ -51,21 +50,19 @@ import java.util.Properties;
*/ */
class CoordinatorJettyServerInitializer implements JettyServerInitializer class CoordinatorJettyServerInitializer implements JettyServerInitializer
{ {
private static List<String> UNSECURED_PATHS = ImmutableList.of( private static final List<String> UNSECURED_PATHS = ImmutableList.of(
"/coordinator/false", "/coordinator/false",
"/overlord/false", "/overlord/false",
"/status/health", "/status/health",
"/druid/coordinator/v1/isLeader" "/druid/coordinator/v1/isLeader"
); );
private final DruidCoordinatorConfig config;
private final boolean beOverlord; private final boolean beOverlord;
private final ServerConfig serverConfig; private final ServerConfig serverConfig;
@Inject @Inject
CoordinatorJettyServerInitializer(DruidCoordinatorConfig config, Properties properties, ServerConfig serverConfig) CoordinatorJettyServerInitializer(Properties properties, ServerConfig serverConfig)
{ {
this.config = config;
this.beOverlord = CliCoordinator.isOverlord(properties); this.beOverlord = CliCoordinator.isOverlord(properties);
this.serverConfig = serverConfig; this.serverConfig = serverConfig;
} }
@ -84,6 +81,7 @@ class CoordinatorJettyServerInitializer implements JettyServerInitializer
final ObjectMapper jsonMapper = injector.getInstance(Key.get(ObjectMapper.class, Json.class)); final ObjectMapper jsonMapper = injector.getInstance(Key.get(ObjectMapper.class, Json.class));
final AuthenticatorMapper authenticatorMapper = injector.getInstance(AuthenticatorMapper.class); final AuthenticatorMapper authenticatorMapper = injector.getInstance(AuthenticatorMapper.class);
JettyServerInitUtils.addQosFilters(root, injector);
AuthenticationUtils.addSecuritySanityCheckFilter(root, jsonMapper); AuthenticationUtils.addSecuritySanityCheckFilter(root, jsonMapper);
// perform no-op authorization/authentication for these resources // perform no-op authorization/authentication for these resources

View File

@ -72,6 +72,7 @@ class MiddleManagerJettyServerInitializer implements JettyServerInitializer
final ObjectMapper jsonMapper = injector.getInstance(Key.get(ObjectMapper.class, Json.class)); final ObjectMapper jsonMapper = injector.getInstance(Key.get(ObjectMapper.class, Json.class));
final AuthenticatorMapper authenticatorMapper = injector.getInstance(AuthenticatorMapper.class); final AuthenticatorMapper authenticatorMapper = injector.getInstance(AuthenticatorMapper.class);
JettyServerInitUtils.addQosFilters(root, injector);
AuthenticationUtils.addSecuritySanityCheckFilter(root, jsonMapper); AuthenticationUtils.addSecuritySanityCheckFilter(root, jsonMapper);
// perform no-op authorization/authentication for these resources // perform no-op authorization/authentication for these resources

View File

@ -98,6 +98,7 @@ public class QueryJettyServerInitializer implements JettyServerInitializer
final ObjectMapper jsonMapper = injector.getInstance(Key.get(ObjectMapper.class, Json.class)); final ObjectMapper jsonMapper = injector.getInstance(Key.get(ObjectMapper.class, Json.class));
final AuthenticatorMapper authenticatorMapper = injector.getInstance(AuthenticatorMapper.class); final AuthenticatorMapper authenticatorMapper = injector.getInstance(AuthenticatorMapper.class);
JettyServerInitUtils.addQosFilters(root, injector);
AuthenticationUtils.addSecuritySanityCheckFilter(root, jsonMapper); AuthenticationUtils.addSecuritySanityCheckFilter(root, jsonMapper);
// perform no-op authorization for these resources // perform no-op authorization for these resources

View File

@ -118,6 +118,7 @@ public class RouterJettyServerInitializer implements JettyServerInitializer
final ObjectMapper jsonMapper = injector.getInstance(Key.get(ObjectMapper.class, Json.class)); final ObjectMapper jsonMapper = injector.getInstance(Key.get(ObjectMapper.class, Json.class));
final AuthenticatorMapper authenticatorMapper = injector.getInstance(AuthenticatorMapper.class); final AuthenticatorMapper authenticatorMapper = injector.getInstance(AuthenticatorMapper.class);
JettyServerInitUtils.addQosFilters(root, injector);
AuthenticationUtils.addSecuritySanityCheckFilter(root, jsonMapper); AuthenticationUtils.addSecuritySanityCheckFilter(root, jsonMapper);
// perform no-op authorization/authentication for these resources // perform no-op authorization/authentication for these resources