mirror of https://github.com/apache/druid.git
Add new config to filter internal Druid-related messages from Query API response (#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
This commit is contained in:
parent
988623b7ae
commit
a04b08e45c
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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.druid.common.exception;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class AllowedRegexErrorResponseTransformStrategy implements ErrorResponseTransformStrategy
|
||||
{
|
||||
@JsonProperty
|
||||
private final List<String> allowedRegexString;
|
||||
|
||||
private final List<Pattern> allowedRegexPattern;
|
||||
|
||||
@JsonCreator
|
||||
public AllowedRegexErrorResponseTransformStrategy(
|
||||
@JsonProperty("allowedRegex") List<String> allowedRegexString
|
||||
)
|
||||
{
|
||||
this.allowedRegexString = allowedRegexString;
|
||||
this.allowedRegexPattern = allowedRegexString == null
|
||||
? ImmutableList.of()
|
||||
: allowedRegexString.stream().map(Pattern::compile).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Function<String, String> getErrorMessageTransformFunction()
|
||||
{
|
||||
return (String errorMessage) -> {
|
||||
if (allowedRegexPattern.stream().anyMatch(pattern -> pattern.matcher(errorMessage).matches())) {
|
||||
return errorMessage;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
AllowedRegexErrorResponseTransformStrategy that = (AllowedRegexErrorResponseTransformStrategy) o;
|
||||
return Objects.equals(allowedRegexString, that.allowedRegexString);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hash(allowedRegexString);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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.druid.common.exception;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.function.Function;
|
||||
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "strategy", defaultImpl = NoErrorResponseTransformStrategy.class)
|
||||
@JsonSubTypes(value = {
|
||||
@JsonSubTypes.Type(name = "none", value = NoErrorResponseTransformStrategy.class),
|
||||
@JsonSubTypes.Type(name = "allowedRegex", value = AllowedRegexErrorResponseTransformStrategy.class)
|
||||
})
|
||||
public interface ErrorResponseTransformStrategy
|
||||
{
|
||||
/**
|
||||
* For a given {@link SanitizableException} apply the transformation strategy and return the sanitized Exception
|
||||
* if the transformation stategy was applied.
|
||||
*/
|
||||
default Exception transformIfNeeded(SanitizableException exception)
|
||||
{
|
||||
return exception.sanitize(getErrorMessageTransformFunction());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a function for checking and transforming the error message if needed.
|
||||
* Function can return null if error message needs to be omitted or return String to be use instead.
|
||||
*/
|
||||
@NotNull
|
||||
Function<String, String> getErrorMessageTransformFunction();
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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.druid.common.exception;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Error response transform strategy that does nothing and simply return the same Exception back without any change
|
||||
*/
|
||||
public class NoErrorResponseTransformStrategy implements ErrorResponseTransformStrategy
|
||||
{
|
||||
public static final NoErrorResponseTransformStrategy INSTANCE = new NoErrorResponseTransformStrategy();
|
||||
|
||||
@Override
|
||||
public Exception transformIfNeeded(SanitizableException exception)
|
||||
{
|
||||
return (Exception) exception;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Function<String, String> getErrorMessageTransformFunction()
|
||||
{
|
||||
return (String errorMessage) -> errorMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
return !(o == null || getClass() != o.getClass());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return NoErrorResponseTransformStrategy.class.hashCode();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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.druid.common.exception;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
public interface SanitizableException
|
||||
{
|
||||
/**
|
||||
* Apply the function for transforming the error message then return new Exception with sanitized fields and transformed message.
|
||||
* The {@param errorMessageTransformFunction} is only intended to be use to transform the error message
|
||||
* String of the Exception as only the error message String is common to all Exception classes.
|
||||
* For other fields (which may be unique to each particular Exception class), each implementation of this method can
|
||||
* decide for itself how to sanitized those fields (i.e. leaving unchanged, changing to null, changing to a fixed String, etc.).
|
||||
* Note that this method returns a new Exception of the same type since Exception error message is immutable.
|
||||
*/
|
||||
Exception sanitize(
|
||||
Function<String, String> errorMessageTransformFunction
|
||||
);
|
||||
}
|
|
@ -22,16 +22,19 @@ package org.apache.druid.query;
|
|||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.apache.druid.common.exception.SanitizableException;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.net.InetAddress;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Base serializable error response
|
||||
*
|
||||
* QueryResource and SqlResource are expected to emit the JSON form of this object when errors happen.
|
||||
*/
|
||||
public class QueryException extends RuntimeException
|
||||
public class QueryException extends RuntimeException implements SanitizableException
|
||||
{
|
||||
private final String errorCode;
|
||||
private final String errorClass;
|
||||
|
@ -96,4 +99,10 @@ public class QueryException extends RuntimeException
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryException sanitize(@NotNull Function<String, String> errorMessageTransformFunction)
|
||||
{
|
||||
return new QueryException(errorCode, errorMessageTransformFunction.apply(getMessage()), null, null);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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.druid.common.exception;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import nl.jqno.equalsverifier.EqualsVerifier;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class AllowedRegexErrorResponseTransformStrategyTest
|
||||
{
|
||||
@Test
|
||||
public void testGetErrorMessageTransformFunctionWithMatchingAllowedRegexFilter()
|
||||
{
|
||||
AllowedRegexErrorResponseTransformStrategy allowedRegex = new AllowedRegexErrorResponseTransformStrategy(
|
||||
ImmutableList.of("acbd", "test .*")
|
||||
);
|
||||
String message = "test message 123";
|
||||
String result = allowedRegex.getErrorMessageTransformFunction().apply(message);
|
||||
Assert.assertEquals(message, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetErrorMessageTransformFunctionWithNoMatchingAllowedRegexFilter()
|
||||
{
|
||||
AllowedRegexErrorResponseTransformStrategy allowedRegex = new AllowedRegexErrorResponseTransformStrategy(
|
||||
ImmutableList.of("acbd", "qwer")
|
||||
);
|
||||
String message = "test message 123";
|
||||
String result = allowedRegex.getErrorMessageTransformFunction().apply(message);
|
||||
Assert.assertNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetErrorMessageTransformFunctionWithEmptyAllowedRegexFilter()
|
||||
{
|
||||
AllowedRegexErrorResponseTransformStrategy allowedRegex = new AllowedRegexErrorResponseTransformStrategy(
|
||||
ImmutableList.of()
|
||||
);
|
||||
String message = "test message 123";
|
||||
String result = allowedRegex.getErrorMessageTransformFunction().apply(message);
|
||||
Assert.assertNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEqualsAndHashCode()
|
||||
{
|
||||
EqualsVerifier.forClass(AllowedRegexErrorResponseTransformStrategy.class)
|
||||
.withIgnoredFields("allowedRegexPattern")
|
||||
.usingGetClass()
|
||||
.verify();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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.druid.common.exception;
|
||||
|
||||
import nl.jqno.equalsverifier.EqualsVerifier;
|
||||
import org.junit.Test;
|
||||
|
||||
public class NoErrorResponseTransformStrategyTest
|
||||
{
|
||||
@Test
|
||||
public void testEqualsAndHashCode()
|
||||
{
|
||||
EqualsVerifier.forClass(NoErrorResponseTransformStrategy.class)
|
||||
.usingGetClass()
|
||||
.verify();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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.druid.query;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class QueryExceptionTest
|
||||
{
|
||||
private static final String ERROR_CODE = "error code";
|
||||
private static final String ERROR_CLASS = "error code";
|
||||
private static final String HOST = "error code";
|
||||
private static final String ERROR_MESSAGE_ORIGINAL = "aaaa";
|
||||
private static final String ERROR_MESSAGE_TRANSFORMED = "bbbb";
|
||||
|
||||
@Mock
|
||||
private Function<String, String> trasformFunction;
|
||||
|
||||
@Test
|
||||
public void testSanitizeWithTransformFunctionReturningNull()
|
||||
{
|
||||
Mockito.when(trasformFunction.apply(ArgumentMatchers.eq(ERROR_MESSAGE_ORIGINAL))).thenReturn(null);
|
||||
QueryException queryException = new QueryException(ERROR_CODE, ERROR_MESSAGE_ORIGINAL, ERROR_CLASS, HOST);
|
||||
QueryException actual = queryException.sanitize(trasformFunction);
|
||||
Assert.assertNotNull(actual);
|
||||
Assert.assertEquals(actual.getErrorCode(), ERROR_CODE);
|
||||
Assert.assertNull(actual.getMessage());
|
||||
Assert.assertNull(actual.getHost());
|
||||
Assert.assertNull(actual.getErrorClass());
|
||||
Mockito.verify(trasformFunction).apply(ArgumentMatchers.eq(ERROR_MESSAGE_ORIGINAL));
|
||||
Mockito.verifyNoMoreInteractions(trasformFunction);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSanitizeWithTransformFunctionReturningNewString()
|
||||
{
|
||||
Mockito.when(trasformFunction.apply(ArgumentMatchers.eq(ERROR_MESSAGE_ORIGINAL))).thenReturn(ERROR_MESSAGE_TRANSFORMED);
|
||||
QueryException queryException = new QueryException(ERROR_CODE, ERROR_MESSAGE_ORIGINAL, ERROR_CLASS, HOST);
|
||||
QueryException actual = queryException.sanitize(trasformFunction);
|
||||
Assert.assertNotNull(actual);
|
||||
Assert.assertEquals(actual.getErrorCode(), ERROR_CODE);
|
||||
Assert.assertEquals(actual.getMessage(), ERROR_MESSAGE_TRANSFORMED);
|
||||
Assert.assertNull(actual.getHost());
|
||||
Assert.assertNull(actual.getErrorClass());
|
||||
Mockito.verify(trasformFunction).apply(ArgumentMatchers.eq(ERROR_MESSAGE_ORIGINAL));
|
||||
Mockito.verifyNoMoreInteractions(trasformFunction);
|
||||
}
|
||||
}
|
|
@ -258,7 +258,7 @@ public class ITTLSTest
|
|||
config.getCustomCertCheckRouterTLSUrl() + "/druid/v2",
|
||||
"Custom cert check",
|
||||
ISE.class,
|
||||
"Error while making request to url[https://127.0.0.1:9091/druid/v2] status[400 Bad Request] content[{\"error\":\"No content to map due to end-of-input",
|
||||
"Error while making request to url[https://127.0.0.1:9091/druid/v2] status[400 Bad Request] content[{\"error\":\"Unknown exception\",\"errorMessage\":\"No content to map due to end-of-input",
|
||||
true
|
||||
);
|
||||
|
||||
|
|
|
@ -21,6 +21,8 @@ package org.apache.druid.server.initialization;
|
|||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import org.apache.druid.common.exception.ErrorResponseTransformStrategy;
|
||||
import org.apache.druid.common.exception.NoErrorResponseTransformStrategy;
|
||||
import org.apache.druid.java.util.common.HumanReadableBytes;
|
||||
import org.apache.druid.java.util.common.HumanReadableBytesRange;
|
||||
import org.apache.druid.utils.JvmUtils;
|
||||
|
@ -61,7 +63,9 @@ public class ServerConfig
|
|||
int inflateBufferSize,
|
||||
int compressionLevel,
|
||||
boolean enableForwardedRequestCustomizer,
|
||||
@NotNull List<String> allowedHttpMethods
|
||||
@NotNull List<String> allowedHttpMethods,
|
||||
boolean showDetailedJettyErrors,
|
||||
ErrorResponseTransformStrategy errorResponseTransformStrategy
|
||||
)
|
||||
{
|
||||
this.numThreads = numThreads;
|
||||
|
@ -79,6 +83,8 @@ public class ServerConfig
|
|||
this.compressionLevel = compressionLevel;
|
||||
this.enableForwardedRequestCustomizer = enableForwardedRequestCustomizer;
|
||||
this.allowedHttpMethods = allowedHttpMethods;
|
||||
this.showDetailedJettyErrors = showDetailedJettyErrors;
|
||||
this.errorResponseTransformStrategy = errorResponseTransformStrategy;
|
||||
}
|
||||
|
||||
public ServerConfig()
|
||||
|
@ -145,6 +151,13 @@ public class ServerConfig
|
|||
@NotNull
|
||||
private List<String> allowedHttpMethods = ImmutableList.of();
|
||||
|
||||
@JsonProperty("errorResponseTransform")
|
||||
@NotNull
|
||||
private ErrorResponseTransformStrategy errorResponseTransformStrategy = NoErrorResponseTransformStrategy.INSTANCE;
|
||||
|
||||
@JsonProperty
|
||||
private boolean showDetailedJettyErrors = true;
|
||||
|
||||
public int getNumThreads()
|
||||
{
|
||||
return numThreads;
|
||||
|
@ -215,6 +228,16 @@ public class ServerConfig
|
|||
return enableForwardedRequestCustomizer;
|
||||
}
|
||||
|
||||
public boolean isShowDetailedJettyErrors()
|
||||
{
|
||||
return showDetailedJettyErrors;
|
||||
}
|
||||
|
||||
public ErrorResponseTransformStrategy getErrorResponseTransformStrategy()
|
||||
{
|
||||
return errorResponseTransformStrategy;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public List<String> getAllowedHttpMethods()
|
||||
{
|
||||
|
@ -235,17 +258,19 @@ public class ServerConfig
|
|||
queueSize == that.queueSize &&
|
||||
enableRequestLimit == that.enableRequestLimit &&
|
||||
defaultQueryTimeout == that.defaultQueryTimeout &&
|
||||
maxScatterGatherBytes.equals(that.maxScatterGatherBytes) &&
|
||||
maxSubqueryRows == that.maxSubqueryRows &&
|
||||
maxQueryTimeout == that.maxQueryTimeout &&
|
||||
maxRequestHeaderSize == that.maxRequestHeaderSize &&
|
||||
inflateBufferSize == that.inflateBufferSize &&
|
||||
compressionLevel == that.compressionLevel &&
|
||||
enableForwardedRequestCustomizer == that.enableForwardedRequestCustomizer &&
|
||||
showDetailedJettyErrors == that.showDetailedJettyErrors &&
|
||||
maxIdleTime.equals(that.maxIdleTime) &&
|
||||
maxScatterGatherBytes.equals(that.maxScatterGatherBytes) &&
|
||||
gracefulShutdownTimeout.equals(that.gracefulShutdownTimeout) &&
|
||||
unannouncePropagationDelay.equals(that.unannouncePropagationDelay) &&
|
||||
allowedHttpMethods.equals(that.allowedHttpMethods);
|
||||
allowedHttpMethods.equals(that.allowedHttpMethods) &&
|
||||
errorResponseTransformStrategy.equals(that.errorResponseTransformStrategy);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -266,7 +291,9 @@ public class ServerConfig
|
|||
inflateBufferSize,
|
||||
compressionLevel,
|
||||
enableForwardedRequestCustomizer,
|
||||
allowedHttpMethods
|
||||
allowedHttpMethods,
|
||||
errorResponseTransformStrategy,
|
||||
showDetailedJettyErrors
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -288,7 +315,9 @@ public class ServerConfig
|
|||
", inflateBufferSize=" + inflateBufferSize +
|
||||
", compressionLevel=" + compressionLevel +
|
||||
", enableForwardedRequestCustomizer=" + enableForwardedRequestCustomizer +
|
||||
", allowedMethods=" + allowedHttpMethods +
|
||||
", allowedHttpMethods=" + allowedHttpMethods +
|
||||
", errorResponseTransformStrategy=" + errorResponseTransformStrategy +
|
||||
", showDetailedJettyErrors=" + showDetailedJettyErrors +
|
||||
'}';
|
||||
}
|
||||
|
||||
|
|
|
@ -160,7 +160,9 @@ public class CliIndexerServerModule implements Module
|
|||
oldConfig.getInflateBufferSize(),
|
||||
oldConfig.getCompressionLevel(),
|
||||
oldConfig.isEnableForwardedRequestCustomizer(),
|
||||
oldConfig.getAllowedHttpMethods()
|
||||
oldConfig.getAllowedHttpMethods(),
|
||||
oldConfig.isShowDetailedJettyErrors(),
|
||||
oldConfig.getErrorResponseTransformStrategy()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,10 +68,12 @@ import org.eclipse.jetty.server.ForwardedRequestCustomizer;
|
|||
import org.eclipse.jetty.server.Handler;
|
||||
import org.eclipse.jetty.server.HttpConfiguration;
|
||||
import org.eclipse.jetty.server.HttpConnectionFactory;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.SecureRequestCustomizer;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.server.SslConnectionFactory;
|
||||
import org.eclipse.jetty.server.handler.ErrorHandler;
|
||||
import org.eclipse.jetty.util.component.LifeCycle;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
import org.eclipse.jetty.util.thread.QueuedThreadPool;
|
||||
|
@ -82,6 +84,11 @@ import javax.net.ssl.SSLEngine;
|
|||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509ExtendedTrustManager;
|
||||
import javax.servlet.RequestDispatcher;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.cert.CRL;
|
||||
import java.util.ArrayList;
|
||||
|
@ -466,6 +473,28 @@ public class JettyServerModule extends JerseyServletModule
|
|||
Lifecycle.Stage.SERVER
|
||||
);
|
||||
|
||||
if (!config.isShowDetailedJettyErrors()) {
|
||||
server.setErrorHandler(new ErrorHandler() {
|
||||
@Override
|
||||
public boolean isShowServlet()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(
|
||||
String target,
|
||||
Request baseRequest,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response
|
||||
) throws IOException, ServletException
|
||||
{
|
||||
request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, null);
|
||||
super.handle(target, baseRequest, request, response);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
|
|
|
@ -21,16 +21,22 @@ package org.apache.druid.server.security;
|
|||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.base.Strings;
|
||||
import org.apache.druid.common.exception.SanitizableException;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Throw this when a request is unauthorized and we want to send a 403 response back, Jersey exception mapper will
|
||||
* take care of sending the response.
|
||||
*/
|
||||
public class ForbiddenException extends RuntimeException
|
||||
public class ForbiddenException extends RuntimeException implements SanitizableException
|
||||
{
|
||||
static final String DEFAULT_ERROR_MESSAGE = "Unauthorized.";
|
||||
|
||||
public ForbiddenException()
|
||||
{
|
||||
super("Unauthorized.");
|
||||
super(DEFAULT_ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
@JsonCreator
|
||||
|
@ -44,4 +50,15 @@ public class ForbiddenException extends RuntimeException
|
|||
{
|
||||
return super.getMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ForbiddenException sanitize(Function<String, String> errorMessageTransformFunction)
|
||||
{
|
||||
String transformedErrorMessage = errorMessageTransformFunction.apply(getMessage());
|
||||
if (Strings.isNullOrEmpty(transformedErrorMessage)) {
|
||||
return new ForbiddenException();
|
||||
} else {
|
||||
return new ForbiddenException(transformedErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ package org.apache.druid.initialization;
|
|||
import com.google.common.collect.ImmutableList;
|
||||
import nl.jqno.equalsverifier.EqualsVerifier;
|
||||
import nl.jqno.equalsverifier.Warning;
|
||||
import org.apache.druid.common.exception.AllowedRegexErrorResponseTransformStrategy;
|
||||
import org.apache.druid.jackson.DefaultObjectMapper;
|
||||
import org.apache.druid.server.initialization.ServerConfig;
|
||||
import org.junit.Assert;
|
||||
|
@ -57,13 +58,16 @@ public class ServerConfigTest
|
|||
defaultConfig.getInflateBufferSize(),
|
||||
defaultConfig.getCompressionLevel(),
|
||||
true,
|
||||
ImmutableList.of(HttpMethod.OPTIONS)
|
||||
ImmutableList.of(HttpMethod.OPTIONS),
|
||||
true,
|
||||
new AllowedRegexErrorResponseTransformStrategy(ImmutableList.of(".*"))
|
||||
);
|
||||
String modifiedConfigJson = OBJECT_MAPPER.writeValueAsString(modifiedConfig);
|
||||
ServerConfig modifiedConfig2 = OBJECT_MAPPER.readValue(modifiedConfigJson, ServerConfig.class);
|
||||
Assert.assertEquals(modifiedConfig, modifiedConfig2);
|
||||
Assert.assertEquals(999, modifiedConfig2.getNumThreads());
|
||||
Assert.assertEquals(888, modifiedConfig2.getQueueSize());
|
||||
Assert.assertTrue(modifiedConfig2.getErrorResponseTransformStrategy() instanceof AllowedRegexErrorResponseTransformStrategy);
|
||||
Assert.assertTrue(modifiedConfig2.isEnableForwardedRequestCustomizer());
|
||||
Assert.assertEquals(1, modifiedConfig2.getAllowedHttpMethods().size());
|
||||
Assert.assertTrue(modifiedConfig2.getAllowedHttpMethods().contains(HttpMethod.OPTIONS));
|
||||
|
|
|
@ -76,7 +76,7 @@ public abstract class BaseJettyTest
|
|||
protected int port = -1;
|
||||
protected int tlsPort = -1;
|
||||
|
||||
public static void setProperties()
|
||||
protected void setProperties()
|
||||
{
|
||||
System.setProperty("druid.server.http.numThreads", "20");
|
||||
System.setProperty("druid.server.http.maxIdleTime", "PT1S");
|
||||
|
|
|
@ -554,6 +554,13 @@ public class JettyTest extends BaseJettyTest
|
|||
Assert.assertTrue(endpointIdentificationAlgorithm == null || endpointIdentificationAlgorithm.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testJettyErrorHandlerWithFilter()
|
||||
{
|
||||
// Response filter is disabled by default hence we show servlet information
|
||||
Assert.assertTrue(server.getErrorHandler().isShowServlet());
|
||||
}
|
||||
|
||||
private void waitForJettyServerModuleActiveConnectionsZero(JettyServerModule jsm) throws InterruptedException
|
||||
{
|
||||
// it can take a bit to close the connection, so maybe sleep for a while and hope it closes
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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.druid.server.initialization;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class JettyWithResponseFilterEnabledTest extends JettyTest
|
||||
{
|
||||
@Override
|
||||
public void setProperties()
|
||||
{
|
||||
System.setProperty("druid.server.http.showDetailedJettyErrors", "false");
|
||||
super.setProperties();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Override
|
||||
public void testJettyErrorHandlerWithFilter()
|
||||
{
|
||||
// Response filter is enabled by config hence we do not show servlet information
|
||||
Assert.assertFalse(server.getErrorHandler().isShowServlet());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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.druid.server.security;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class ForbiddenExceptionTest
|
||||
{
|
||||
private static final String ERROR_MESSAGE_ORIGINAL = "aaaa";
|
||||
private static final String ERROR_MESSAGE_TRANSFORMED = "bbbb";
|
||||
|
||||
@Mock
|
||||
private Function<String, String> trasformFunction;
|
||||
|
||||
@Test
|
||||
public void testSanitizeWithTransformFunctionReturningNull()
|
||||
{
|
||||
Mockito.when(trasformFunction.apply(ArgumentMatchers.eq(ERROR_MESSAGE_ORIGINAL))).thenReturn(null);
|
||||
ForbiddenException forbiddenException = new ForbiddenException(ERROR_MESSAGE_ORIGINAL);
|
||||
ForbiddenException actual = forbiddenException.sanitize(trasformFunction);
|
||||
Assert.assertNotNull(actual);
|
||||
Assert.assertEquals(actual.getMessage(), ForbiddenException.DEFAULT_ERROR_MESSAGE);
|
||||
Mockito.verify(trasformFunction).apply(ArgumentMatchers.eq(ERROR_MESSAGE_ORIGINAL));
|
||||
Mockito.verifyNoMoreInteractions(trasformFunction);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSanitizeWithTransformFunctionReturningNewString()
|
||||
{
|
||||
Mockito.when(trasformFunction.apply(ArgumentMatchers.eq(ERROR_MESSAGE_ORIGINAL))).thenReturn(ERROR_MESSAGE_TRANSFORMED);
|
||||
ForbiddenException forbiddenException = new ForbiddenException(ERROR_MESSAGE_ORIGINAL);
|
||||
ForbiddenException actual = forbiddenException.sanitize(trasformFunction);
|
||||
Assert.assertNotNull(actual);
|
||||
Assert.assertEquals(actual.getMessage(), ERROR_MESSAGE_TRANSFORMED);
|
||||
Mockito.verify(trasformFunction).apply(ArgumentMatchers.eq(ERROR_MESSAGE_ORIGINAL));
|
||||
Mockito.verifyNoMoreInteractions(trasformFunction);
|
||||
}
|
||||
}
|
|
@ -42,8 +42,10 @@ import org.apache.druid.java.util.emitter.service.ServiceEmitter;
|
|||
import org.apache.druid.query.DruidMetrics;
|
||||
import org.apache.druid.query.GenericQueryMetricsFactory;
|
||||
import org.apache.druid.query.Query;
|
||||
import org.apache.druid.query.QueryInterruptedException;
|
||||
import org.apache.druid.query.QueryMetrics;
|
||||
import org.apache.druid.query.QueryToolChestWarehouse;
|
||||
import org.apache.druid.server.initialization.ServerConfig;
|
||||
import org.apache.druid.server.log.RequestLogger;
|
||||
import org.apache.druid.server.metrics.QueryCountStatsProvider;
|
||||
import org.apache.druid.server.router.QueryHostFinder;
|
||||
|
@ -102,17 +104,18 @@ public class AsyncQueryForwardingServlet extends AsyncProxyServlet implements Qu
|
|||
private final AtomicLong failedQueryCount = new AtomicLong();
|
||||
private final AtomicLong interruptedQueryCount = new AtomicLong();
|
||||
|
||||
private static void handleException(HttpServletResponse response, ObjectMapper objectMapper, Exception exception)
|
||||
@VisibleForTesting
|
||||
void handleException(HttpServletResponse response, ObjectMapper objectMapper, Exception exception)
|
||||
throws IOException
|
||||
{
|
||||
QueryInterruptedException exceptionToReport = QueryInterruptedException.wrapIfNeeded(exception);
|
||||
LOG.warn(exceptionToReport, "Unexpected exception occurs");
|
||||
if (!response.isCommitted()) {
|
||||
final String errorMessage = exception.getMessage() == null ? "null exception" : exception.getMessage();
|
||||
|
||||
response.resetBuffer();
|
||||
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
objectMapper.writeValue(
|
||||
response.getOutputStream(),
|
||||
ImmutableMap.of("error", errorMessage)
|
||||
serverConfig.getErrorResponseTransformStrategy().transformIfNeeded(exceptionToReport)
|
||||
);
|
||||
}
|
||||
response.flushBuffer();
|
||||
|
@ -129,6 +132,7 @@ public class AsyncQueryForwardingServlet extends AsyncProxyServlet implements Qu
|
|||
private final GenericQueryMetricsFactory queryMetricsFactory;
|
||||
private final AuthenticatorMapper authenticatorMapper;
|
||||
private final ProtobufTranslation protobufTranslation;
|
||||
private final ServerConfig serverConfig;
|
||||
|
||||
private final boolean routeSqlByStrategy;
|
||||
|
||||
|
@ -146,7 +150,8 @@ public class AsyncQueryForwardingServlet extends AsyncProxyServlet implements Qu
|
|||
RequestLogger requestLogger,
|
||||
GenericQueryMetricsFactory queryMetricsFactory,
|
||||
AuthenticatorMapper authenticatorMapper,
|
||||
Properties properties
|
||||
Properties properties,
|
||||
final ServerConfig serverConfig
|
||||
)
|
||||
{
|
||||
this.warehouse = warehouse;
|
||||
|
@ -163,6 +168,7 @@ public class AsyncQueryForwardingServlet extends AsyncProxyServlet implements Qu
|
|||
this.routeSqlByStrategy = Boolean.parseBoolean(
|
||||
properties.getProperty(PROPERTY_SQL_ENABLE, PROPERTY_SQL_ENABLE_DEFAULT)
|
||||
);
|
||||
this.serverConfig = serverConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -320,7 +326,8 @@ public class AsyncQueryForwardingServlet extends AsyncProxyServlet implements Qu
|
|||
interruptedQueryCount.incrementAndGet();
|
||||
}
|
||||
|
||||
private void handleQueryParseException(
|
||||
@VisibleForTesting
|
||||
void handleQueryParseException(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
ObjectMapper objectMapper,
|
||||
|
@ -328,11 +335,12 @@ public class AsyncQueryForwardingServlet extends AsyncProxyServlet implements Qu
|
|||
boolean isNativeQuery
|
||||
) throws IOException
|
||||
{
|
||||
LOG.warn(parseException, "Exception parsing query");
|
||||
QueryInterruptedException exceptionToReport = QueryInterruptedException.wrapIfNeeded(parseException);
|
||||
LOG.warn(exceptionToReport, "Exception parsing query");
|
||||
|
||||
// Log the error message
|
||||
final String errorMessage = parseException.getMessage() == null
|
||||
? "no error message" : parseException.getMessage();
|
||||
final String errorMessage = exceptionToReport.getMessage() == null
|
||||
? "no error message" : exceptionToReport.getMessage();
|
||||
if (isNativeQuery) {
|
||||
requestLogger.logNativeQuery(
|
||||
RequestLogLine.forNative(
|
||||
|
@ -359,7 +367,7 @@ public class AsyncQueryForwardingServlet extends AsyncProxyServlet implements Qu
|
|||
response.setContentType(MediaType.APPLICATION_JSON);
|
||||
objectMapper.writeValue(
|
||||
response.getOutputStream(),
|
||||
ImmutableMap.of("error", errorMessage)
|
||||
serverConfig.getErrorResponseTransformStrategy().transformIfNeeded(exceptionToReport)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,8 @@ import com.google.inject.Module;
|
|||
import com.google.inject.servlet.GuiceFilter;
|
||||
import org.apache.calcite.avatica.Meta;
|
||||
import org.apache.calcite.avatica.remote.Service;
|
||||
import org.apache.druid.common.exception.AllowedRegexErrorResponseTransformStrategy;
|
||||
import org.apache.druid.common.exception.ErrorResponseTransformStrategy;
|
||||
import org.apache.druid.common.utils.SocketUtil;
|
||||
import org.apache.druid.guice.GuiceInjectors;
|
||||
import org.apache.druid.guice.Jerseys;
|
||||
|
@ -50,6 +52,8 @@ import org.apache.druid.query.DefaultGenericQueryMetricsFactory;
|
|||
import org.apache.druid.query.Druids;
|
||||
import org.apache.druid.query.MapQueryToolChestWarehouse;
|
||||
import org.apache.druid.query.Query;
|
||||
import org.apache.druid.query.QueryException;
|
||||
import org.apache.druid.query.QueryInterruptedException;
|
||||
import org.apache.druid.query.timeseries.TimeseriesQuery;
|
||||
import org.apache.druid.segment.TestHelper;
|
||||
import org.apache.druid.server.initialization.BaseJettyTest;
|
||||
|
@ -78,13 +82,18 @@ import org.eclipse.jetty.servlet.ServletHolder;
|
|||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import javax.servlet.ReadListener;
|
||||
import javax.servlet.ServletInputStream;
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
|
@ -224,6 +233,253 @@ public class AsyncQueryForwardingServletTest extends BaseJettyTest
|
|||
verifyServletCallsForQuery(query, false, hostFinder, new Properties());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandleExceptionWithFilterDisabled() throws Exception
|
||||
{
|
||||
String errorMessage = "test exception message";
|
||||
ObjectMapper mockMapper = Mockito.mock(ObjectMapper.class);
|
||||
HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
|
||||
ServletOutputStream outputStream = Mockito.mock(ServletOutputStream.class);
|
||||
Mockito.when(response.getOutputStream()).thenReturn(outputStream);
|
||||
final AsyncQueryForwardingServlet servlet = new AsyncQueryForwardingServlet(
|
||||
new MapQueryToolChestWarehouse(ImmutableMap.of()),
|
||||
mockMapper,
|
||||
TestHelper.makeSmileMapper(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
new NoopServiceEmitter(),
|
||||
new NoopRequestLogger(),
|
||||
new DefaultGenericQueryMetricsFactory(),
|
||||
new AuthenticatorMapper(ImmutableMap.of()),
|
||||
new Properties(),
|
||||
new ServerConfig()
|
||||
);
|
||||
Exception testException = new IllegalStateException(errorMessage);
|
||||
servlet.handleException(response, mockMapper, testException);
|
||||
ArgumentCaptor<Exception> captor = ArgumentCaptor.forClass(Exception.class);
|
||||
Mockito.verify(mockMapper).writeValue(ArgumentMatchers.eq(outputStream), captor.capture());
|
||||
Assert.assertTrue(captor.getValue() instanceof QueryException);
|
||||
Assert.assertEquals(QueryInterruptedException.UNKNOWN_EXCEPTION, ((QueryException) captor.getValue()).getErrorCode());
|
||||
Assert.assertEquals(errorMessage, captor.getValue().getMessage());
|
||||
Assert.assertEquals(IllegalStateException.class.getName(), ((QueryException) captor.getValue()).getErrorClass());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandleExceptionWithFilterEnabled() throws Exception
|
||||
{
|
||||
String errorMessage = "test exception message";
|
||||
ObjectMapper mockMapper = Mockito.mock(ObjectMapper.class);
|
||||
HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
|
||||
ServletOutputStream outputStream = Mockito.mock(ServletOutputStream.class);
|
||||
Mockito.when(response.getOutputStream()).thenReturn(outputStream);
|
||||
final AsyncQueryForwardingServlet servlet = new AsyncQueryForwardingServlet(
|
||||
new MapQueryToolChestWarehouse(ImmutableMap.of()),
|
||||
mockMapper,
|
||||
TestHelper.makeSmileMapper(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
new NoopServiceEmitter(),
|
||||
new NoopRequestLogger(),
|
||||
new DefaultGenericQueryMetricsFactory(),
|
||||
new AuthenticatorMapper(ImmutableMap.of()),
|
||||
new Properties(),
|
||||
new ServerConfig() {
|
||||
@Override
|
||||
public boolean isShowDetailedJettyErrors()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ErrorResponseTransformStrategy getErrorResponseTransformStrategy()
|
||||
{
|
||||
return new AllowedRegexErrorResponseTransformStrategy(ImmutableList.of());
|
||||
}
|
||||
}
|
||||
);
|
||||
Exception testException = new IllegalStateException(errorMessage);
|
||||
servlet.handleException(response, mockMapper, testException);
|
||||
ArgumentCaptor<Exception> captor = ArgumentCaptor.forClass(Exception.class);
|
||||
Mockito.verify(mockMapper).writeValue(ArgumentMatchers.eq(outputStream), captor.capture());
|
||||
Assert.assertTrue(captor.getValue() instanceof QueryException);
|
||||
Assert.assertEquals(QueryInterruptedException.UNKNOWN_EXCEPTION, ((QueryException) captor.getValue()).getErrorCode());
|
||||
Assert.assertNull(captor.getValue().getMessage());
|
||||
Assert.assertNull(((QueryException) captor.getValue()).getErrorClass());
|
||||
Assert.assertNull(((QueryException) captor.getValue()).getHost());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandleExceptionWithFilterEnabledButMessageMatchAllowedRegex() throws Exception
|
||||
{
|
||||
String errorMessage = "test exception message";
|
||||
ObjectMapper mockMapper = Mockito.mock(ObjectMapper.class);
|
||||
HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
|
||||
ServletOutputStream outputStream = Mockito.mock(ServletOutputStream.class);
|
||||
Mockito.when(response.getOutputStream()).thenReturn(outputStream);
|
||||
final AsyncQueryForwardingServlet servlet = new AsyncQueryForwardingServlet(
|
||||
new MapQueryToolChestWarehouse(ImmutableMap.of()),
|
||||
mockMapper,
|
||||
TestHelper.makeSmileMapper(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
new NoopServiceEmitter(),
|
||||
new NoopRequestLogger(),
|
||||
new DefaultGenericQueryMetricsFactory(),
|
||||
new AuthenticatorMapper(ImmutableMap.of()),
|
||||
new Properties(),
|
||||
new ServerConfig() {
|
||||
@Override
|
||||
public boolean isShowDetailedJettyErrors()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ErrorResponseTransformStrategy getErrorResponseTransformStrategy()
|
||||
{
|
||||
return new AllowedRegexErrorResponseTransformStrategy(ImmutableList.of("test .*"));
|
||||
}
|
||||
}
|
||||
);
|
||||
Exception testException = new IllegalStateException(errorMessage);
|
||||
servlet.handleException(response, mockMapper, testException);
|
||||
ArgumentCaptor<Exception> captor = ArgumentCaptor.forClass(Exception.class);
|
||||
Mockito.verify(mockMapper).writeValue(ArgumentMatchers.eq(outputStream), captor.capture());
|
||||
Assert.assertTrue(captor.getValue() instanceof QueryException);
|
||||
Assert.assertEquals(QueryInterruptedException.UNKNOWN_EXCEPTION, ((QueryException) captor.getValue()).getErrorCode());
|
||||
Assert.assertEquals(errorMessage, captor.getValue().getMessage());
|
||||
Assert.assertNull(((QueryException) captor.getValue()).getErrorClass());
|
||||
Assert.assertNull(((QueryException) captor.getValue()).getHost());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandleQueryParseExceptionWithFilterDisabled() throws Exception
|
||||
{
|
||||
String errorMessage = "test exception message";
|
||||
ObjectMapper mockMapper = Mockito.mock(ObjectMapper.class);
|
||||
HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
|
||||
HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
|
||||
ServletOutputStream outputStream = Mockito.mock(ServletOutputStream.class);
|
||||
Mockito.when(response.getOutputStream()).thenReturn(outputStream);
|
||||
final AsyncQueryForwardingServlet servlet = new AsyncQueryForwardingServlet(
|
||||
new MapQueryToolChestWarehouse(ImmutableMap.of()),
|
||||
mockMapper,
|
||||
TestHelper.makeSmileMapper(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
new NoopServiceEmitter(),
|
||||
new NoopRequestLogger(),
|
||||
new DefaultGenericQueryMetricsFactory(),
|
||||
new AuthenticatorMapper(ImmutableMap.of()),
|
||||
new Properties(),
|
||||
new ServerConfig()
|
||||
);
|
||||
IOException testException = new IOException(errorMessage);
|
||||
servlet.handleQueryParseException(request, response, mockMapper, testException, false);
|
||||
ArgumentCaptor<Exception> captor = ArgumentCaptor.forClass(Exception.class);
|
||||
Mockito.verify(mockMapper).writeValue(ArgumentMatchers.eq(outputStream), captor.capture());
|
||||
Assert.assertTrue(captor.getValue() instanceof QueryException);
|
||||
Assert.assertEquals(QueryInterruptedException.UNKNOWN_EXCEPTION, ((QueryException) captor.getValue()).getErrorCode());
|
||||
Assert.assertEquals(errorMessage, captor.getValue().getMessage());
|
||||
Assert.assertEquals(IOException.class.getName(), ((QueryException) captor.getValue()).getErrorClass());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandleQueryParseExceptionWithFilterEnabled() throws Exception
|
||||
{
|
||||
String errorMessage = "test exception message";
|
||||
ObjectMapper mockMapper = Mockito.mock(ObjectMapper.class);
|
||||
HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
|
||||
HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
|
||||
ServletOutputStream outputStream = Mockito.mock(ServletOutputStream.class);
|
||||
Mockito.when(response.getOutputStream()).thenReturn(outputStream);
|
||||
final AsyncQueryForwardingServlet servlet = new AsyncQueryForwardingServlet(
|
||||
new MapQueryToolChestWarehouse(ImmutableMap.of()),
|
||||
mockMapper,
|
||||
TestHelper.makeSmileMapper(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
new NoopServiceEmitter(),
|
||||
new NoopRequestLogger(),
|
||||
new DefaultGenericQueryMetricsFactory(),
|
||||
new AuthenticatorMapper(ImmutableMap.of()),
|
||||
new Properties(),
|
||||
new ServerConfig() {
|
||||
@Override
|
||||
public boolean isShowDetailedJettyErrors()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ErrorResponseTransformStrategy getErrorResponseTransformStrategy()
|
||||
{
|
||||
return new AllowedRegexErrorResponseTransformStrategy(ImmutableList.of());
|
||||
}
|
||||
}
|
||||
);
|
||||
IOException testException = new IOException(errorMessage);
|
||||
servlet.handleQueryParseException(request, response, mockMapper, testException, false);
|
||||
ArgumentCaptor<Exception> captor = ArgumentCaptor.forClass(Exception.class);
|
||||
Mockito.verify(mockMapper).writeValue(ArgumentMatchers.eq(outputStream), captor.capture());
|
||||
Assert.assertTrue(captor.getValue() instanceof QueryException);
|
||||
Assert.assertEquals(QueryInterruptedException.UNKNOWN_EXCEPTION, ((QueryException) captor.getValue()).getErrorCode());
|
||||
Assert.assertNull(captor.getValue().getMessage());
|
||||
Assert.assertNull(((QueryException) captor.getValue()).getErrorClass());
|
||||
Assert.assertNull(((QueryException) captor.getValue()).getHost());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandleQueryParseExceptionWithFilterEnabledButMessageMatchAllowedRegex() throws Exception
|
||||
{
|
||||
String errorMessage = "test exception message";
|
||||
ObjectMapper mockMapper = Mockito.mock(ObjectMapper.class);
|
||||
HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
|
||||
HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
|
||||
ServletOutputStream outputStream = Mockito.mock(ServletOutputStream.class);
|
||||
Mockito.when(response.getOutputStream()).thenReturn(outputStream);
|
||||
final AsyncQueryForwardingServlet servlet = new AsyncQueryForwardingServlet(
|
||||
new MapQueryToolChestWarehouse(ImmutableMap.of()),
|
||||
mockMapper,
|
||||
TestHelper.makeSmileMapper(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
new NoopServiceEmitter(),
|
||||
new NoopRequestLogger(),
|
||||
new DefaultGenericQueryMetricsFactory(),
|
||||
new AuthenticatorMapper(ImmutableMap.of()),
|
||||
new Properties(),
|
||||
new ServerConfig() {
|
||||
@Override
|
||||
public boolean isShowDetailedJettyErrors()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ErrorResponseTransformStrategy getErrorResponseTransformStrategy()
|
||||
{
|
||||
return new AllowedRegexErrorResponseTransformStrategy(ImmutableList.of("test .*"));
|
||||
}
|
||||
}
|
||||
);
|
||||
IOException testException = new IOException(errorMessage);
|
||||
servlet.handleQueryParseException(request, response, mockMapper, testException, false);
|
||||
ArgumentCaptor<Exception> captor = ArgumentCaptor.forClass(Exception.class);
|
||||
Mockito.verify(mockMapper).writeValue(ArgumentMatchers.eq(outputStream), captor.capture());
|
||||
Assert.assertTrue(captor.getValue() instanceof QueryException);
|
||||
Assert.assertEquals(QueryInterruptedException.UNKNOWN_EXCEPTION, ((QueryException) captor.getValue()).getErrorCode());
|
||||
Assert.assertEquals(errorMessage, captor.getValue().getMessage());
|
||||
Assert.assertNull(((QueryException) captor.getValue()).getErrorClass());
|
||||
Assert.assertNull(((QueryException) captor.getValue()).getHost());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the Servlet calls the right methods the right number of times.
|
||||
*/
|
||||
|
@ -296,7 +552,8 @@ public class AsyncQueryForwardingServletTest extends BaseJettyTest
|
|||
new NoopRequestLogger(),
|
||||
new DefaultGenericQueryMetricsFactory(),
|
||||
new AuthenticatorMapper(ImmutableMap.of()),
|
||||
properties
|
||||
properties,
|
||||
new ServerConfig()
|
||||
)
|
||||
{
|
||||
@Override
|
||||
|
@ -389,7 +646,8 @@ public class AsyncQueryForwardingServletTest extends BaseJettyTest
|
|||
new NoopRequestLogger(),
|
||||
new DefaultGenericQueryMetricsFactory(),
|
||||
new AuthenticatorMapper(ImmutableMap.of()),
|
||||
new Properties()
|
||||
new Properties(),
|
||||
new ServerConfig()
|
||||
)
|
||||
{
|
||||
@Override
|
||||
|
|
|
@ -26,6 +26,7 @@ import com.google.common.collect.Iterables;
|
|||
import com.google.common.io.CountingOutputStream;
|
||||
import com.google.inject.Inject;
|
||||
import org.apache.calcite.plan.RelOptPlanner;
|
||||
import org.apache.druid.common.exception.SanitizableException;
|
||||
import org.apache.druid.guice.annotations.Json;
|
||||
import org.apache.druid.java.util.common.ISE;
|
||||
import org.apache.druid.java.util.common.StringUtils;
|
||||
|
@ -39,6 +40,7 @@ import org.apache.druid.query.QueryInterruptedException;
|
|||
import org.apache.druid.query.QueryTimeoutException;
|
||||
import org.apache.druid.query.QueryUnsupportedException;
|
||||
import org.apache.druid.query.ResourceLimitExceededException;
|
||||
import org.apache.druid.server.initialization.ServerConfig;
|
||||
import org.apache.druid.server.security.Access;
|
||||
import org.apache.druid.server.security.AuthorizationUtils;
|
||||
import org.apache.druid.server.security.AuthorizerMapper;
|
||||
|
@ -77,19 +79,22 @@ public class SqlResource
|
|||
private final AuthorizerMapper authorizerMapper;
|
||||
private final SqlLifecycleFactory sqlLifecycleFactory;
|
||||
private final SqlLifecycleManager sqlLifecycleManager;
|
||||
private final ServerConfig serverConfig;
|
||||
|
||||
@Inject
|
||||
public SqlResource(
|
||||
@Json ObjectMapper jsonMapper,
|
||||
AuthorizerMapper authorizerMapper,
|
||||
SqlLifecycleFactory sqlLifecycleFactory,
|
||||
SqlLifecycleManager sqlLifecycleManager
|
||||
SqlLifecycleManager sqlLifecycleManager,
|
||||
ServerConfig serverConfig
|
||||
)
|
||||
{
|
||||
this.jsonMapper = Preconditions.checkNotNull(jsonMapper, "jsonMapper");
|
||||
this.authorizerMapper = Preconditions.checkNotNull(authorizerMapper, "authorizerMapper");
|
||||
this.sqlLifecycleFactory = Preconditions.checkNotNull(sqlLifecycleFactory, "sqlLifecycleFactory");
|
||||
this.sqlLifecycleManager = Preconditions.checkNotNull(sqlLifecycleManager, "sqlLifecycleManager");
|
||||
this.serverConfig = serverConfig;
|
||||
}
|
||||
|
||||
@POST
|
||||
|
@ -186,7 +191,7 @@ public class SqlResource
|
|||
}
|
||||
catch (ForbiddenException e) {
|
||||
endLifecycleWithoutEmittingMetrics(sqlQueryId, lifecycle);
|
||||
throw e; // let ForbiddenExceptionMapper handle this
|
||||
throw (ForbiddenException) serverConfig.getErrorResponseTransformStrategy().transformIfNeeded(e); // let ForbiddenExceptionMapper handle this
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn(e, "Failed to handle query: %s", sqlQuery);
|
||||
|
@ -230,11 +235,15 @@ public class SqlResource
|
|||
sqlLifecycleManager.remove(sqlQueryId, lifecycle);
|
||||
}
|
||||
|
||||
private Response buildNonOkResponse(int status, Exception e) throws JsonProcessingException
|
||||
private Response buildNonOkResponse(int status, SanitizableException e) throws JsonProcessingException
|
||||
{
|
||||
return Response.status(status)
|
||||
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||
.entity(jsonMapper.writeValueAsBytes(e))
|
||||
.entity(
|
||||
jsonMapper.writeValueAsBytes(
|
||||
serverConfig.getErrorResponseTransformStrategy().transformIfNeeded(e)
|
||||
)
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
|
|
@ -30,6 +30,8 @@ import com.google.common.util.concurrent.MoreExecutors;
|
|||
import org.apache.calcite.avatica.SqlType;
|
||||
import org.apache.calcite.tools.RelConversionException;
|
||||
import org.apache.druid.common.config.NullHandling;
|
||||
import org.apache.druid.common.exception.AllowedRegexErrorResponseTransformStrategy;
|
||||
import org.apache.druid.common.exception.ErrorResponseTransformStrategy;
|
||||
import org.apache.druid.common.guava.SettableSupplier;
|
||||
import org.apache.druid.jackson.DefaultObjectMapper;
|
||||
import org.apache.druid.java.util.common.ISE;
|
||||
|
@ -121,6 +123,7 @@ public class SqlResourceTest extends CalciteTestBase
|
|||
private HttpServletRequest req;
|
||||
private ListeningExecutorService executorService;
|
||||
private SqlLifecycleManager lifecycleManager;
|
||||
private SqlLifecycleFactory sqlLifecycleFactory;
|
||||
|
||||
private CountDownLatch lifecycleAddLatch;
|
||||
private final SettableSupplier<NonnullPair<CountDownLatch, Boolean>> validateAndAuthorizeLatchSupplier = new SettableSupplier<>();
|
||||
|
@ -236,10 +239,7 @@ public class SqlResourceTest extends CalciteTestBase
|
|||
}
|
||||
};
|
||||
final ServiceEmitter emitter = new NoopServiceEmitter();
|
||||
resource = new SqlResource(
|
||||
JSON_MAPPER,
|
||||
CalciteTests.TEST_AUTHORIZER_MAPPER,
|
||||
new SqlLifecycleFactory(
|
||||
sqlLifecycleFactory = new SqlLifecycleFactory(
|
||||
plannerFactory,
|
||||
emitter,
|
||||
testRequestLogger,
|
||||
|
@ -262,8 +262,13 @@ public class SqlResourceTest extends CalciteTestBase
|
|||
sequenceMapFnSupplier
|
||||
);
|
||||
}
|
||||
},
|
||||
lifecycleManager
|
||||
};
|
||||
resource = new SqlResource(
|
||||
JSON_MAPPER,
|
||||
CalciteTests.TEST_AUTHORIZER_MAPPER,
|
||||
sqlLifecycleFactory,
|
||||
lifecycleManager,
|
||||
new ServerConfig()
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -968,6 +973,44 @@ public class SqlResourceTest extends CalciteTestBase
|
|||
Assert.assertTrue(lifecycleManager.getAll("id").isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnsupportedQueryThrowsExceptionWithFilterResponse() throws Exception
|
||||
{
|
||||
resource = new SqlResource(
|
||||
JSON_MAPPER,
|
||||
CalciteTests.TEST_AUTHORIZER_MAPPER,
|
||||
sqlLifecycleFactory,
|
||||
lifecycleManager,
|
||||
new ServerConfig() {
|
||||
@Override
|
||||
public boolean isShowDetailedJettyErrors()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@Override
|
||||
public ErrorResponseTransformStrategy getErrorResponseTransformStrategy()
|
||||
{
|
||||
return new AllowedRegexErrorResponseTransformStrategy(ImmutableList.of());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
String errorMessage = "This will be support in Druid 9999";
|
||||
SqlQuery badQuery = EasyMock.createMock(SqlQuery.class);
|
||||
EasyMock.expect(badQuery.getQuery()).andReturn("SELECT ANSWER TO LIFE");
|
||||
EasyMock.expect(badQuery.getContext()).andReturn(ImmutableMap.of("sqlQueryId", "id"));
|
||||
EasyMock.expect(badQuery.getParameterList()).andThrow(new QueryUnsupportedException(errorMessage));
|
||||
EasyMock.replay(badQuery);
|
||||
final QueryException exception = doPost(badQuery).lhs;
|
||||
|
||||
Assert.assertNotNull(exception);
|
||||
Assert.assertNull(exception.getMessage());
|
||||
Assert.assertNull(exception.getHost());
|
||||
Assert.assertEquals(exception.getErrorCode(), QueryUnsupportedException.ERROR_CODE);
|
||||
Assert.assertNull(exception.getErrorClass());
|
||||
Assert.assertTrue(lifecycleManager.getAll("id").isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTooManyRequests() throws Exception
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue