Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -14,6 +14,7 @@
import org.elasticsearch.xpack.application.connector.ConnectorTemplateRegistry;
import org.elasticsearch.xpack.application.rules.action.ListQueryRulesetsAction;
import org.elasticsearch.xpack.application.rules.retriever.QueryRuleRetrieverBuilder;
import org.elasticsearch.xpack.application.rules.QueryRule;

import java.util.Map;
import java.util.Set;
Expand All @@ -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