Skip to content

Commit 5e0a808

Browse files
martijnvgjpountz
andcommitted
Add validation for dynamic templates (elastic#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 elastic#17411 Closes elastic#24419 Co-authored-by: Adrien Grand <[email protected]>
1 parent 52fa465 commit 5e0a808

File tree

6 files changed

+355
-5
lines changed

6 files changed

+355
-5
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-
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[[release-notes-7.7.0]]
2+
== {es} version 7.7.0
3+
4+
coming[7.7.0]
5+
6+
[[breaking-7.7.0]]
7+
[float]
8+
=== Breaking changes
9+
10+
Mapping::
11+
* Dynamic mappings in indices created on 8.0 and later have stricter validation at mapping update time and
12+
results in a deprecation warning for indices created in Elasticsearch 7.7.0 and later.
13+
(e.g. incorrect analyzer settings or unknown field types). {pull}51233[#51233]

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

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

357+
String getName() {
358+
return name;
359+
}
360+
361+
XContentFieldType getXContentFieldType() {
362+
return xcontentFieldType;
363+
}
364+
365+
Map<String, Object> getMapping() {
366+
return mapping;
367+
}
368+
357369
@Override
358370
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
359371
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
@@ -886,4 +886,5 @@ public synchronized List<String> reloadSearchAnalyzers(AnalysisRegistry registry
886886
}
887887
return reloadedAnalyzers;
888888
}
889+
889890
}

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

Lines changed: 120 additions & 4 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[]{
@@ -128,15 +136,15 @@ public Mapper.Builder parse(String name, Map<String, Object> node, ParserContext
128136
String fieldName = entry.getKey();
129137
Object fieldNode = entry.getValue();
130138
if (parseObjectOrDocumentTypeProperties(fieldName, fieldNode, parserContext, builder)
131-
|| processField(builder, fieldName, fieldNode, parserContext.indexVersionCreated())) {
139+
|| processField(builder, fieldName, fieldNode, parserContext)) {
132140
iterator.remove();
133141
}
134142
}
135143
return builder;
136144
}
137145

138146
protected boolean processField(RootObjectMapper.Builder builder, String fieldName, Object fieldNode,
139-
Version indexVersionCreated) {
147+
ParserContext parserContext) {
140148
if (fieldName.equals("date_formats") || fieldName.equals("dynamic_date_formats")) {
141149
if (fieldNode instanceof List) {
142150
List<DateFormatter> formatters = new ArrayList<>();
@@ -159,7 +167,7 @@ protected boolean processField(RootObjectMapper.Builder builder, String fieldNam
159167
// "template_1" : {
160168
// "match" : "*_test",
161169
// "match_mapping_type" : "string",
162-
// "mapping" : { "type" : "string", "store" : "yes" }
170+
// "mapping" : { "type" : "keyword", "store" : "yes" }
163171
// }
164172
// }
165173
// ]
@@ -176,8 +184,9 @@ protected boolean processField(RootObjectMapper.Builder builder, String fieldNam
176184
Map.Entry<String, Object> entry = tmpl.entrySet().iterator().next();
177185
String templateName = entry.getKey();
178186
Map<String, Object> templateParams = (Map<String, Object>) entry.getValue();
179-
DynamicTemplate template = DynamicTemplate.parse(templateName, templateParams, indexVersionCreated);
187+
DynamicTemplate template = DynamicTemplate.parse(templateName, templateParams, parserContext.indexVersionCreated());
180188
if (template != null) {
189+
validateDynamicTemplate(parserContext, template);
181190
templates.add(template);
182191
}
183192
}
@@ -326,4 +335,111 @@ protected void doXContent(XContentBuilder builder, ToXContent.Params params) thr
326335
builder.field("numeric_detection", numericDetection.value());
327336
}
328337
}
338+
339+
private static void validateDynamicTemplate(Mapper.TypeParser.ParserContext parserContext,
340+
DynamicTemplate dynamicTemplate) {
341+
342+
if (containsSnippet(dynamicTemplate.getMapping(), "{name}")) {
343+
// Can't validate template, because field names can't be guessed up front.
344+
return;
345+
}
346+
347+
final XContentFieldType[] types;
348+
if (dynamicTemplate.getXContentFieldType() != null) {
349+
types = new XContentFieldType[]{dynamicTemplate.getXContentFieldType()};
350+
} else {
351+
types = XContentFieldType.values();
352+
}
353+
354+
Exception lastError = null;
355+
boolean dynamicTemplateInvalid = true;
356+
357+
for (XContentFieldType contentFieldType : types) {
358+
String defaultDynamicType = contentFieldType.defaultMappingType();
359+
String mappingType = dynamicTemplate.mappingType(defaultDynamicType);
360+
Mapper.TypeParser typeParser = parserContext.typeParser(mappingType);
361+
if (typeParser == null) {
362+
lastError = new IllegalArgumentException("No mapper found for type [" + mappingType + "]");
363+
continue;
364+
}
365+
366+
Map<String, Object> fieldTypeConfig = dynamicTemplate.mappingForName("__dummy__", defaultDynamicType);
367+
fieldTypeConfig.remove("type");
368+
try {
369+
Mapper.Builder<?, ?> dummyBuilder = typeParser.parse("__dummy__", fieldTypeConfig, parserContext);
370+
if (fieldTypeConfig.isEmpty()) {
371+
Settings indexSettings = parserContext.mapperService().getIndexSettings().getSettings();
372+
BuilderContext builderContext = new BuilderContext(indexSettings, new ContentPath(1));
373+
dummyBuilder.build(builderContext);
374+
dynamicTemplateInvalid = false;
375+
break;
376+
} else {
377+
lastError = new IllegalArgumentException("Unused mapping attributes [" + fieldTypeConfig + "]");
378+
}
379+
} catch (Exception e) {
380+
lastError = e;
381+
}
382+
}
383+
384+
final boolean shouldEmitDeprecationWarning = parserContext.indexVersionCreated().onOrAfter(Version.V_7_7_0);
385+
if (dynamicTemplateInvalid && shouldEmitDeprecationWarning) {
386+
String message = String.format(Locale.ROOT, "dynamic template [%s] has invalid content [%s]",
387+
dynamicTemplate.getName(), Strings.toString(dynamicTemplate));
388+
389+
final String deprecationMessage;
390+
if (lastError != null) {
391+
deprecationMessage = String.format(Locale.ROOT, "%s, caused by [%s]", message, lastError.getMessage());
392+
} else {
393+
deprecationMessage = message;
394+
}
395+
DEPRECATION_LOGGER.deprecatedAndMaybeLog("invalid_dynamic_template", deprecationMessage);
396+
}
397+
}
398+
399+
private static boolean containsSnippet(Map<?, ?> map, String snippet) {
400+
for (Map.Entry<?, ?> entry : map.entrySet()) {
401+
String key = entry.getKey().toString();
402+
if (key.contains(snippet)) {
403+
return true;
404+
}
405+
406+
Object value = entry.getValue();
407+
if (value instanceof Map) {
408+
if (containsSnippet((Map<?, ?>) value, snippet)) {
409+
return true;
410+
}
411+
} else if (value instanceof List) {
412+
if (containsSnippet((List<?>) value, snippet)) {
413+
return true;
414+
}
415+
} else if (value instanceof String) {
416+
String valueString = (String) value;
417+
if (valueString.contains(snippet)) {
418+
return true;
419+
}
420+
}
421+
}
422+
423+
return false;
424+
}
425+
426+
private static boolean containsSnippet(List<?> list, String snippet) {
427+
for (Object value : list) {
428+
if (value instanceof Map) {
429+
if (containsSnippet((Map<?, ?>) value, snippet)) {
430+
return true;
431+
}
432+
} else if (value instanceof List) {
433+
if (containsSnippet((List<?>) value, snippet)) {
434+
return true;
435+
}
436+
} else if (value instanceof String) {
437+
String valueString = (String) value;
438+
if (valueString.contains(snippet)) {
439+
return true;
440+
}
441+
}
442+
}
443+
return false;
444+
}
329445
}

0 commit comments

Comments
 (0)