Skip to content

Commit fcdc509

Browse files
authored
Make sure to reject mappings with type _doc when include_type_name is false. (#38484)
`CreateIndexRequest#source(Map<String, Object>, ... )`, which is used when deserializing index creation requests, accidentally accepts mappings that are nested twice under the type key (as described in the bug report #38266). This in turn causes us to be too lenient in parsing typeless mappings. In particular, we accept the following index creation request, even though it should not contain the type key `_doc`: ``` PUT index?include_type_name=false { "mappings": { "_doc": { "properties": { ... } } } } ``` There is a similar issue for both 'put templates' and 'put mappings' requests as well. This PR makes the minimal changes to detect and reject these typed mappings in requests. It does not address #38266 generally, or attempt a larger refactor around types in these server-side requests, as I think this should be done at a later time.
1 parent 652e7c5 commit fcdc509

File tree

14 files changed

+271
-143
lines changed

14 files changed

+271
-143
lines changed

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

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -313,15 +313,13 @@ public void testCreateIndex() throws IOException {
313313
{
314314
request = new CreateIndexRequest("twitter2");
315315
//tag::create-index-mappings-map
316-
Map<String, Object> jsonMap = new HashMap<>();
317316
Map<String, Object> message = new HashMap<>();
318317
message.put("type", "text");
319318
Map<String, Object> properties = new HashMap<>();
320319
properties.put("message", message);
321320
Map<String, Object> mapping = new HashMap<>();
322321
mapping.put("properties", properties);
323-
jsonMap.put("_doc", mapping);
324-
request.mapping(jsonMap); // <1>
322+
request.mapping(mapping); // <1>
325323
//end::create-index-mappings-map
326324
CreateIndexResponse createIndexResponse = client.indices().create(request, RequestOptions.DEFAULT);
327325
assertTrue(createIndexResponse.isAcknowledged());
@@ -332,15 +330,11 @@ public void testCreateIndex() throws IOException {
332330
XContentBuilder builder = XContentFactory.jsonBuilder();
333331
builder.startObject();
334332
{
335-
builder.startObject("_doc");
333+
builder.startObject("properties");
336334
{
337-
builder.startObject("properties");
335+
builder.startObject("message");
338336
{
339-
builder.startObject("message");
340-
{
341-
builder.field("type", "text");
342-
}
343-
builder.endObject();
337+
builder.field("type", "text");
344338
}
345339
builder.endObject();
346340
}
@@ -381,10 +375,8 @@ public void testCreateIndex() throws IOException {
381375
" \"number_of_replicas\" : 0\n" +
382376
" },\n" +
383377
" \"mappings\" : {\n" +
384-
" \"_doc\" : {\n" +
385-
" \"properties\" : {\n" +
386-
" \"message\" : { \"type\" : \"text\" }\n" +
387-
" }\n" +
378+
" \"properties\" : {\n" +
379+
" \"message\" : { \"type\" : \"text\" }\n" +
388380
" }\n" +
389381
" },\n" +
390382
" \"aliases\" : {\n" +

rest-api-spec/src/main/resources/rest-api-spec/test/indices.create/10_basic.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,23 @@
113113
properties:
114114
"":
115115
type: keyword
116+
117+
---
118+
"Create index with explicit _doc type":
119+
- skip:
120+
version: " - 6.6.99"
121+
reason: include_type_name was introduced in 6.7.0
122+
- do:
123+
catch: bad_request
124+
indices.create:
125+
include_type_name: false
126+
index: test_index
127+
body:
128+
mappings:
129+
_doc:
130+
properties:
131+
field:
132+
type: keyword
133+
134+
- match: { error.type: "illegal_argument_exception" }
135+
- match: { error.reason: "The mapping definition cannot be nested under a type [_doc] unless include_type_name is set to true." }

rest-api-spec/src/main/resources/rest-api-spec/test/indices.put_mapping/10_basic.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,28 @@
7474
properties:
7575
"":
7676
type: keyword
77+
78+
---
79+
"Put mappings with explicit _doc type":
80+
- skip:
81+
version: " - 6.6.99"
82+
reason: include_type_name was introduced in 6.7.0
83+
84+
- do:
85+
indices.create:
86+
include_type_name: false
87+
index: test_index
88+
89+
- do:
90+
catch: bad_request
91+
indices.put_mapping:
92+
include_type_name: false
93+
index: test_index
94+
body:
95+
_doc:
96+
properties:
97+
field:
98+
type: keyword
99+
100+
- match: { error.type: "illegal_argument_exception" }
101+
- match: { error.reason: "Types cannot be provided in put mapping requests, unless the include_type_name parameter is set to true." }

rest-api-spec/src/main/resources/rest-api-spec/test/indices.put_template/10_basic.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,3 +260,25 @@
260260
indices.put_template:
261261
name: test
262262
body: {}
263+
264+
---
265+
"Put template with explicit _doc type":
266+
- skip:
267+
version: " - 6.6.99"
268+
reason: include_type_name was introduced in 6.7.0
269+
270+
- do:
271+
catch: bad_request
272+
indices.put_template:
273+
include_type_name: false
274+
name: test
275+
body:
276+
index_patterns: test-*
277+
mappings:
278+
_doc:
279+
properties:
280+
field:
281+
type: keyword
282+
283+
- match: { error.type: "illegal_argument_exception" }
284+
- match: { error.reason: "The mapping definition cannot be nested under a type [_doc] unless include_type_name is set to true." }

rest-api-spec/src/main/resources/rest-api-spec/test/indices.rollover/40_mapping.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,33 @@
4545

4646
- match: { conditions: { "[max_docs: 2]": true } }
4747
- match: { rolled_over: true }
48+
49+
---
50+
"Mappings with explicit _doc type":
51+
- skip:
52+
version: " - 6.6.99"
53+
reason: include_type_name was introduced in 6.7.0
54+
55+
- do:
56+
indices.create:
57+
index: logs-1
58+
body:
59+
aliases:
60+
logs_search: {}
61+
62+
- do:
63+
catch: bad_request
64+
indices.rollover:
65+
include_type_name: false
66+
alias: "logs_search"
67+
body:
68+
conditions:
69+
max_docs: 2
70+
mappings:
71+
_doc:
72+
properties:
73+
field:
74+
type: keyword
75+
76+
- match: { error.caused_by.type: "illegal_argument_exception" }
77+
- match: { error.caused_by.reason: "The mapping definition cannot be nested under a type [_doc] unless include_type_name is set to true." }

server/src/main/java/org/elasticsearch/action/admin/indices/rollover/RolloverRequest.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,17 @@ public class RolloverRequest extends AcknowledgedRequest<RolloverRequest> implem
7070
CONDITIONS, ObjectParser.ValueType.OBJECT);
7171
PARSER.declareField((parser, request, context) -> request.createIndexRequest.settings(parser.map()),
7272
CreateIndexRequest.SETTINGS, ObjectParser.ValueType.OBJECT);
73-
PARSER.declareField((parser, request, isTypeIncluded) -> {
74-
if (isTypeIncluded) {
73+
PARSER.declareField((parser, request, includeTypeName) -> {
74+
if (includeTypeName) {
7575
for (Map.Entry<String, Object> mappingsEntry : parser.map().entrySet()) {
7676
request.createIndexRequest.mapping(mappingsEntry.getKey(), (Map<String, Object>) mappingsEntry.getValue());
7777
}
7878
} else {
79-
// a type is not included, add a dummy _doc type
80-
request.createIndexRequest.mapping(MapperService.SINGLE_MAPPING_NAME, parser.map());
79+
Map<String, Object> mappings = parser.map();
80+
if (MapperService.isMappingSourceTyped(MapperService.SINGLE_MAPPING_NAME, mappings)) {
81+
throw new IllegalArgumentException("The mapping definition cannot be nested under a type " +
82+
"[" + MapperService.SINGLE_MAPPING_NAME + "] unless include_type_name is set to true.");
83+
}
8184
}
8285
}, CreateIndexRequest.MAPPINGS, ObjectParser.ValueType.OBJECT);
8386
PARSER.declareField((parser, request, context) -> request.createIndexRequest.aliases(parser.map()),

server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataMappingService.java

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,6 @@
3838
import org.elasticsearch.common.compress.CompressedXContent;
3939
import org.elasticsearch.common.inject.Inject;
4040
import org.elasticsearch.common.unit.TimeValue;
41-
import org.elasticsearch.common.xcontent.XContentHelper;
42-
import org.elasticsearch.common.xcontent.XContentType;
4341
import org.elasticsearch.core.internal.io.IOUtils;
4442
import org.elasticsearch.index.Index;
4543
import org.elasticsearch.index.IndexService;
@@ -57,6 +55,7 @@
5755
import java.util.List;
5856
import java.util.Map;
5957

58+
import static org.elasticsearch.index.mapper.MapperService.isMappingSourceTyped;
6059
import static org.elasticsearch.indices.cluster.IndicesClusterStateService.AllocatedIndices.IndexRemovalReason.NO_LONGER_ASSIGNED;
6160

6261
/**
@@ -276,7 +275,7 @@ private ClusterState applyRequest(ClusterState currentState, PutMappingClusterSt
276275
// try and parse it (no need to add it here) so we can bail early in case of parsing exception
277276
DocumentMapper newMapper;
278277
DocumentMapper existingMapper = mapperService.documentMapper(mappingType);
279-
if (existingMapper == null && isMappingSourceTyped(mapperService, mappingUpdateSource, request.type()) == false) {
278+
if (existingMapper == null && isMappingSourceTyped(request.type(), mappingUpdateSource) == false) {
280279
existingMapper = getMapperForUpdate(mapperService, mappingType);
281280
}
282281
String typeForUpdate = existingMapper == null ? mappingType : existingMapper.type();
@@ -337,7 +336,7 @@ private ClusterState applyRequest(ClusterState currentState, PutMappingClusterSt
337336
String typeForUpdate = mappingType;
338337
CompressedXContent existingSource = null;
339338
DocumentMapper existingMapper = mapperService.documentMapper(mappingType);
340-
if (existingMapper == null && isMappingSourceTyped(mapperService, mappingUpdateSource, request.type()) == false) {
339+
if (existingMapper == null && isMappingSourceTyped(request.type(), mappingUpdateSource) == false) {
341340
existingMapper = getMapperForUpdate(mapperService, mappingType);
342341
}
343342
if (existingMapper != null) {
@@ -400,15 +399,6 @@ public String describeTasks(List<PutMappingClusterStateUpdateRequest> tasks) {
400399
}
401400
}
402401

403-
/**
404-
* Returns {@code true} if the given {@code mappingSource} includes a type
405-
* as a top-level object.
406-
*/
407-
private static boolean isMappingSourceTyped(MapperService mapperService, CompressedXContent mappingSource, String type) {
408-
Map<String, Object> root = XContentHelper.convertToMap(mappingSource.compressedReference(), true, XContentType.JSON).v2();
409-
return root.size() == 1 && root.keySet().iterator().next().equals(type);
410-
}
411-
412402
public void putMapping(final PutMappingClusterStateUpdateRequest request, final ActionListener<ClusterStateUpdateResponse> listener) {
413403
clusterService.submitStateUpdateTask("put-mapping",
414404
request,

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
4141
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
4242
import org.elasticsearch.common.xcontent.XContentFactory;
43+
import org.elasticsearch.common.xcontent.XContentHelper;
4344
import org.elasticsearch.common.xcontent.XContentParser;
4445
import org.elasticsearch.common.xcontent.XContentType;
4546
import org.elasticsearch.index.AbstractIndexComponent;
@@ -725,6 +726,19 @@ public DocumentMapperForType documentMapperWithAutoCreate(String type) {
725726
return new DocumentMapperForType(mapper, mapper.mapping());
726727
}
727728

729+
/**
730+
* Returns {@code true} if the given {@code mappingSource} includes a type
731+
* as a top-level object.
732+
*/
733+
public static boolean isMappingSourceTyped(String type, Map<String, Object> mapping) {
734+
return mapping.size() == 1 && mapping.keySet().iterator().next().equals(type);
735+
}
736+
737+
public static boolean isMappingSourceTyped(String type, CompressedXContent mappingSource) {
738+
Map<String, Object> root = XContentHelper.convertToMap(mappingSource.compressedReference(), true, XContentType.JSON).v2();
739+
return isMappingSourceTyped(type, root);
740+
}
741+
728742
/**
729743
* Returns the {@link MappedFieldType} for the give fullName.
730744
*

server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestCreateIndexAction.java

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,22 +57,16 @@ public String getName() {
5757

5858
@Override
5959
public RestChannelConsumer prepareRequest(final RestRequest request, final NodeClient client) throws IOException {
60-
final boolean includeTypeName = request.paramAsBoolean(INCLUDE_TYPE_NAME_PARAMETER,
61-
DEFAULT_INCLUDE_TYPE_NAME_POLICY);
62-
6360
CreateIndexRequest createIndexRequest = new CreateIndexRequest(request.param("index"));
61+
62+
boolean includeTypeName = request.paramAsBoolean(INCLUDE_TYPE_NAME_PARAMETER,
63+
DEFAULT_INCLUDE_TYPE_NAME_POLICY);
6464
if (request.hasContent()) {
6565
Map<String, Object> sourceAsMap = XContentHelper.convertToMap(request.content(), false, request.getXContentType()).v2();
66-
if (sourceAsMap.containsKey("mappings")) {
67-
if (includeTypeName == false) {
68-
Map<String, Object> newSourceAsMap = new HashMap<>(sourceAsMap);
69-
newSourceAsMap.put("mappings", Collections.singletonMap(
70-
MapperService.SINGLE_MAPPING_NAME, sourceAsMap.get("mappings")));
71-
sourceAsMap = newSourceAsMap;
72-
} else {
73-
deprecationLogger.deprecatedAndMaybeLog("create_index_with_types", TYPES_DEPRECATION_MESSAGE);
74-
}
66+
if (includeTypeName && sourceAsMap.containsKey("mappings")) {
67+
deprecationLogger.deprecatedAndMaybeLog("create_index_with_types", TYPES_DEPRECATION_MESSAGE);
7568
}
69+
sourceAsMap = prepareMappings(sourceAsMap, includeTypeName);
7670
createIndexRequest.source(sourceAsMap, LoggingDeprecationHandler.INSTANCE);
7771
}
7872

@@ -85,4 +79,25 @@ public RestChannelConsumer prepareRequest(final RestRequest request, final NodeC
8579
createIndexRequest.waitForActiveShards(ActiveShardCount.parseString(request.param("wait_for_active_shards")));
8680
return channel -> client.admin().indices().create(createIndexRequest, new RestToXContentListener<>(channel));
8781
}
82+
83+
static Map<String, Object> prepareMappings(Map<String, Object> source, boolean includeTypeName) {
84+
if (includeTypeName
85+
|| source.containsKey("mappings") == false
86+
|| (source.get("mappings") instanceof Map) == false) {
87+
return source;
88+
}
89+
90+
Map<String, Object> newSource = new HashMap<>(source);
91+
92+
@SuppressWarnings("unchecked")
93+
Map<String, Object> mappings = (Map<String, Object>) source.get("mappings");
94+
95+
if (MapperService.isMappingSourceTyped(MapperService.SINGLE_MAPPING_NAME, mappings)) {
96+
throw new IllegalArgumentException("The mapping definition cannot be nested under a type " +
97+
"[" + MapperService.SINGLE_MAPPING_NAME + "] unless include_type_name is set to true.");
98+
}
99+
100+
newSource.put("mappings", Collections.singletonMap(MapperService.SINGLE_MAPPING_NAME, mappings));
101+
return newSource;
102+
}
88103
}

server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestPutIndexTemplateAction.java

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
import org.elasticsearch.common.logging.DeprecationLogger;
2727
import org.elasticsearch.common.settings.Settings;
2828
import org.elasticsearch.common.xcontent.XContentHelper;
29-
import org.elasticsearch.index.mapper.MapperService;
3029
import org.elasticsearch.rest.BaseRestHandler;
3130
import org.elasticsearch.rest.RestController;
3231
import org.elasticsearch.rest.RestRequest;
@@ -35,7 +34,6 @@
3534
import java.io.IOException;
3635
import java.util.Arrays;
3736
import java.util.Collections;
38-
import java.util.HashMap;
3937
import java.util.Map;
4038

4139
public class RestPutIndexTemplateAction extends BaseRestHandler {
@@ -72,24 +70,14 @@ public RestChannelConsumer prepareRequest(final RestRequest request, final NodeC
7270
putRequest.cause(request.param("cause", ""));
7371

7472
boolean includeTypeName = request.paramAsBoolean(INCLUDE_TYPE_NAME_PARAMETER, DEFAULT_INCLUDE_TYPE_NAME_POLICY);
75-
Map<String, Object> sourceAsMap = prepareRequestSource(request, includeTypeName);
76-
putRequest.source(sourceAsMap);
77-
78-
return channel -> client.admin().indices().putTemplate(putRequest, new RestToXContentListener<>(channel));
79-
}
80-
81-
Map<String, Object> prepareRequestSource(RestRequest request, boolean includeTypeName) {
8273
Map<String, Object> sourceAsMap = XContentHelper.convertToMap(request.requiredContent(), false,
8374
request.getXContentType()).v2();
84-
if (includeTypeName == false && sourceAsMap.containsKey("mappings")) {
85-
Map<String, Object> newSourceAsMap = new HashMap<>(sourceAsMap);
86-
newSourceAsMap.put("mappings", Collections.singletonMap(MapperService.SINGLE_MAPPING_NAME, sourceAsMap.get("mappings")));
87-
return newSourceAsMap;
88-
} else {
89-
if(includeTypeName && sourceAsMap.containsKey("mappings") ) {
90-
DEPRECATION_LOGGER.deprecatedAndMaybeLog("put_index_template_with_types", TYPES_DEPRECATION_MESSAGE);
91-
}
92-
return sourceAsMap;
75+
if (includeTypeName && sourceAsMap.containsKey("mappings")) {
76+
DEPRECATION_LOGGER.deprecatedAndMaybeLog("put_index_template_with_types", TYPES_DEPRECATION_MESSAGE);
9377
}
78+
sourceAsMap = RestCreateIndexAction.prepareMappings(sourceAsMap, includeTypeName);
79+
putRequest.source(sourceAsMap);
80+
81+
return channel -> client.admin().indices().putTemplate(putRequest, new RestToXContentListener<>(channel));
9482
}
9583
}

0 commit comments

Comments
 (0)