From d2f9337b7002e2a742db41a2017c337e5e71bccf Mon Sep 17 00:00:00 2001 From: Zachary Tong Date: Wed, 2 May 2018 14:39:49 +0000 Subject: [PATCH 1/3] [Rollup] Validate timezone in range queries When validating the search request, we make sure any date_histogram aggregations have timezones that match the jobs. But we didn't do any such validation on range queries. While it wouldn't produce incorrect results, it would be confusing to the user as no documents would match the aggregation (because we add a filter clause on the timezone for the agg). Now the user gets an exception up front, and some helpful text about why the range query didnt match, and which timezones are acceptable --- .../action/TransportRollupSearchAction.java | 29 ++++++++++++++++- .../rollup/action/SearchActionTests.java | 32 +++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/action/TransportRollupSearchAction.java b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/action/TransportRollupSearchAction.java index ef8c8bdb8c5fc..9dcc2e482d079 100644 --- a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/action/TransportRollupSearchAction.java +++ b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/action/TransportRollupSearchAction.java @@ -56,10 +56,12 @@ import org.elasticsearch.xpack.core.rollup.RollupField; import org.elasticsearch.xpack.core.rollup.action.RollupJobCaps; import org.elasticsearch.xpack.core.rollup.action.RollupSearchAction; +import org.elasticsearch.xpack.core.rollup.job.DateHistoGroupConfig; import org.elasticsearch.xpack.rollup.Rollup; import org.elasticsearch.xpack.rollup.RollupJobIdentifierUtils; import org.elasticsearch.xpack.rollup.RollupRequestTranslator; import org.elasticsearch.xpack.rollup.RollupResponseTranslator; +import org.joda.time.DateTimeZone; import java.io.IOException; import java.util.ArrayList; @@ -277,6 +279,7 @@ static QueryBuilder rewriteQuery(QueryBuilder builder, Set jobCap ? ((RangeQueryBuilder)builder).fieldName() : ((TermQueryBuilder)builder).fieldName(); + List incorrectTimeZones = new ArrayList<>(); List rewrittenFieldName = jobCaps.stream() // We only care about job caps that have the query's target field .filter(caps -> caps.getFieldCaps().keySet().contains(fieldName)) @@ -286,6 +289,24 @@ static QueryBuilder rewriteQuery(QueryBuilder builder, Set jobCap // For now, we only allow filtering on grouping fields .filter(agg -> { String type = (String)agg.get(RollupField.AGG); + + // If the cap is for a date_histo, and the query is a range, the timezones need to match + if (type.equals(DateHistogramAggregationBuilder.NAME) && builder instanceof RangeQueryBuilder) { + String timeZone = ((RangeQueryBuilder)builder).timeZone(); + + // Many range queries don't include the timezone because the default is UTC, but the query + // builder will return null so we need to set it here + if (timeZone == null) { + timeZone = DateTimeZone.UTC.toString(); + } + boolean matchingTZ = ((String)agg.get(DateHistoGroupConfig.TIME_ZONE.getPreferredName())) + .equalsIgnoreCase(timeZone); + if (matchingTZ == false) { + incorrectTimeZones.add((String)agg.get(DateHistoGroupConfig.TIME_ZONE.getPreferredName())); + } + return matchingTZ; + } + // Otherwise just make sure it's one of the three groups return type.equals(TermsAggregationBuilder.NAME) || type.equals(DateHistogramAggregationBuilder.NAME) || type.equals(HistogramAggregationBuilder.NAME); @@ -304,8 +325,14 @@ static QueryBuilder rewriteQuery(QueryBuilder builder, Set jobCap .collect(ArrayList::new, List::addAll, List::addAll); if (rewrittenFieldName.isEmpty()) { - throw new IllegalArgumentException("Field [" + fieldName + "] in [" + builder.getWriteableName() + if (incorrectTimeZones.isEmpty()) { + throw new IllegalArgumentException("Field [" + fieldName + "] in [" + builder.getWriteableName() + "] query is not available in selected rollup indices, cannot query."); + } else { + throw new IllegalArgumentException("Field [" + fieldName + "] in [" + builder.getWriteableName() + + "] query was found in rollup indices, but requested timezone is not compatible. Options include: " + + incorrectTimeZones); + } } if (rewrittenFieldName.size() > 1) { diff --git a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/SearchActionTests.java b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/SearchActionTests.java index 23d60ef01e4f2..1957263e0d70c 100644 --- a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/SearchActionTests.java +++ b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/SearchActionTests.java @@ -114,6 +114,24 @@ public void testBadQuery() { } public void testRange() { + RollupJobConfig.Builder job = ConfigTestHelpers.getRollupJob("foo"); + GroupConfig.Builder group = ConfigTestHelpers.getGroupConfig(); + group.setDateHisto(new DateHistoGroupConfig.Builder().setField("foo").setInterval(new DateHistogramInterval("1h")).build()); + job.setGroupConfig(group.build()); + RollupJobCaps cap = new RollupJobCaps(job.build()); + Set caps = new HashSet<>(); + caps.add(cap); + QueryBuilder rewritten = null; + try { + rewritten = TransportRollupSearchAction.rewriteQuery(new RangeQueryBuilder("foo").gt(1).timeZone("UTC"), caps); + } catch (Exception e) { + fail("Should not have thrown exception when parsing query."); + } + assertThat(rewritten, instanceOf(RangeQueryBuilder.class)); + assertThat(((RangeQueryBuilder)rewritten).fieldName(), equalTo("foo.date_histogram.timestamp")); + } + + public void testRangeNullTimeZone() { RollupJobConfig.Builder job = ConfigTestHelpers.getRollupJob("foo"); GroupConfig.Builder group = ConfigTestHelpers.getGroupConfig(); group.setDateHisto(new DateHistoGroupConfig.Builder().setField("foo").setInterval(new DateHistogramInterval("1h")).build()); @@ -131,6 +149,20 @@ public void testRange() { assertThat(((RangeQueryBuilder)rewritten).fieldName(), equalTo("foo.date_histogram.timestamp")); } + public void testRangeWrongTZ() { + RollupJobConfig.Builder job = ConfigTestHelpers.getRollupJob("foo"); + GroupConfig.Builder group = ConfigTestHelpers.getGroupConfig(); + group.setDateHisto(new DateHistoGroupConfig.Builder().setField("foo").setInterval(new DateHistogramInterval("1h")).build()); + job.setGroupConfig(group.build()); + RollupJobCaps cap = new RollupJobCaps(job.build()); + Set caps = new HashSet<>(); + caps.add(cap); + Exception e = expectThrows(IllegalArgumentException.class, + () -> TransportRollupSearchAction.rewriteQuery(new RangeQueryBuilder("foo").gt(1).timeZone("EST"), caps)); + assertThat(e.getMessage(), equalTo("Field [foo] in [range] query was found in rollup indices, but requested timezone is not " + + "compatible. Options include: [UTC]")); + } + public void testTerms() { RollupJobConfig.Builder job = ConfigTestHelpers.getRollupJob("foo"); GroupConfig.Builder group = ConfigTestHelpers.getGroupConfig(); From 9fab5a4868bc4d6c04193fe1361816afb13d626c Mon Sep 17 00:00:00 2001 From: Zachary Tong Date: Thu, 3 May 2018 15:47:41 +0000 Subject: [PATCH 2/3] No need to catch exceptions, just allow tests to throw --- .../rollup/action/SearchActionTests.java | 36 ++++--------------- 1 file changed, 6 insertions(+), 30 deletions(-) diff --git a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/SearchActionTests.java b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/SearchActionTests.java index 1957263e0d70c..d9d3e672a0afc 100644 --- a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/SearchActionTests.java +++ b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/SearchActionTests.java @@ -121,12 +121,7 @@ public void testRange() { RollupJobCaps cap = new RollupJobCaps(job.build()); Set caps = new HashSet<>(); caps.add(cap); - QueryBuilder rewritten = null; - try { - rewritten = TransportRollupSearchAction.rewriteQuery(new RangeQueryBuilder("foo").gt(1).timeZone("UTC"), caps); - } catch (Exception e) { - fail("Should not have thrown exception when parsing query."); - } + QueryBuilder rewritten = TransportRollupSearchAction.rewriteQuery(new RangeQueryBuilder("foo").gt(1).timeZone("UTC"), caps); assertThat(rewritten, instanceOf(RangeQueryBuilder.class)); assertThat(((RangeQueryBuilder)rewritten).fieldName(), equalTo("foo.date_histogram.timestamp")); } @@ -139,12 +134,7 @@ public void testRangeNullTimeZone() { RollupJobCaps cap = new RollupJobCaps(job.build()); Set caps = new HashSet<>(); caps.add(cap); - QueryBuilder rewritten = null; - try { - rewritten = TransportRollupSearchAction.rewriteQuery(new RangeQueryBuilder("foo").gt(1), caps); - } catch (Exception e) { - fail("Should not have thrown exception when parsing query."); - } + QueryBuilder rewritten = TransportRollupSearchAction.rewriteQuery(new RangeQueryBuilder("foo").gt(1), caps); assertThat(rewritten, instanceOf(RangeQueryBuilder.class)); assertThat(((RangeQueryBuilder)rewritten).fieldName(), equalTo("foo.date_histogram.timestamp")); } @@ -171,12 +161,7 @@ public void testTerms() { RollupJobCaps cap = new RollupJobCaps(job.build()); Set caps = new HashSet<>(); caps.add(cap); - QueryBuilder rewritten = null; - try { - rewritten = TransportRollupSearchAction.rewriteQuery(new TermQueryBuilder("foo", "bar"), caps); - } catch (Exception e) { - fail("Should not have thrown exception when parsing query."); - } + QueryBuilder rewritten = TransportRollupSearchAction.rewriteQuery(new TermQueryBuilder("foo", "bar"), caps); assertThat(rewritten, instanceOf(TermQueryBuilder.class)); assertThat(((TermQueryBuilder)rewritten).fieldName(), equalTo("foo.terms.value")); } @@ -192,12 +177,7 @@ public void testCompounds() { BoolQueryBuilder builder = new BoolQueryBuilder(); builder.must(getQueryBuilder(2)); - QueryBuilder rewritten = null; - try { - rewritten = TransportRollupSearchAction.rewriteQuery(builder, caps); - } catch (Exception e) { - fail("Should not have thrown exception when parsing query."); - } + QueryBuilder rewritten = TransportRollupSearchAction.rewriteQuery(builder, caps); assertThat(rewritten, instanceOf(BoolQueryBuilder.class)); assertThat(((BoolQueryBuilder)rewritten).must().size(), equalTo(1)); } @@ -210,12 +190,8 @@ public void testMatchAll() { RollupJobCaps cap = new RollupJobCaps(job.build()); Set caps = new HashSet<>(); caps.add(cap); - try { - QueryBuilder rewritten = TransportRollupSearchAction.rewriteQuery(new MatchAllQueryBuilder(), caps); - assertThat(rewritten, instanceOf(MatchAllQueryBuilder.class)); - } catch (Exception e) { - fail("Should not have thrown exception when parsing query."); - } + QueryBuilder rewritten = TransportRollupSearchAction.rewriteQuery(new MatchAllQueryBuilder(), caps); + assertThat(rewritten, instanceOf(MatchAllQueryBuilder.class)); } public void testAmbiguousResolution() { From 3789e43a84275b824c32ed6d477219ee85c751e9 Mon Sep 17 00:00:00 2001 From: Zachary Tong Date: Fri, 4 May 2018 14:07:34 +0000 Subject: [PATCH 3/3] [DOCS] Update Changelog --- docs/CHANGELOG.asciidoc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc index 9e33c10a56b12..f4ecaf44a6c5a 100644 --- a/docs/CHANGELOG.asciidoc +++ b/docs/CHANGELOG.asciidoc @@ -107,6 +107,10 @@ ones that the user is authorized to access in case field level security is enabl Fixed prerelease version of elasticsearch in the `deb` package to sort before GA versions ({pull}29000[#29000]) +Rollup:: +* Validate timezone in range queries to ensure they match the selected job when +searching ({pull}30338[#30338]) + [float] === Regressions Fail snapshot operations early when creating or deleting a snapshot on a repository that has been @@ -167,6 +171,10 @@ Machine Learning:: * Account for gaps in data counts after job is reopened ({pull}30294[#30294]) +Rollup:: +* Validate timezone in range queries to ensure they match the selected job when +searching ({pull}30338[#30338]) + //[float] //=== Regressions