Repository navigation
Add new config to filter internal Druid-related messages from Query API response - #11711
Conversation
clintropolis
left a comment
There was a problem hiding this comment.
concept makes sense, have some questions/suggestions about longer term stuff;
this will need docs i think too once configs settle
| } | ||
|
|
||
| @VisibleForTesting | ||
| String applyErrorMessageFilter(final String errorMessage, final List<Pattern> whitelistRegex) |
There was a problem hiding this comment.
Maybe this should be a utility method on QueryException or someplace similar so it can be better shared for when JDBC and native JSON queries are added (also a few other places i see this .stream().noneMatch(pattern -> pattern.matcher(errorMessage).matches())) pattern in this PR that could probably be shared). If you imagine this will only ever be done to queries, maybe it makes sense to just bake method into QueryException so that all implementations just know how to trim the information they contain, then everything just has to make sure they throw some sort of QueryException and everything should be good.
Thinking further though, if this functionality only applies to queries, do we really need to use regex? Could we instead just configure a set of exception message by errorCode or maybe the classes of QueryException (not errorClass, but like java class name) to allow, and anything that doesn't match would be sanitized? I worry that regex based on the error messages might be a bit tricky/steep learning curve, and if we go this way we should probably update the documents to include several examples of the types of messages that query errors can encounter, and example configs.
There was a problem hiding this comment.
Moved to a common class so that we can reuse in multiple places.
| && serverConfig.getResponseWhitelistRegex().stream().noneMatch(pattern -> pattern.matcher(errorMessage).matches())) { | ||
| objectMapper.writeValue( | ||
| response.getOutputStream(), | ||
| ImmutableMap.of("error", DEFAULT_QUERY_PARSE_EXCEPTION_MESSAGE) |
There was a problem hiding this comment.
Maybe it is finally time to make a basic ErrorResponse class to capture this ImmutableMap.of("error", ...) pattern, searching the code-base I see it in a lot of places...
On the other hand, it is somewhat inconsistent with QueryException though, which uses error for the errorCode and has separate message, class, and some additional information. Since this is specifically forwarding queries, I wonder if this should be a type of QueryException? (Especially if we bake a method into QueryException to make them clean themselves up if configured as such)
There was a problem hiding this comment.
The response format here is inconsistent with QueryException. I am not sure why this was done this way. However, in this PR my intention is to keep the response format the same as original. Notice, that this PR does not add any new field in the response format, only remove fields that could already be null, and change string message for existing fields.
However, both Exception thrown here and the Exception from further downstream which uses QueryException already make the response inconsistent for the same API call. Since, the format of QueryException is well documented, I think we should change this to use QueryException too.
|
|
||
| @JsonProperty | ||
| @NotNull | ||
| private List<Pattern> responseWhitelistRegex = ImmutableList.of(); |
There was a problem hiding this comment.
this config applies to the http servers, not queries specifically, so I think if it is going to live here it might need a name that indicates it only applies to queries, unless you imagine that at some point this setting will apply to all HTTP error responses?
Alternatively, if it lived on a more query centric config object this wouldn't be necessary, though i'm not quite sure off the top of my head what is most appropriate.
There was a problem hiding this comment.
I imagine the setting for filterResponse will apply to all HTTP error responses. Currently it does apply to API outside of query APIs if the error is thrown in the Jetty filter layer. For example, if you access non-query API with invalid auth or an invalid API endpoint.
The setting for responseWhitelistRegex...I am not sure yet. I think it can apply to all HTTP error responses. Another idea I had was instead of using a whitelist, we can use a blacklist regex. Then it would be simpler to blacklist with regex like org\.apache\..* and java\..* to filter out stack traces. This would then be easy to apply to all HTTP error responses.
|
marking design review because it both adds new config and said config changes API responses 😅 |
clintropolis
left a comment
There was a problem hiding this comment.
this seems pretty flexible/powerful to potentially give operators a lot more control over error responses 👍
| } | ||
| ); | ||
|
|
||
| String errorMessage = "This will be support in Druid 9999"; |
|
|
||
| if (e instanceof RelOptPlanner.CannotPlanException) { | ||
| exceptionToReport = new ISE("Cannot build plan for query: %s", sqlQuery.getQuery()); | ||
| exceptionToReport = QueryInterruptedException.wrapIfNeeded((new ISE("Cannot build plan for query: %s", sqlQuery.getQuery()))); |
There was a problem hiding this comment.
nit: this isn't new or your change, but I think it is semi confusing that there exists aSqlPlanningException that isn't being used here. The reason for this is because I think that this is a different kind of planning exception - one that occurs for some query which we couldn't translate to a native Druid query, which we seem to be treating as a server-side error, where the other one is currently used for SQL parsing and validation errors and considered client side errors since they are things the user can fix.
I wonder if we should have some sort of unsupported query exception to give this a bit more prominence, but I think that is a tangential discussion to this PRs changed.
There was a problem hiding this comment.
Actually now that I look at it...I didn't have to change/refactor those and can just call serverConfig.getErrorResponseTransformStrategy().transformIfNeeded at the end
| catch (ForbiddenException e) { | ||
| endLifecycleWithoutEmittingMetrics(sqlQueryId, lifecycle); | ||
| throw e; // let ForbiddenExceptionMapper handle this | ||
| throw (ForbiddenException) serverConfig.getErrorResponseTransformStrategy().transformIfNeeded(e); // let ForbiddenExceptionMapper handle this |
There was a problem hiding this comment.
at some point in the future, assuming we eventually make this functionality apply to all API response errors, it probably makes sense to push this transformation functionality into the error handlers so that we everything that lets exceptions fall through doesn't need to do this itself. It is probably ok to leave here for now since this is the only thing doing it
There was a problem hiding this comment.
in fact, whenever in the future we do that, it might also make sense to move QueryException handling out to an ExceptionMapper<QueryException> and just rethrow all exceptions so that error response handling can be shared between sql and native json queries. it doesn't gain too much since will still need to handle internally so that metrics can be emitted, but it would cover the response writing.
| catch (QueryCapacityExceededException cap) { | ||
| endLifecycle(sqlQueryId, lifecycle, cap, remoteAddr, -1); | ||
| return buildNonOkResponse(QueryCapacityExceededException.STATUS_CODE, cap); | ||
| return buildNonOkResponse( |
There was a problem hiding this comment.
it looks like all callers of buildNonOkResponse are calling transformIfNeeded, it makes sense to update the signature to just accept SanitizableException
| inflateBufferSize == that.inflateBufferSize && | ||
| compressionLevel == that.compressionLevel && | ||
| enableForwardedRequestCustomizer == that.enableForwardedRequestCustomizer && | ||
| sanitizeJettyErrorResponse == that.sanitizeJettyErrorResponse && |
There was a problem hiding this comment.
hm, I wonder if technically ErrorResponseTransformStrategy should implement equals and be checked here
There was a problem hiding this comment.
I was having problem with the AllowedRegexErrorResponseTransformStrategy as the Pattern class doesn't implement equals method. So i just decided to exclude the ErrorResponseTransformStrategy here
There was a problem hiding this comment.
yeah, I guess you would need to deserialize into strings and keep them around alongside a set of patterns to build (which might be worth doing?)
|
|
||
| import java.util.function.Function; | ||
|
|
||
| public interface SanitizableException |
There was a problem hiding this comment.
nit: if a longer term goal is to make sure all surface exceptions are wrapped in this to allow operators to tidy up the error responses, i wonder if we should take a stronger name for this interface like ServiceApiException or .. idk naming is hard.
There was a problem hiding this comment.
This shouldn't effect/change the UI/UX or configs/settings so I think if we want, we can easily rename it later
There was a problem hiding this comment.
javadocs please. Explanation about how this is expected to be used and why this class exists will be helpful.
There was a problem hiding this comment.
Instead of calling this an Exception - let's make this a Factory class since the interface can't extend an Exception.
public interface SanitizingExceptionFactory<T extends Exception>
{
T transform(T exception, Function<String, String> errorMessageTransformFunction);
}
There was a problem hiding this comment.
Instead of calling this an Exception - let's make this a Factory class since the interface can't extend an Exception
Please ignore this suggestion. It makes the ErrorResponseTransformStrategy clunky
| * Apply the function for transforming the error message then | ||
| * return new Exception with sanitized fields and transformed message. | ||
| */ | ||
| Exception applyErrorMessageTransformAndSanitizeFields( |
There was a problem hiding this comment.
nit: i think transform would be descriptive enough of a method name, between the parameter name and the javadocs.
| if (exception instanceof Exception) { | ||
| return (Exception) exception; | ||
| } else { | ||
| return new IAE( | ||
| "Cannot sanitize error response as given argument[%s] is not an Exception", | ||
| exception.getClass().getName() | ||
| ); | ||
| } |
There was a problem hiding this comment.
why not just return exception; and be a no-op like the javadoc says?
There was a problem hiding this comment.
The method takes in SanitizableException which isn't guarantee to be an Exception. If it not, then it would result in ClassCastException and wouldn't be a no-op too.
There was a problem hiding this comment.
Changed to return (Exception) exception;
suneet-s
left a comment
There was a problem hiding this comment.
+1 on naming of system properties. They seem reasonable.
| public Function<String, String> getErrorMessageTransformFunction() | ||
| { | ||
| return (String errorMessage) -> { | ||
| if (allowedRegex.stream().noneMatch(pattern -> pattern.matcher(errorMessage).matches())) { |
There was a problem hiding this comment.
nit: Is it better to use anyMatch to return the errorMessage so the stream can short circuit and exit early?
|
|
||
| import java.util.function.Function; | ||
|
|
||
| public interface SanitizableException |
There was a problem hiding this comment.
javadocs please. Explanation about how this is expected to be used and why this class exists will be helpful.
| * return new Exception with sanitized fields and transformed message. | ||
| */ | ||
| Exception transform( | ||
| Function<String, String> errorMessageTransformFunction |
There was a problem hiding this comment.
Can you add details on why only the errorMessage is expected to be transformed in this interface to the javadoc please
| * Apply the function for transforming the error message then | ||
| * return new Exception with sanitized fields and transformed message. | ||
| */ | ||
| Exception transform( |
There was a problem hiding this comment.
nit: rename to sanitize to be inline with SanitizableException
There was a problem hiding this comment.
heh oops, was my suggestion to rename transform, but I guess I also suggested renaming SanitizableException to something else, though I feel less strongly about the name of this than the property name in the config, (which I think should be druid.server.http.enableErrorResponseTransform and druid.server.http.errorResponseTransformStrategy)
| if (Strings.isNullOrEmpty(errorMessageTransformFunction.apply(getMessage()))) { | ||
| return new ForbiddenException(); | ||
| } else { | ||
| return this; |
There was a problem hiding this comment.
appears to be a logic bug here
unit tests please
| @Override | ||
| public QueryException transform(Function<String, String> errorMessageTransformFunction) | ||
| { | ||
| return new QueryException(errorCode, errorMessageTransformFunction.apply(getMessage()), null, null); |
There was a problem hiding this comment.
can we add unit tests for this please
| boolean enableForwardedRequestCustomizer, | ||
| @NotNull List<String> allowedHttpMethods | ||
| @NotNull List<String> allowedHttpMethods, | ||
| boolean sanitizeJettyErrorResponse, |
There was a problem hiding this comment.
I think these configs should be called enableErrorResponseTransform and errorResponseTransformStrategy respectively, or something similar, since it is clearer their purpose (and also ErrorResponseTransformStrategy is the type name)
There was a problem hiding this comment.
sorry i got confused about sanitizeJettyErrorResponse, it shouldn't be renamed to enableErrorResponseTransform, though sanitizeDruidErrorResponse should still be renamed errorResponseTransformStrategy
There was a problem hiding this comment.
We don't need enableErrorResponseTransform as not setting errorResponseTransformStrategy or setting errorResponseTransformStrategy to none would be the same as current behavior (no-op) and leaving the error response unchanged.
I renamed sanitizeJettyErrorResponse to showDetailedJettyErrors and flip it
also renamed sanitizeDruidErrorResponse to errorResponseTransformStrategy
There was a problem hiding this comment.
yep sorry confused myself, thinking about too many things at once 🙃 , this sgtm 👍
There was a problem hiding this comment.
Actually it should be errorResponseTransform not errorResponseTransformStrategy for the @JsonProperty
so that the configs look something like
druid.server.http.showDetailedJettyErrors=false
druid.server.http.errorResponseTransform.strategy=allowedRegex
druid.server.http.errorResponseTransform.allowedRegex=["asd"]
| * Apply the function for transforming the error message then | ||
| * return new Exception with sanitized fields and transformed message. | ||
| */ | ||
| Exception transform( |
There was a problem hiding this comment.
heh oops, was my suggestion to rename transform, but I guess I also suggested renaming SanitizableException to something else, though I feel less strongly about the name of this than the property name in the config, (which I think should be druid.server.http.enableErrorResponseTransform and druid.server.http.errorResponseTransformStrategy)
| inflateBufferSize == that.inflateBufferSize && | ||
| compressionLevel == that.compressionLevel && | ||
| enableForwardedRequestCustomizer == that.enableForwardedRequestCustomizer && | ||
| sanitizeJettyErrorResponse == that.sanitizeJettyErrorResponse && |
There was a problem hiding this comment.
yeah, I guess you would need to deserialize into strings and keep them around alongside a set of patterns to build (which might be worth doing?)
clintropolis
left a comment
There was a problem hiding this comment.
👍 lgtm
we should document this, but is ok with me to do as a follow-up
suneet-s
left a comment
There was a problem hiding this comment.
LGTM after CI + docs in a follow up PR
…PI response (apache#11711) * add impl * add impl * add tests * add unit test * fix checkstyle * address comments * fix checkstyle * fix checkstyle * fix checkstyle * fix checkstyle * fix checkstyle * address comments * address comments * address comments * fix test * fix test * fix test * fix test * fix test * change config name * change config name * change config name * address comments * address comments * address comments * address comments * address comments * address comments * fix compile * fix compile * change config * add more tests * fix IT

Add new config to filter internal Druid-related messages from Query API response
Description
This PR adds the following new config:
druid.server.http.sanitizeJettyErrorResponseanddruid.server.http.sanitizeDruidErrorResponse.strategyThese configs will allow Druid API responses to hide internal information (class name, stack trace, internal concept, thread name / servlet name, code, line/column number, host/ip). This is useful for when we have external users using Druid query API.
For the initial functionality of this feature, we will handle the following Query API responses:
The effect of this feature for each layer is described below:
druid.server.http.sanitizeJettyErrorResponseenabled, any error in the Jetty layer / Jetty filter will have"servlet","message","url","status"and"cause"fields in the JSON response. Withdruid.server.http.sanitizeJettyErrorResponseenabled, the JSON response will only contains"message","url", and"status". Note that the value of these fields remain unchanged."error","errorMessage","errorClass", and"host"(as described in https://druid.apache.org/docs/latest/querying/querying.html#query-execution-failures). This change apply to both when the new config is not set and when set.druid.server.http.sanitizeDruidErrorResponse.strategyset or set tonone, any error in AsyncQueryForwardingServlet of the Router and SqlResource (/druid/v2/sql/ POST endpoint) will have the field"error","errorMessage","errorClass", and"host"(as described in https://druid.apache.org/docs/latest/querying/querying.html#query-execution-failures). Whendruid.server.http.sanitizeDruidErrorResponse.strategyis set, the field"errorClass", and"host"will always be null. Moreover, the field"errorMessage"will be transformed based on the set strategy. Currently, this PR only includes one strategy which is based on allowedRegex. Basically, the value of the field"errorMessage"will be checked against a list of regular expressions indruid.server.http.sanitizeDruidErrorResponse.allowedRegex. If the value match any of the regular expressions, then it will be included in the response, otherwise it will be null. Note that the value of the field"error"will remain unchanged. (Note that prior to this PR"errorMessage","errorClass", and"host"can be null)Future work:
We can reuse this config and apply it to other Query API such as Native query and JDBC.
This PR has: