Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/changelog/122823.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 122823
summary: Prevent Query Rule Creation with Invalid Numeric Match Criteria
area: Relevance
type: bug
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ teardown:
ruleset_id: test-query-ruleset-recreating
ignore: 404

- do:
query_rules.delete_ruleset:
ruleset_id: invalid_numeric_test
ignore: 404

- do:
query_rules.delete_ruleset:
ruleset_id: multiple_values_test
ignore: 404

---
'Create Query Ruleset':
- do:
Expand Down Expand Up @@ -160,3 +170,51 @@ teardown:
- 'id2'

- match: { error.type: 'security_exception' }

---
"Test numeric comparison rule validation":
- skip:
features: headers

- requires:
cluster_features: "query_rules.numeric_validation"
reason: "Fixed bug to enforce numeric rule validation starting with 8.19"

- do:
catch: /Input \[not_a_number\] is not valid for CriteriaType \[lte\]/
query_rules.put_ruleset:
ruleset_id: invalid_numeric_test
body:
rules:
- rule_id: invalid_numeric_rule
type: pinned
criteria:
- type: lte
metadata: price
values: ["not_a_number"]
actions:
ids: ["1"]

---
"Test numeric comparison with multiple values":
- skip:
features: headers

- requires:
cluster_features: "query_rules.numeric_validation"
reason: "Fixed bug to enforce numeric rule validation starting with 8.19"

- do:
catch: /Input \[not_a_number\] is not valid for CriteriaType \[lte\]/
query_rules.put_ruleset:
ruleset_id: multiple_values_test
body:
rules:
- rule_id: multiple_values_rule
type: pinned
criteria:
- type: lte
metadata: price
values: ["100", "not_a_number"]
actions:
ids: ["1"]
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.elasticsearch.features.NodeFeature;
import org.elasticsearch.xpack.application.analytics.AnalyticsTemplateRegistry;
import org.elasticsearch.xpack.application.connector.ConnectorTemplateRegistry;
import org.elasticsearch.xpack.application.rules.QueryRule;
import org.elasticsearch.xpack.application.rules.action.ListQueryRulesetsAction;
import org.elasticsearch.xpack.application.rules.retriever.QueryRuleRetrieverBuilder;

Expand Down Expand Up @@ -40,4 +41,9 @@ public Map<NodeFeature, Version> getHistoricalFeatures() {
Version.V_8_12_0
);
}

@Override
public Set<NodeFeature> getTestFeatures() {
return Set.of(QueryRule.NUMERIC_VALIDATION);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.features.NodeFeature;
import org.elasticsearch.xcontent.ConstructingObjectParser;
import org.elasticsearch.xcontent.ParseField;
import org.elasticsearch.xcontent.ToXContentObject;
Expand Down Expand Up @@ -81,6 +82,8 @@ public String toString() {
}
}

public static final NodeFeature NUMERIC_VALIDATION = new NodeFeature("query_rules.numeric_validation", true);

/**
* Public constructor.
*
Expand Down Expand Up @@ -140,7 +143,6 @@ public QueryRule(StreamInput in) throws IOException {
}

private void validate() {

if (priority != null && (priority < MIN_PRIORITY || priority > MAX_PRIORITY)) {
throw new IllegalArgumentException("Priority was " + priority + ", must be between " + MIN_PRIORITY + " and " + MAX_PRIORITY);
}
Expand All @@ -155,6 +157,13 @@ private void validate() {
throw new ElasticsearchParseException(type.toString() + " query rule actions must contain only one of either ids or docs");
}
}

criteria.forEach(criterion -> {
List<Object> values = criterion.criteriaValues();
if (values != null) {
values.forEach(criterion.criteriaType()::validateInput);
}
});
}

private void validateIdOrDocAction(Object action) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,26 @@
import org.elasticsearch.xpack.application.search.TemplateParamValidator;
import org.elasticsearch.xpack.core.action.util.PageParams;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import static org.elasticsearch.test.ESTestCase.generateRandomStringArray;
import static org.elasticsearch.test.ESTestCase.randomAlphaOfLengthBetween;
import static org.elasticsearch.test.ESTestCase.randomBoolean;
import static org.elasticsearch.test.ESTestCase.randomDoubleBetween;
import static org.elasticsearch.test.ESTestCase.randomFrom;
import static org.elasticsearch.test.ESTestCase.randomIdentifier;
import static org.elasticsearch.test.ESTestCase.randomIntBetween;
import static org.elasticsearch.test.ESTestCase.randomList;
import static org.elasticsearch.test.ESTestCase.randomLongBetween;
import static org.elasticsearch.test.ESTestCase.randomMap;
import static org.elasticsearch.xpack.application.rules.QueryRule.MAX_PRIORITY;
import static org.elasticsearch.xpack.application.rules.QueryRule.MIN_PRIORITY;
import static org.elasticsearch.xpack.application.rules.QueryRuleCriteriaType.ALWAYS;

public final class EnterpriseSearchModuleTestUtils {

Expand Down Expand Up @@ -86,19 +87,48 @@ public static Map<String, Object> randomSearchApplicationQueryParams() {
return randomMap(0, 10, () -> Tuple.tuple(randomIdentifier(), randomAlphaOfLengthBetween(0, 10)));
}

public static QueryRuleCriteria randomQueryRuleCriteria() {
// We intentionally don't allow ALWAYS criteria in this method, since we want to test parsing metadata and values
QueryRuleCriteriaType type = randomFrom(Arrays.stream(QueryRuleCriteriaType.values()).filter(t -> t != ALWAYS).toList());
return new QueryRuleCriteria(type, randomAlphaOfLengthBetween(1, 10), randomList(1, 5, () -> randomAlphaOfLengthBetween(1, 10)));
}

public static QueryRule randomQueryRule() {
String id = randomIdentifier();
String ruleId = randomIdentifier();
QueryRule.QueryRuleType type = randomFrom(QueryRule.QueryRuleType.values());
List<QueryRuleCriteria> criteria = List.of(randomQueryRuleCriteria());
Map<String, Object> actions = Map.of(randomFrom("ids", "docs"), List.of(randomAlphaOfLengthBetween(2, 10)));
Integer priority = randomQueryRulePriority();
return new QueryRule(id, type, criteria, actions, priority);
List<QueryRuleCriteria> criteria = new ArrayList<>();
int numCriteria = randomIntBetween(1, 3);

for (int i = 0; i < numCriteria; i++) {
criteria.add(randomQueryRuleCriteria());
}

return new QueryRule(ruleId, type, criteria, randomQueryRuleActions(), randomQueryRulePriority());
}

public static QueryRuleCriteria randomQueryRuleCriteria() {
Set<QueryRuleCriteriaType> numericValueCriteriaType = Set.of(
QueryRuleCriteriaType.GT,
QueryRuleCriteriaType.GTE,
QueryRuleCriteriaType.LT,
QueryRuleCriteriaType.LTE
);
QueryRuleCriteriaType criteriaType = randomFrom(QueryRuleCriteriaType.values());

String metadata;
List<Object> values;
if (criteriaType == QueryRuleCriteriaType.ALWAYS) {
metadata = null;
values = null;
} else {
metadata = randomAlphaOfLengthBetween(3, 10);
values = new ArrayList<>();

int numValues = randomIntBetween(1, 3);
for (int i = 0; i < numValues; i++) {
values.add(
numericValueCriteriaType.contains(criteriaType)
? BigDecimal.valueOf(randomDoubleBetween(0, 1000, true)).setScale(2, RoundingMode.HALF_UP).toString()
: randomAlphaOfLengthBetween(3, 10)
);
}
}

return new QueryRuleCriteria(criteriaType, metadata, values);
}

public static Integer randomQueryRulePriority() {
Expand All @@ -115,8 +145,24 @@ public static QueryRuleset randomQueryRuleset() {
return new QueryRuleset(id, rules);
}

public static Map<String, Object> randomQueryRuleActions() {
if (randomBoolean()) {
return Map.of(QueryRule.IDS_FIELD.getPreferredName(), List.of(randomAlphaOfLengthBetween(3, 10)));
}
return Map.of(
QueryRule.DOCS_FIELD.getPreferredName(),
List.of(
Map.of(
QueryRule.INDEX_FIELD.getPreferredName(),
randomAlphaOfLengthBetween(3, 10),
QueryRule.ID_FIELD.getPreferredName(),
randomAlphaOfLengthBetween(3, 10)
)
)
);
}

public static Map<String, Object> randomMatchCriteria() {
return randomMap(1, 3, () -> Tuple.tuple(randomIdentifier(), randomAlphaOfLengthBetween(0, 10)));
}

}
Loading