Skip to content

Commit 777f4b3

Browse files
authored
Validate query field when creating roles (#46275)
In the current implementation, the validation of the role query occurs at runtime when the query is being executed. This commit adds validation for the role query when creating a role but not for the template query as we do not have the runtime information required for evaluating the template query (eg. authenticated user's information). This is similar to the scripts that we store but do not evaluate or parse if they are valid queries or not. For validation, the query is evaluated (if not a template), parsed to build the QueryBuilder and verify if the query type is allowed. Closes #34252
1 parent f903d92 commit 777f4b3

File tree

23 files changed

+627
-235
lines changed

23 files changed

+627
-235
lines changed

client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityIT.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ private static Role randomRole(String roleName) {
190190
.name(roleName)
191191
.clusterPrivileges(randomSubsetOf(randomInt(3), Role.ClusterPrivilegeName.ALL_ARRAY))
192192
.indicesPrivileges(
193-
randomArray(3, IndicesPrivileges[]::new, () -> IndicesPrivilegesTests.createNewRandom(randomAlphaOfLength(3))))
193+
randomArray(3, IndicesPrivileges[]::new, () -> IndicesPrivilegesTests.createNewRandom("{\"match_all\": {}}")))
194194
.applicationResourcePrivileges(randomArray(3, ApplicationResourcePrivileges[]::new,
195195
() -> ApplicationResourcePrivilegesTests.createNewRandom(randomAlphaOfLength(3).toLowerCase(Locale.ROOT))))
196196
.runAsPrivilege(randomArray(3, String[]::new, () -> randomAlphaOfLength(3)));

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/DocumentPermissions.java

+6-63
Original file line numberDiff line numberDiff line change
@@ -12,29 +12,18 @@
1212
import org.apache.lucene.search.join.ToChildBlockJoinQuery;
1313
import org.elasticsearch.common.bytes.BytesReference;
1414
import org.elasticsearch.common.lucene.search.Queries;
15-
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
16-
import org.elasticsearch.common.xcontent.XContentFactory;
17-
import org.elasticsearch.common.xcontent.XContentParser;
18-
import org.elasticsearch.index.query.BoolQueryBuilder;
19-
import org.elasticsearch.index.query.BoostingQueryBuilder;
20-
import org.elasticsearch.index.query.ConstantScoreQueryBuilder;
21-
import org.elasticsearch.index.query.GeoShapeQueryBuilder;
2215
import org.elasticsearch.index.query.QueryBuilder;
2316
import org.elasticsearch.index.query.QueryRewriteContext;
2417
import org.elasticsearch.index.query.QueryShardContext;
2518
import org.elasticsearch.index.query.Rewriteable;
26-
import org.elasticsearch.index.query.TermsQueryBuilder;
27-
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
2819
import org.elasticsearch.index.search.NestedHelper;
2920
import org.elasticsearch.index.shard.ShardId;
3021
import org.elasticsearch.script.ScriptService;
31-
import org.elasticsearch.xpack.core.security.authz.support.SecurityQueryTemplateEvaluator;
22+
import org.elasticsearch.xpack.core.security.authz.support.DLSRoleQueryValidator;
3223
import org.elasticsearch.xpack.core.security.user.User;
3324

3425
import java.io.IOException;
35-
import java.util.ArrayList;
3626
import java.util.Collections;
37-
import java.util.List;
3827
import java.util.Set;
3928
import java.util.function.Function;
4029

@@ -127,23 +116,21 @@ private static void buildRoleQuery(User user, ScriptService scriptService, Shard
127116
BooleanQuery.Builder filter) throws IOException {
128117
for (BytesReference bytesReference : queries) {
129118
QueryShardContext queryShardContext = queryShardContextProvider.apply(shardId);
130-
String templateResult = SecurityQueryTemplateEvaluator.evaluateTemplate(bytesReference.utf8ToString(), scriptService, user);
131-
try (XContentParser parser = XContentFactory.xContent(templateResult).createParser(queryShardContext.getXContentRegistry(),
132-
LoggingDeprecationHandler.INSTANCE, templateResult)) {
133-
QueryBuilder queryBuilder = queryShardContext.parseInnerQueryBuilder(parser);
134-
verifyRoleQuery(queryBuilder);
119+
QueryBuilder queryBuilder = DLSRoleQueryValidator.evaluateAndVerifyRoleQuery(bytesReference, scriptService,
120+
queryShardContext.getXContentRegistry(), user);
121+
if (queryBuilder != null) {
135122
failIfQueryUsesClient(queryBuilder, queryShardContext);
136123
Query roleQuery = queryShardContext.toQuery(queryBuilder).query();
137124
filter.add(roleQuery, SHOULD);
138125
if (queryShardContext.getMapperService().hasNested()) {
139126
NestedHelper nestedHelper = new NestedHelper(queryShardContext.getMapperService());
140127
if (nestedHelper.mightMatchNestedDocs(roleQuery)) {
141128
roleQuery = new BooleanQuery.Builder().add(roleQuery, FILTER)
142-
.add(Queries.newNonNestedFilter(), FILTER).build();
129+
.add(Queries.newNonNestedFilter(), FILTER).build();
143130
}
144131
// If access is allowed on root doc then also access is allowed on all nested docs of that root document:
145132
BitSetProducer rootDocs = queryShardContext
146-
.bitsetFilter(Queries.newNonNestedFilter());
133+
.bitsetFilter(Queries.newNonNestedFilter());
147134
ToChildBlockJoinQuery includeNestedDocs = new ToChildBlockJoinQuery(roleQuery, rootDocs);
148135
filter.add(includeNestedDocs, SHOULD);
149136
}
@@ -153,50 +140,6 @@ private static void buildRoleQuery(User user, ScriptService scriptService, Shard
153140
filter.setMinimumNumberShouldMatch(1);
154141
}
155142

156-
/**
157-
* Checks whether the role query contains queries we know can't be used as DLS role query.
158-
*/
159-
static void verifyRoleQuery(QueryBuilder queryBuilder) throws IOException {
160-
if (queryBuilder instanceof TermsQueryBuilder) {
161-
TermsQueryBuilder termsQueryBuilder = (TermsQueryBuilder) queryBuilder;
162-
if (termsQueryBuilder.termsLookup() != null) {
163-
throw new IllegalArgumentException("terms query with terms lookup isn't supported as part of a role query");
164-
}
165-
} else if (queryBuilder instanceof GeoShapeQueryBuilder) {
166-
GeoShapeQueryBuilder geoShapeQueryBuilder = (GeoShapeQueryBuilder) queryBuilder;
167-
if (geoShapeQueryBuilder.shape() == null) {
168-
throw new IllegalArgumentException("geoshape query referring to indexed shapes isn't support as part of a role query");
169-
}
170-
} else if (queryBuilder.getName().equals("percolate")) {
171-
// actually only if percolate query is referring to an existing document then this is problematic,
172-
// a normal percolate query does work. However we can't check that here as this query builder is inside
173-
// another module. So we don't allow the entire percolate query. I don't think users would ever use
174-
// a percolate query as role query, so this restriction shouldn't prohibit anyone from using dls.
175-
throw new IllegalArgumentException("percolate query isn't support as part of a role query");
176-
} else if (queryBuilder.getName().equals("has_child")) {
177-
throw new IllegalArgumentException("has_child query isn't support as part of a role query");
178-
} else if (queryBuilder.getName().equals("has_parent")) {
179-
throw new IllegalArgumentException("has_parent query isn't support as part of a role query");
180-
} else if (queryBuilder instanceof BoolQueryBuilder) {
181-
BoolQueryBuilder boolQueryBuilder = (BoolQueryBuilder) queryBuilder;
182-
List<QueryBuilder> clauses = new ArrayList<>();
183-
clauses.addAll(boolQueryBuilder.filter());
184-
clauses.addAll(boolQueryBuilder.must());
185-
clauses.addAll(boolQueryBuilder.mustNot());
186-
clauses.addAll(boolQueryBuilder.should());
187-
for (QueryBuilder clause : clauses) {
188-
verifyRoleQuery(clause);
189-
}
190-
} else if (queryBuilder instanceof ConstantScoreQueryBuilder) {
191-
verifyRoleQuery(((ConstantScoreQueryBuilder) queryBuilder).innerQuery());
192-
} else if (queryBuilder instanceof FunctionScoreQueryBuilder) {
193-
verifyRoleQuery(((FunctionScoreQueryBuilder) queryBuilder).query());
194-
} else if (queryBuilder instanceof BoostingQueryBuilder) {
195-
verifyRoleQuery(((BoostingQueryBuilder) queryBuilder).negativeQuery());
196-
verifyRoleQuery(((BoostingQueryBuilder) queryBuilder).positiveQuery());
197-
}
198-
}
199-
200143
/**
201144
* Fall back validation that verifies that queries during rewrite don't use
202145
* the client to make remote calls. In the case of DLS this can cause a dead
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
package org.elasticsearch.xpack.core.security.authz.support;
7+
8+
import org.elasticsearch.ElasticsearchParseException;
9+
import org.elasticsearch.common.Nullable;
10+
import org.elasticsearch.common.ParsingException;
11+
import org.elasticsearch.common.Strings;
12+
import org.elasticsearch.common.bytes.BytesReference;
13+
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
14+
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
15+
import org.elasticsearch.common.xcontent.XContentFactory;
16+
import org.elasticsearch.common.xcontent.XContentParseException;
17+
import org.elasticsearch.common.xcontent.XContentParser;
18+
import org.elasticsearch.common.xcontent.XContentType;
19+
import org.elasticsearch.index.query.AbstractQueryBuilder;
20+
import org.elasticsearch.index.query.BoolQueryBuilder;
21+
import org.elasticsearch.index.query.BoostingQueryBuilder;
22+
import org.elasticsearch.index.query.ConstantScoreQueryBuilder;
23+
import org.elasticsearch.index.query.GeoShapeQueryBuilder;
24+
import org.elasticsearch.index.query.QueryBuilder;
25+
import org.elasticsearch.index.query.TermsQueryBuilder;
26+
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
27+
import org.elasticsearch.script.ScriptService;
28+
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
29+
import org.elasticsearch.xpack.core.security.user.User;
30+
31+
import java.io.IOException;
32+
import java.util.ArrayList;
33+
import java.util.List;
34+
35+
/**
36+
* This class helps in evaluating the query field if it is template,
37+
* validating the query and checking if the query type is allowed to be used in DLS role query.
38+
*/
39+
public final class DLSRoleQueryValidator {
40+
41+
private DLSRoleQueryValidator() {
42+
}
43+
44+
/**
45+
* Validates the query field in the {@link RoleDescriptor.IndicesPrivileges} only if it is not a template query.<br>
46+
* It parses the query and builds the {@link QueryBuilder}, also checks if the query type is supported in DLS role query.
47+
*
48+
* @param indicesPrivileges {@link RoleDescriptor.IndicesPrivileges}
49+
* @param xContentRegistry {@link NamedXContentRegistry} for finding named queries
50+
*/
51+
public static void validateQueryField(RoleDescriptor.IndicesPrivileges[] indicesPrivileges,
52+
NamedXContentRegistry xContentRegistry) {
53+
if (indicesPrivileges != null) {
54+
for (int i = 0; i < indicesPrivileges.length; i++) {
55+
BytesReference query = indicesPrivileges[i].getQuery();
56+
try {
57+
if (query != null) {
58+
if (isTemplateQuery(query, xContentRegistry)) {
59+
// skip template query, this requires runtime information like 'User' information.
60+
continue;
61+
}
62+
63+
evaluateAndVerifyRoleQuery(query.utf8ToString(), xContentRegistry);
64+
}
65+
} catch (ParsingException | IllegalArgumentException | IOException e) {
66+
throw new ElasticsearchParseException("failed to parse field 'query' for indices [" +
67+
Strings.arrayToCommaDelimitedString(indicesPrivileges[i].getIndices()) +
68+
"] at index privilege [" + i + "] of role descriptor", e);
69+
}
70+
}
71+
}
72+
}
73+
74+
private static boolean isTemplateQuery(BytesReference query, NamedXContentRegistry xContentRegistry) throws IOException {
75+
try (XContentParser parser = XContentType.JSON.xContent().createParser(xContentRegistry,
76+
LoggingDeprecationHandler.INSTANCE, query.utf8ToString())) {
77+
XContentParser.Token token = parser.nextToken();
78+
if (token != XContentParser.Token.START_OBJECT) {
79+
throw new XContentParseException(parser.getTokenLocation(), "expected [" + XContentParser.Token.START_OBJECT + "] but " +
80+
"found [" + token + "] instead");
81+
}
82+
token = parser.nextToken();
83+
if (token != XContentParser.Token.FIELD_NAME) {
84+
throw new XContentParseException(parser.getTokenLocation(), "expected [" + XContentParser.Token.FIELD_NAME + "] with " +
85+
"value a query name or 'template' but found [" + token + "] instead");
86+
}
87+
String fieldName = parser.currentName();
88+
if ("template".equals(fieldName)) {
89+
return true;
90+
}
91+
}
92+
93+
return false;
94+
}
95+
96+
/**
97+
* Evaluates the query if it is a template and then validates the query by parsing
98+
* and building the {@link QueryBuilder}. It also checks if the query type is
99+
* supported in DLS role query.
100+
*
101+
* @param query {@link BytesReference} query field from the role
102+
* @param scriptService {@link ScriptService} used for evaluation of a template query
103+
* @param xContentRegistry {@link NamedXContentRegistry} for finding named queries
104+
* @param user {@link User} used when evaluation a template query
105+
* @return {@link QueryBuilder} if the query is valid and allowed, in case {@link RoleDescriptor.IndicesPrivileges}
106+
* * does not have a query field then it returns {@code null}.
107+
*/
108+
@Nullable
109+
public static QueryBuilder evaluateAndVerifyRoleQuery(BytesReference query, ScriptService scriptService,
110+
NamedXContentRegistry xContentRegistry, User user) {
111+
if (query != null) {
112+
String templateResult = SecurityQueryTemplateEvaluator.evaluateTemplate(query.utf8ToString(), scriptService,
113+
user);
114+
try {
115+
return evaluateAndVerifyRoleQuery(templateResult, xContentRegistry);
116+
} catch (ElasticsearchParseException | ParsingException | XContentParseException | IOException e) {
117+
throw new ElasticsearchParseException("failed to parse field 'query' from the role descriptor", e);
118+
}
119+
}
120+
return null;
121+
}
122+
123+
@Nullable
124+
private static QueryBuilder evaluateAndVerifyRoleQuery(String query, NamedXContentRegistry xContentRegistry) throws IOException {
125+
if (query != null) {
126+
try (XContentParser parser = XContentFactory.xContent(query).createParser(xContentRegistry,
127+
LoggingDeprecationHandler.INSTANCE, query)) {
128+
QueryBuilder queryBuilder = AbstractQueryBuilder.parseInnerQueryBuilder(parser);
129+
verifyRoleQuery(queryBuilder);
130+
return queryBuilder;
131+
}
132+
}
133+
return null;
134+
}
135+
136+
/**
137+
* Checks whether the role query contains queries we know can't be used as DLS role query.
138+
*
139+
* @param queryBuilder {@link QueryBuilder} for given query
140+
*/
141+
// pkg protected for testing
142+
static void verifyRoleQuery(QueryBuilder queryBuilder) {
143+
if (queryBuilder instanceof TermsQueryBuilder) {
144+
TermsQueryBuilder termsQueryBuilder = (TermsQueryBuilder) queryBuilder;
145+
if (termsQueryBuilder.termsLookup() != null) {
146+
throw new IllegalArgumentException("terms query with terms lookup isn't supported as part of a role query");
147+
}
148+
} else if (queryBuilder instanceof GeoShapeQueryBuilder) {
149+
GeoShapeQueryBuilder geoShapeQueryBuilder = (GeoShapeQueryBuilder) queryBuilder;
150+
if (geoShapeQueryBuilder.shape() == null) {
151+
throw new IllegalArgumentException("geoshape query referring to indexed shapes isn't supported as part of a role query");
152+
}
153+
} else if (queryBuilder.getName().equals("percolate")) {
154+
// actually only if percolate query is referring to an existing document then this is problematic,
155+
// a normal percolate query does work. However we can't check that here as this query builder is inside
156+
// another module. So we don't allow the entire percolate query. I don't think users would ever use
157+
// a percolate query as role query, so this restriction shouldn't prohibit anyone from using dls.
158+
throw new IllegalArgumentException("percolate query isn't supported as part of a role query");
159+
} else if (queryBuilder.getName().equals("has_child")) {
160+
throw new IllegalArgumentException("has_child query isn't supported as part of a role query");
161+
} else if (queryBuilder.getName().equals("has_parent")) {
162+
throw new IllegalArgumentException("has_parent query isn't supported as part of a role query");
163+
} else if (queryBuilder instanceof BoolQueryBuilder) {
164+
BoolQueryBuilder boolQueryBuilder = (BoolQueryBuilder) queryBuilder;
165+
List<QueryBuilder> clauses = new ArrayList<>();
166+
clauses.addAll(boolQueryBuilder.filter());
167+
clauses.addAll(boolQueryBuilder.must());
168+
clauses.addAll(boolQueryBuilder.mustNot());
169+
clauses.addAll(boolQueryBuilder.should());
170+
for (QueryBuilder clause : clauses) {
171+
verifyRoleQuery(clause);
172+
}
173+
} else if (queryBuilder instanceof ConstantScoreQueryBuilder) {
174+
verifyRoleQuery(((ConstantScoreQueryBuilder) queryBuilder).innerQuery());
175+
} else if (queryBuilder instanceof FunctionScoreQueryBuilder) {
176+
verifyRoleQuery(((FunctionScoreQueryBuilder) queryBuilder).query());
177+
} else if (queryBuilder instanceof BoostingQueryBuilder) {
178+
verifyRoleQuery(((BoostingQueryBuilder) queryBuilder).negativeQuery());
179+
verifyRoleQuery(((BoostingQueryBuilder) queryBuilder).positiveQuery());
180+
}
181+
}
182+
}

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/support/SecurityQueryTemplateEvaluator.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,8 @@ private SecurityQueryTemplateEvaluator() {
4444
* @return resultant query string after compiling and executing the script.
4545
* If the source does not contain template then it will return the query
4646
* source without any modifications.
47-
* @throws IOException thrown when there is any error parsing the query
48-
* string.
4947
*/
50-
public static String evaluateTemplate(final String querySource, final ScriptService scriptService, final User user) throws IOException {
48+
public static String evaluateTemplate(final String querySource, final ScriptService scriptService, final User user) {
5149
// EMPTY is safe here because we never use namedObject
5250
try (XContentParser parser = XContentFactory.xContent(querySource).createParser(NamedXContentRegistry.EMPTY,
5351
LoggingDeprecationHandler.INSTANCE, querySource)) {
@@ -76,6 +74,8 @@ public static String evaluateTemplate(final String querySource, final ScriptServ
7674
} else {
7775
return querySource;
7876
}
77+
} catch (IOException ioe) {
78+
throw new ElasticsearchParseException("failed to parse query", ioe);
7979
}
8080
}
8181

0 commit comments

Comments
 (0)