Add strict parsing of aggregation ranges (#25769)
Currently we ignore unknown field names when parsing RangeAggregator.Range and GeoDistanceAggregationBuilder.Range from `range`, `date_range` or `geo_distance` aggregations. This can hide subtle errors in the query. This change makes parsing `ranges` stricter.
This commit is contained in:
parent
c0e6dafcab
commit
e24af64de2
|
@ -30,6 +30,7 @@ import org.elasticsearch.common.xcontent.ObjectParser;
|
|||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
import org.elasticsearch.common.xcontent.XContentParser.Token;
|
||||
import org.elasticsearch.common.xcontent.XContentParserUtils;
|
||||
import org.elasticsearch.search.aggregations.AggregationBuilder;
|
||||
import org.elasticsearch.search.aggregations.AggregatorFactories.Builder;
|
||||
import org.elasticsearch.search.aggregations.AggregatorFactory;
|
||||
|
@ -45,6 +46,10 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.elasticsearch.search.aggregations.bucket.range.RangeAggregator.Range.FROM_FIELD;
|
||||
import static org.elasticsearch.search.aggregations.bucket.range.RangeAggregator.Range.KEY_FIELD;
|
||||
import static org.elasticsearch.search.aggregations.bucket.range.RangeAggregator.Range.TO_FIELD;
|
||||
|
||||
public class GeoDistanceAggregationBuilder extends ValuesSourceAggregationBuilder<ValuesSource.GeoPoint, GeoDistanceAggregationBuilder> {
|
||||
public static final String NAME = "geo_distance";
|
||||
static final ParseField ORIGIN_FIELD = new ParseField("origin", "center", "point", "por");
|
||||
|
@ -167,25 +172,38 @@ public class GeoDistanceAggregationBuilder extends ValuesSourceAggregationBuilde
|
|||
double from = 0.0;
|
||||
double to = Double.POSITIVE_INFINITY;
|
||||
String key = null;
|
||||
String toOrFromOrKey = null;
|
||||
String currentFieldName = null;
|
||||
Token token;
|
||||
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
|
||||
if (token == XContentParser.Token.FIELD_NAME) {
|
||||
toOrFromOrKey = parser.currentName();
|
||||
currentFieldName = parser.currentName();
|
||||
} else if (token == XContentParser.Token.VALUE_NUMBER) {
|
||||
if (Range.FROM_FIELD.match(toOrFromOrKey)) {
|
||||
if (FROM_FIELD.match(currentFieldName)) {
|
||||
from = parser.doubleValue();
|
||||
} else if (Range.TO_FIELD.match(toOrFromOrKey)) {
|
||||
} else if (TO_FIELD.match(currentFieldName)) {
|
||||
to = parser.doubleValue();
|
||||
} else {
|
||||
XContentParserUtils.throwUnknownField(currentFieldName, parser.getTokenLocation());
|
||||
}
|
||||
} else if (token == XContentParser.Token.VALUE_STRING) {
|
||||
if (Range.KEY_FIELD.match(toOrFromOrKey)) {
|
||||
if (KEY_FIELD.match(currentFieldName)) {
|
||||
key = parser.text();
|
||||
} else if (Range.FROM_FIELD.match(toOrFromOrKey)) {
|
||||
} else if (FROM_FIELD.match(currentFieldName)) {
|
||||
fromAsStr = parser.text();
|
||||
} else if (Range.TO_FIELD.match(toOrFromOrKey)) {
|
||||
} else if (TO_FIELD.match(currentFieldName)) {
|
||||
toAsStr = parser.text();
|
||||
} else {
|
||||
XContentParserUtils.throwUnknownField(currentFieldName, parser.getTokenLocation());
|
||||
}
|
||||
} else if (token == XContentParser.Token.VALUE_NULL) {
|
||||
if (FROM_FIELD.match(currentFieldName) || TO_FIELD.match(currentFieldName)
|
||||
|| KEY_FIELD.match(currentFieldName)) {
|
||||
// ignore null value
|
||||
} else {
|
||||
XContentParserUtils.throwUnknownField(currentFieldName, parser.getTokenLocation());
|
||||
}
|
||||
} else {
|
||||
XContentParserUtils.throwUnknownToken(token, parser.getTokenLocation());
|
||||
}
|
||||
}
|
||||
if (fromAsStr != null || toAsStr != null) {
|
||||
|
|
|
@ -26,6 +26,7 @@ import org.elasticsearch.common.io.stream.Writeable;
|
|||
import org.elasticsearch.common.xcontent.ToXContent;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
import org.elasticsearch.common.xcontent.XContentParserUtils;
|
||||
import org.elasticsearch.index.fielddata.SortedNumericDoubleValues;
|
||||
import org.elasticsearch.search.DocValueFormat;
|
||||
import org.elasticsearch.search.aggregations.Aggregator;
|
||||
|
@ -143,6 +144,8 @@ public class RangeAggregator extends BucketsAggregator {
|
|||
from = parser.doubleValue();
|
||||
} else if (TO_FIELD.match(currentFieldName)) {
|
||||
to = parser.doubleValue();
|
||||
} else {
|
||||
XContentParserUtils.throwUnknownField(currentFieldName, parser.getTokenLocation());
|
||||
}
|
||||
} else if (token == XContentParser.Token.VALUE_STRING) {
|
||||
if (FROM_FIELD.match(currentFieldName)) {
|
||||
|
@ -151,7 +154,18 @@ public class RangeAggregator extends BucketsAggregator {
|
|||
toAsStr = parser.text();
|
||||
} else if (KEY_FIELD.match(currentFieldName)) {
|
||||
key = parser.text();
|
||||
} else {
|
||||
XContentParserUtils.throwUnknownField(currentFieldName, parser.getTokenLocation());
|
||||
}
|
||||
} else if (token == XContentParser.Token.VALUE_NULL) {
|
||||
if (FROM_FIELD.match(currentFieldName) || TO_FIELD.match(currentFieldName)
|
||||
|| KEY_FIELD.match(currentFieldName)) {
|
||||
// ignore null value
|
||||
} else {
|
||||
XContentParserUtils.throwUnknownField(currentFieldName, parser.getTokenLocation());
|
||||
}
|
||||
} else {
|
||||
XContentParserUtils.throwUnknownToken(token, parser.getTokenLocation());
|
||||
}
|
||||
}
|
||||
return new Range(key, from, fromAsStr, to, toAsStr);
|
||||
|
|
|
@ -19,10 +19,17 @@
|
|||
|
||||
package org.elasticsearch.search.aggregations.bucket;
|
||||
|
||||
import org.elasticsearch.common.ParsingException;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
import org.elasticsearch.common.xcontent.json.JsonXContent;
|
||||
import org.elasticsearch.search.aggregations.BaseAggregationTestCase;
|
||||
import org.elasticsearch.search.aggregations.bucket.range.DateRangeAggregationBuilder;
|
||||
import org.elasticsearch.search.aggregations.bucket.range.RangeAggregator.Range;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
|
||||
public class DateRangeTests extends BaseAggregationTestCase<DateRangeAggregationBuilder> {
|
||||
|
||||
@Override
|
||||
|
@ -62,4 +69,17 @@ public class DateRangeTests extends BaseAggregationTestCase<DateRangeAggregation
|
|||
return factory;
|
||||
}
|
||||
|
||||
public void testParsingRangeStrict() throws IOException {
|
||||
final String rangeAggregation = "{\n" +
|
||||
"\"field\" : \"date\",\n" +
|
||||
"\"format\" : \"yyyy-MM-dd\",\n" +
|
||||
"\"ranges\" : [\n" +
|
||||
" { \"from\" : \"2017-01-01\", \"to\" : \"2017-01-02\", \"badField\" : \"abcd\" }\n" +
|
||||
"]\n" +
|
||||
"}";
|
||||
XContentParser parser = createParser(JsonXContent.jsonXContent, rangeAggregation);
|
||||
ParsingException ex = expectThrows(ParsingException.class, () -> DateRangeAggregationBuilder.parse("aggregationName", parser));
|
||||
assertThat(ex.getDetailedMessage(), containsString("badField"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -19,14 +19,21 @@
|
|||
|
||||
package org.elasticsearch.search.aggregations.bucket;
|
||||
|
||||
import org.elasticsearch.common.ParsingException;
|
||||
import org.elasticsearch.common.geo.GeoDistance;
|
||||
import org.elasticsearch.common.geo.GeoPoint;
|
||||
import org.elasticsearch.common.unit.DistanceUnit;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
import org.elasticsearch.common.xcontent.json.JsonXContent;
|
||||
import org.elasticsearch.search.aggregations.BaseAggregationTestCase;
|
||||
import org.elasticsearch.search.aggregations.bucket.range.GeoDistanceAggregationBuilder;
|
||||
import org.elasticsearch.search.aggregations.bucket.range.GeoDistanceAggregationBuilder.Range;
|
||||
import org.elasticsearch.test.geo.RandomShapeGenerator;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
|
||||
public class GeoDistanceRangeTests extends BaseAggregationTestCase<GeoDistanceAggregationBuilder> {
|
||||
|
||||
@Override
|
||||
|
@ -61,4 +68,37 @@ public class GeoDistanceRangeTests extends BaseAggregationTestCase<GeoDistanceAg
|
|||
return factory;
|
||||
}
|
||||
|
||||
public void testParsingRangeStrict() throws IOException {
|
||||
final String rangeAggregation = "{\n" +
|
||||
"\"field\" : \"location\",\n" +
|
||||
"\"origin\" : \"52.3760, 4.894\",\n" +
|
||||
"\"unit\" : \"m\",\n" +
|
||||
"\"ranges\" : [\n" +
|
||||
" { \"from\" : 10000, \"to\" : 20000, \"badField\" : \"abcd\" }\n" +
|
||||
"]\n" +
|
||||
"}";
|
||||
XContentParser parser = createParser(JsonXContent.jsonXContent, rangeAggregation);
|
||||
ParsingException ex = expectThrows(ParsingException.class, () -> GeoDistanceAggregationBuilder.parse("aggregationName", parser));
|
||||
assertThat(ex.getDetailedMessage(), containsString("badField"));
|
||||
}
|
||||
|
||||
/**
|
||||
* We never render "null" values to xContent, but we should test that we can parse them (and they return correct defaults)
|
||||
*/
|
||||
public void testParsingNull() throws IOException {
|
||||
final String rangeAggregation = "{\n" +
|
||||
"\"field\" : \"location\",\n" +
|
||||
"\"origin\" : \"52.3760, 4.894\",\n" +
|
||||
"\"unit\" : \"m\",\n" +
|
||||
"\"ranges\" : [\n" +
|
||||
" { \"from\" : null, \"to\" : null }\n" +
|
||||
"]\n" +
|
||||
"}";
|
||||
XContentParser parser = createParser(JsonXContent.jsonXContent, rangeAggregation);
|
||||
GeoDistanceAggregationBuilder aggregationBuilder = (GeoDistanceAggregationBuilder) GeoDistanceAggregationBuilder
|
||||
.parse("aggregationName", parser);
|
||||
assertEquals(1, aggregationBuilder.range().size());
|
||||
assertEquals(0.0, aggregationBuilder.range().get(0).getFrom(), 0.0);
|
||||
assertEquals(Double.POSITIVE_INFINITY, aggregationBuilder.range().get(0).getTo(), 0.0);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,9 +19,16 @@
|
|||
|
||||
package org.elasticsearch.search.aggregations.bucket;
|
||||
|
||||
import org.elasticsearch.common.ParsingException;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
import org.elasticsearch.common.xcontent.json.JsonXContent;
|
||||
import org.elasticsearch.search.aggregations.BaseAggregationTestCase;
|
||||
import org.elasticsearch.search.aggregations.bucket.range.RangeAggregator.Range;
|
||||
import org.elasticsearch.search.aggregations.bucket.range.RangeAggregationBuilder;
|
||||
import org.elasticsearch.search.aggregations.bucket.range.RangeAggregator.Range;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
|
||||
public class RangeTests extends BaseAggregationTestCase<RangeAggregationBuilder> {
|
||||
|
||||
|
@ -59,4 +66,32 @@ public class RangeTests extends BaseAggregationTestCase<RangeAggregationBuilder>
|
|||
return factory;
|
||||
}
|
||||
|
||||
public void testParsingRangeStrict() throws IOException {
|
||||
final String rangeAggregation = "{\n" +
|
||||
"\"field\" : \"price\",\n" +
|
||||
"\"ranges\" : [\n" +
|
||||
" { \"from\" : 50, \"to\" : 100, \"badField\" : \"abcd\" }\n" +
|
||||
"]\n" +
|
||||
"}";
|
||||
XContentParser parser = createParser(JsonXContent.jsonXContent, rangeAggregation);
|
||||
ParsingException ex = expectThrows(ParsingException.class, () -> RangeAggregationBuilder.parse("aggregationName", parser));
|
||||
assertThat(ex.getDetailedMessage(), containsString("badField"));
|
||||
}
|
||||
|
||||
/**
|
||||
* We never render "null" values to xContent, but we should test that we can parse them (and they return correct defaults)
|
||||
*/
|
||||
public void testParsingNull() throws IOException {
|
||||
final String rangeAggregation = "{\n" +
|
||||
"\"field\" : \"price\",\n" +
|
||||
"\"ranges\" : [\n" +
|
||||
" { \"from\" : null, \"to\" : null }\n" +
|
||||
"]\n" +
|
||||
"}";
|
||||
XContentParser parser = createParser(JsonXContent.jsonXContent, rangeAggregation);
|
||||
RangeAggregationBuilder aggregationBuilder = (RangeAggregationBuilder) RangeAggregationBuilder.parse("aggregationName", parser);
|
||||
assertEquals(1, aggregationBuilder.ranges().size());
|
||||
assertEquals(Double.NEGATIVE_INFINITY, aggregationBuilder.ranges().get(0).getFrom(), 0.0);
|
||||
assertEquals(Double.POSITIVE_INFINITY, aggregationBuilder.ranges().get(0).getTo(), 0.0);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue