Skip to content

Commit 31b2987

Browse files
martijnvgjpountz
andauthored
Add validation for dynamic templates (#51233)
Tries to load a `Mapper` instance for the mapping snippet of a dynamic template. This should catch things like using an analyzer that is undefined or mapping attributes that are unused. This is best effort: * If `{{name}}` placeholder is used in the mapping snippet then validation is skipped. * If `match_mapping_type` is not specified then validation is performed for all mapping types. If parsing succeeds with a single mapping type then this the dynamic mapping is considered valid. If is detected that a dynamic template mapping snippet is invalid at mapping update time then the mapping update is failed for indices created on 8.0.0-alpha1 and later. For indices created on prior version a deprecation warning is omitted instead. In 7.x clusters the mapping update will never fail in case of an invalid dynamic template mapping snippet and a deprecation warning will always be omitted. Closes #17411 Closes #24419 Co-authored-by: Adrien Grand <[email protected]>
1 parent 4ff5e03 commit 31b2987

File tree

6 files changed

+345
-4
lines changed

6 files changed

+345
-4
lines changed

docs/reference/mapping/dynamic/templates.asciidoc

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,19 @@ Dynamic templates are specified as an array of named objects:
3737
<2> The match conditions can include any of : `match_mapping_type`, `match`, `match_pattern`, `unmatch`, `path_match`, `path_unmatch`.
3838
<3> The mapping that the matched field should use.
3939

40+
If a provided mapping contains an invalid mapping snippet then that results in
41+
a validation error. Validation always occurs when applying the dynamic template
42+
at index time or in most cases when updating the dynamic template.
43+
44+
Whether updating the dynamic template fails when supplying an invalid mapping snippet depends on the following:
45+
* If no `match_mapping_type` has been specified then if the template is valid with one predefined mapping type then
46+
the mapping snippet is considered valid. However if at index time a field that matches with the template is indexed
47+
as a different type then an validation error will occur at index time instead. For example configuring a dynamic
48+
template with no `match_mapping_type` is considered valid as string type, but at index time a field that matches with
49+
the dynamic template is indexed as a long, then at index time a validation error may still occur.
50+
* If the `{{name}}` placeholder is used in the mapping snippet then the validation is skipped when updating
51+
the dynamic template. This is because the field name is unknown at that time. The validation will then occur
52+
when applying the template at index time.
4053

4154
Templates are processed in order -- the first matching template wins. When
4255
putting new dynamic templates through the <<indices-put-mapping, put mapping>> API,
@@ -409,4 +422,3 @@ PUT my_index
409422

410423
<1> Like the default dynamic mapping rules, doubles are mapped as floats, which
411424
are usually accurate enough, yet require half the disk space.
412-

docs/reference/release-notes/8.0.0-alpha1.asciidoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,8 @@ The changes listed below have been released for the first time in {es}
1111
Aggregations::
1212
* Disallow specifying the same percentile multiple times in percentiles aggregation {pull}52257[#52257]
1313

14+
Mapping::
15+
* Dynamic mappings in indices created on 8.0 and later have stricter validation at mapping update time.
16+
(e.g. incorrect analyzer settings or unknown field types). {pull}51233[#51233]
17+
1418
coming[8.0.0]

server/src/main/java/org/elasticsearch/index/mapper/DynamicTemplate.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,18 @@ private List processList(List list, String name, String dynamicType) {
334334
return processedList;
335335
}
336336

337+
String getName() {
338+
return name;
339+
}
340+
341+
XContentFieldType getXContentFieldType() {
342+
return xcontentFieldType;
343+
}
344+
345+
Map<String, Object> getMapping() {
346+
return mapping;
347+
}
348+
337349
@Override
338350
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
339351
builder.startObject();

server/src/main/java/org/elasticsearch/index/mapper/MapperService.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,4 +716,5 @@ public synchronized List<String> reloadSearchAnalyzers(AnalysisRegistry registry
716716
}
717717
return reloadedAnalyzers;
718718
}
719+
719720
}

server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java

Lines changed: 122 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,13 @@
1919

2020
package org.elasticsearch.index.mapper;
2121

22+
import org.apache.logging.log4j.LogManager;
23+
import org.apache.logging.log4j.Logger;
2224
import org.elasticsearch.Version;
2325
import org.elasticsearch.common.Explicit;
2426
import org.elasticsearch.common.Nullable;
27+
import org.elasticsearch.common.Strings;
28+
import org.elasticsearch.common.logging.DeprecationLogger;
2529
import org.elasticsearch.common.settings.Settings;
2630
import org.elasticsearch.common.time.DateFormatter;
2731
import org.elasticsearch.common.xcontent.ToXContent;
@@ -34,13 +38,17 @@
3438
import java.util.Collections;
3539
import java.util.Iterator;
3640
import java.util.List;
41+
import java.util.Locale;
3742
import java.util.Map;
3843

3944
import static org.elasticsearch.common.xcontent.support.XContentMapValues.nodeBooleanValue;
4045
import static org.elasticsearch.index.mapper.TypeParsers.parseDateTimeFormatter;
4146

4247
public class RootObjectMapper extends ObjectMapper {
4348

49+
private static final Logger LOGGER = LogManager.getLogger(RootObjectMapper.class);
50+
private static final DeprecationLogger DEPRECATION_LOGGER = new DeprecationLogger(LOGGER);
51+
4452
public static class Defaults {
4553
public static final DateFormatter[] DYNAMIC_DATE_TIME_FORMATTERS =
4654
new DateFormatter[]{
@@ -130,7 +138,7 @@ public Mapper.Builder parse(String name, Map<String, Object> node, ParserContext
130138
String fieldName = entry.getKey();
131139
Object fieldNode = entry.getValue();
132140
if (parseObjectOrDocumentTypeProperties(fieldName, fieldNode, parserContext, builder)
133-
|| processField(builder, fieldName, fieldNode, parserContext.indexVersionCreated())) {
141+
|| processField(builder, fieldName, fieldNode, parserContext)) {
134142
iterator.remove();
135143
}
136144
}
@@ -139,7 +147,7 @@ public Mapper.Builder parse(String name, Map<String, Object> node, ParserContext
139147

140148
@SuppressWarnings("unchecked")
141149
protected boolean processField(RootObjectMapper.Builder builder, String fieldName, Object fieldNode,
142-
Version indexVersionCreated) {
150+
ParserContext parserContext) {
143151
if (fieldName.equals("date_formats") || fieldName.equals("dynamic_date_formats")) {
144152
if (fieldNode instanceof List) {
145153
List<DateFormatter> formatters = new ArrayList<>();
@@ -163,7 +171,7 @@ protected boolean processField(RootObjectMapper.Builder builder, String fieldNam
163171
"template_1" : {
164172
"match" : "*_test",
165173
"match_mapping_type" : "string",
166-
"mapping" : { "type" : "string", "store" : "yes" }
174+
"mapping" : { "type" : "keyword", "store" : "yes" }
167175
}
168176
}
169177
]
@@ -183,6 +191,7 @@ protected boolean processField(RootObjectMapper.Builder builder, String fieldNam
183191
Map<String, Object> templateParams = (Map<String, Object>) entry.getValue();
184192
DynamicTemplate template = DynamicTemplate.parse(templateName, templateParams);
185193
if (template != null) {
194+
validateDynamicTemplate(parserContext, template);
186195
templates.add(template);
187196
}
188197
}
@@ -333,4 +342,114 @@ protected void doXContent(XContentBuilder builder, ToXContent.Params params) thr
333342
builder.field("numeric_detection", numericDetection.value());
334343
}
335344
}
345+
346+
private static void validateDynamicTemplate(Mapper.TypeParser.ParserContext parserContext,
347+
DynamicTemplate dynamicTemplate) {
348+
349+
if (containsSnippet(dynamicTemplate.getMapping(), "{name}")) {
350+
// Can't validate template, because field names can't be guessed up front.
351+
return;
352+
}
353+
354+
final XContentFieldType[] types;
355+
if (dynamicTemplate.getXContentFieldType() != null) {
356+
types = new XContentFieldType[]{dynamicTemplate.getXContentFieldType()};
357+
} else {
358+
types = XContentFieldType.values();
359+
}
360+
361+
Exception lastError = null;
362+
boolean dynamicTemplateInvalid = true;
363+
364+
for (XContentFieldType contentFieldType : types) {
365+
String defaultDynamicType = contentFieldType.defaultMappingType();
366+
String mappingType = dynamicTemplate.mappingType(defaultDynamicType);
367+
Mapper.TypeParser typeParser = parserContext.typeParser(mappingType);
368+
if (typeParser == null) {
369+
lastError = new IllegalArgumentException("No mapper found for type [" + mappingType + "]");
370+
continue;
371+
}
372+
373+
Map<String, Object> fieldTypeConfig = dynamicTemplate.mappingForName("__dummy__", defaultDynamicType);
374+
fieldTypeConfig.remove("type");
375+
try {
376+
Mapper.Builder<?, ?> dummyBuilder = typeParser.parse("__dummy__", fieldTypeConfig, parserContext);
377+
if (fieldTypeConfig.isEmpty()) {
378+
Settings indexSettings = parserContext.mapperService().getIndexSettings().getSettings();
379+
BuilderContext builderContext = new BuilderContext(indexSettings, new ContentPath(1));
380+
dummyBuilder.build(builderContext);
381+
dynamicTemplateInvalid = false;
382+
break;
383+
} else {
384+
lastError = new IllegalArgumentException("Unused mapping attributes [" + fieldTypeConfig + "]");
385+
}
386+
} catch (Exception e) {
387+
lastError = e;
388+
}
389+
}
390+
391+
final boolean failInvalidDynamicTemplates = parserContext.indexVersionCreated().onOrAfter(Version.V_8_0_0);
392+
if (dynamicTemplateInvalid) {
393+
String message = String.format(Locale.ROOT, "dynamic template [%s] has invalid content [%s]",
394+
dynamicTemplate.getName(), Strings.toString(dynamicTemplate));
395+
if (failInvalidDynamicTemplates) {
396+
throw new IllegalArgumentException(message, lastError);
397+
} else {
398+
final String deprecationMessage;
399+
if (lastError != null) {
400+
deprecationMessage = String.format(Locale.ROOT, "%s, caused by [%s]", message, lastError.getMessage());
401+
} else {
402+
deprecationMessage = message;
403+
}
404+
DEPRECATION_LOGGER.deprecatedAndMaybeLog("invalid_dynamic_template", deprecationMessage);
405+
}
406+
}
407+
}
408+
409+
private static boolean containsSnippet(Map<?, ?> map, String snippet) {
410+
for (Map.Entry<?, ?> entry : map.entrySet()) {
411+
String key = entry.getKey().toString();
412+
if (key.contains(snippet)) {
413+
return true;
414+
}
415+
416+
Object value = entry.getValue();
417+
if (value instanceof Map) {
418+
if (containsSnippet((Map<?, ?>) value, snippet)) {
419+
return true;
420+
}
421+
} else if (value instanceof List) {
422+
if (containsSnippet((List<?>) value, snippet)) {
423+
return true;
424+
}
425+
} else if (value instanceof String) {
426+
String valueString = (String) value;
427+
if (valueString.contains(snippet)) {
428+
return true;
429+
}
430+
}
431+
}
432+
433+
return false;
434+
}
435+
436+
private static boolean containsSnippet(List<?> list, String snippet) {
437+
for (Object value : list) {
438+
if (value instanceof Map) {
439+
if (containsSnippet((Map<?, ?>) value, snippet)) {
440+
return true;
441+
}
442+
} else if (value instanceof List) {
443+
if (containsSnippet((List<?>) value, snippet)) {
444+
return true;
445+
}
446+
} else if (value instanceof String) {
447+
String valueString = (String) value;
448+
if (valueString.contains(snippet)) {
449+
return true;
450+
}
451+
}
452+
}
453+
return false;
454+
}
336455
}

0 commit comments

Comments
 (0)