Skip to content

Commit ba60ed7

Browse files
authored
Create SPI to allow prohibiting certain top-level mappings (#132360)
The RootObjectNamespaceValidator now allows prohibiting "namespaces" or top level fields in Elasticsearch mappings: both "normal" index mappings and runtime mappings. The same validator is used to check both index-time runtime mappings and query-time index mappings. The primary target use case for this SPI is a feature in serverless to prohibit mappings like `_project` or `_project.my_sub_field`, but the mechanism provided in this PR allows any possible namespace detection and prevention.
1 parent e3c4c75 commit ba60ed7

File tree

13 files changed

+497
-18
lines changed

13 files changed

+497
-18
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.index.mapper;
11+
12+
/**
13+
* No-op Default of RootObjectMapperNamespaceValidator used in non-serverless Elasticsearch envs.
14+
*/
15+
public class DefaultRootObjectMapperNamespaceValidator implements RootObjectMapperNamespaceValidator {
16+
@Override
17+
public void validateNamespace(ObjectMapper.Subobjects subobjects, String name) {}
18+
}

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,23 @@ public final class MapperRegistry {
3131
private final Map<String, MetadataFieldMapper.TypeParser> metadataMapperParsers6x;
3232
private final Map<String, MetadataFieldMapper.TypeParser> metadataMapperParsers5x;
3333
private final Function<String, FieldPredicate> fieldFilter;
34+
private final RootObjectMapperNamespaceValidator namespaceValidator;
3435

3536
public MapperRegistry(
3637
Map<String, Mapper.TypeParser> mapperParsers,
3738
Map<String, RuntimeField.Parser> runtimeFieldParsers,
3839
Map<String, MetadataFieldMapper.TypeParser> metadataMapperParsers,
3940
Function<String, FieldPredicate> fieldFilter
41+
) {
42+
this(mapperParsers, runtimeFieldParsers, metadataMapperParsers, fieldFilter, null);
43+
}
44+
45+
public MapperRegistry(
46+
Map<String, Mapper.TypeParser> mapperParsers,
47+
Map<String, RuntimeField.Parser> runtimeFieldParsers,
48+
Map<String, MetadataFieldMapper.TypeParser> metadataMapperParsers,
49+
Function<String, FieldPredicate> fieldFilter,
50+
RootObjectMapperNamespaceValidator namespaceValidator
4051
) {
4152
this.mapperParsers = Collections.unmodifiableMap(new LinkedHashMap<>(mapperParsers));
4253
this.runtimeFieldParsers = runtimeFieldParsers;
@@ -50,6 +61,7 @@ public MapperRegistry(
5061
metadata5x.put(LegacyTypeFieldMapper.NAME, LegacyTypeFieldMapper.PARSER);
5162
this.metadataMapperParsers5x = metadata5x;
5263
this.fieldFilter = fieldFilter;
64+
this.namespaceValidator = namespaceValidator;
5365
}
5466

5567
/**
@@ -72,6 +84,10 @@ public Map<String, RuntimeField.Parser> getRuntimeFieldParsers() {
7284
return runtimeFieldParsers;
7385
}
7486

87+
public RootObjectMapperNamespaceValidator getNamespaceValidator() {
88+
return namespaceValidator;
89+
}
90+
7591
/**
7692
* Return a map of the meta mappers that have been registered. The
7793
* returned map uses the name of the field as a key.

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,8 @@ public MapperService(
245245
indexAnalyzers,
246246
indexSettings,
247247
idFieldMapper,
248-
bitSetProducer
248+
bitSetProducer,
249+
mapperRegistry.getNamespaceValidator()
249250
);
250251
this.documentParser = new DocumentParser(parserConfiguration, this.mappingParserContextSupplier.get());
251252
Map<String, MetadataFieldMapper.TypeParser> metadataMapperParsers = mapperRegistry.getMetadataMapperParsers(

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

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public class MappingParserContext {
4343
private final Function<Query, BitSetProducer> bitSetProducer;
4444
private final long mappingObjectDepthLimit;
4545
private long mappingObjectDepth = 0;
46+
private final RootObjectMapperNamespaceValidator namespaceValidator;
4647

4748
public MappingParserContext(
4849
Function<String, SimilarityProvider> similarityLookupService,
@@ -55,7 +56,8 @@ public MappingParserContext(
5556
IndexAnalyzers indexAnalyzers,
5657
IndexSettings indexSettings,
5758
IdFieldMapper idFieldMapper,
58-
Function<Query, BitSetProducer> bitSetProducer
59+
Function<Query, BitSetProducer> bitSetProducer,
60+
RootObjectMapperNamespaceValidator namespaceValidator
5961
) {
6062
this.similarityLookupService = similarityLookupService;
6163
this.typeParsers = typeParsers;
@@ -69,6 +71,40 @@ public MappingParserContext(
6971
this.idFieldMapper = idFieldMapper;
7072
this.mappingObjectDepthLimit = indexSettings.getMappingDepthLimit();
7173
this.bitSetProducer = bitSetProducer;
74+
this.namespaceValidator = namespaceValidator;
75+
}
76+
77+
public MappingParserContext(
78+
Function<String, SimilarityProvider> similarityLookupService,
79+
Function<String, Mapper.TypeParser> typeParsers,
80+
Function<String, RuntimeField.Parser> runtimeFieldParsers,
81+
IndexVersion indexVersionCreated,
82+
Supplier<TransportVersion> clusterTransportVersion,
83+
Supplier<SearchExecutionContext> searchExecutionContextSupplier,
84+
ScriptCompiler scriptCompiler,
85+
IndexAnalyzers indexAnalyzers,
86+
IndexSettings indexSettings,
87+
IdFieldMapper idFieldMapper,
88+
Function<Query, BitSetProducer> bitSetProducer
89+
) {
90+
this(
91+
similarityLookupService,
92+
typeParsers,
93+
runtimeFieldParsers,
94+
indexVersionCreated,
95+
clusterTransportVersion,
96+
searchExecutionContextSupplier,
97+
scriptCompiler,
98+
indexAnalyzers,
99+
indexSettings,
100+
idFieldMapper,
101+
bitSetProducer,
102+
null
103+
);
104+
}
105+
106+
public RootObjectMapperNamespaceValidator getNamespaceValidator() {
107+
return namespaceValidator;
72108
}
73109

74110
public IndexAnalyzers getIndexAnalyzers() {
@@ -170,7 +206,8 @@ private static class MultiFieldParserContext extends MappingParserContext {
170206
in.indexAnalyzers,
171207
in.indexSettings,
172208
in.idFieldMapper,
173-
in.bitSetProducer
209+
in.bitSetProducer,
210+
in.namespaceValidator
174211
);
175212
}
176213

@@ -200,7 +237,8 @@ private static class DynamicTemplateParserContext extends MappingParserContext {
200237
in.indexAnalyzers,
201238
in.indexSettings,
202239
in.idFieldMapper,
203-
in.bitSetProducer
240+
in.bitSetProducer,
241+
in.namespaceValidator
204242
);
205243
this.dateFormatter = dateFormatter;
206244
}

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -548,12 +548,21 @@ public final Optional<SourceKeepMode> sourceKeepMode() {
548548
}
549549

550550
@Override
551-
public void validate(MappingLookup mappers) {
551+
public final void validate(MappingLookup mappers) {
552552
for (Mapper mapper : this.mappers.values()) {
553-
mapper.validate(mappers);
553+
validateSubField(mapper, mappers);
554554
}
555555
}
556556

557+
/**
558+
* This method is separated out to allow subclasses (such as RootObjectMapper) to
559+
* override it and add in additional validations beyond what the mapper.validate()
560+
* method will check on each mapping.
561+
*/
562+
protected void validateSubField(Mapper mapper, MappingLookup mappers) {
563+
mapper.validate(mappers);
564+
}
565+
557566
protected MapperMergeContext createChildContext(MapperMergeContext mapperMergeContext, String name) {
558567
return mapperMergeContext.createChildContext(name, dynamic);
559568
}

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

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ public static class Builder extends ObjectMapper.Builder {
7676
protected final Map<String, RuntimeField> runtimeFields = new HashMap<>();
7777
protected Explicit<Boolean> dateDetection = Defaults.DATE_DETECTION;
7878
protected Explicit<Boolean> numericDetection = Defaults.NUMERIC_DETECTION;
79+
protected RootObjectMapperNamespaceValidator namespaceValidator;
7980

8081
public Builder(String name) {
8182
this(name, ObjectMapper.Defaults.SUBOBJECTS);
@@ -85,6 +86,11 @@ public Builder(String name, Explicit<Subobjects> subobjects) {
8586
super(name, subobjects);
8687
}
8788

89+
public Builder addNamespaceValidator(RootObjectMapperNamespaceValidator namespaceValidator) {
90+
this.namespaceValidator = namespaceValidator;
91+
return this;
92+
}
93+
8894
public Builder dynamicDateTimeFormatter(Collection<DateFormatter> dateTimeFormatters) {
8995
this.dynamicDateTimeFormatters = new Explicit<>(dateTimeFormatters.toArray(new DateFormatter[0]), true);
9096
return this;
@@ -124,7 +130,8 @@ public RootObjectMapper build(MapperBuilderContext context) {
124130
dynamicDateTimeFormatters,
125131
dynamicTemplates,
126132
dateDetection,
127-
numericDetection
133+
numericDetection,
134+
namespaceValidator
128135
);
129136
}
130137
}
@@ -134,6 +141,7 @@ public RootObjectMapper build(MapperBuilderContext context) {
134141
private final Explicit<Boolean> numericDetection;
135142
private final Explicit<DynamicTemplate[]> dynamicTemplates;
136143
private final Map<String, RuntimeField> runtimeFields;
144+
private final RootObjectMapperNamespaceValidator namespaceValidator;
137145

138146
RootObjectMapper(
139147
String name,
@@ -146,14 +154,16 @@ public RootObjectMapper build(MapperBuilderContext context) {
146154
Explicit<DateFormatter[]> dynamicDateTimeFormatters,
147155
Explicit<DynamicTemplate[]> dynamicTemplates,
148156
Explicit<Boolean> dateDetection,
149-
Explicit<Boolean> numericDetection
157+
Explicit<Boolean> numericDetection,
158+
RootObjectMapperNamespaceValidator namespaceValidator
150159
) {
151160
super(name, name, enabled, subobjects, sourceKeepMode, dynamic, mappers);
152161
this.runtimeFields = runtimeFields;
153162
this.dynamicTemplates = dynamicTemplates;
154163
this.dynamicDateTimeFormatters = dynamicDateTimeFormatters;
155164
this.dateDetection = dateDetection;
156165
this.numericDetection = numericDetection;
166+
this.namespaceValidator = namespaceValidator == null ? new DefaultRootObjectMapperNamespaceValidator() : namespaceValidator;
157167
if (sourceKeepMode.orElse(SourceKeepMode.NONE) == SourceKeepMode.ALL) {
158168
throw new MapperParsingException(
159169
"root object can't be configured with [" + Mapper.SYNTHETIC_SOURCE_KEEP_PARAM + ":" + SourceKeepMode.ALL + "]"
@@ -182,7 +192,8 @@ RootObjectMapper withoutMappers() {
182192
dynamicDateTimeFormatters,
183193
dynamicTemplates,
184194
dateDetection,
185-
numericDetection
195+
numericDetection,
196+
namespaceValidator
186197
);
187198
}
188199

@@ -298,7 +309,8 @@ public RootObjectMapper merge(Mapper mergeWith, MapperMergeContext parentMergeCo
298309
dynamicDateTimeFormatters,
299310
dynamicTemplates,
300311
dateDetection,
301-
numericDetection
312+
numericDetection,
313+
namespaceValidator
302314
);
303315
}
304316

@@ -455,6 +467,7 @@ public static RootObjectMapper.Builder parse(String name, Map<String, Object> no
455467
throws MapperParsingException {
456468
Explicit<Subobjects> subobjects = parseSubobjects(node);
457469
RootObjectMapper.Builder builder = new Builder(name, subobjects);
470+
builder.addNamespaceValidator(parserContext.getNamespaceValidator());
458471
Iterator<Map.Entry<String, Object>> iterator = node.entrySet().iterator();
459472
while (iterator.hasNext()) {
460473
Map.Entry<String, Object> entry = iterator.next();
@@ -544,4 +557,14 @@ private static boolean processField(
544557
public int getTotalFieldsCount() {
545558
return super.getTotalFieldsCount() - 1 + runtimeFields.size();
546559
}
560+
561+
/**
562+
* Overrides in order to run the namespace validator first and then delegates to the
563+
* standard validateSubField on the parent class
564+
*/
565+
@Override
566+
protected void validateSubField(Mapper mapper, MappingLookup mappers) {
567+
namespaceValidator.validateNamespace(subobjects(), mapper.leafName());
568+
super.validateSubField(mapper, mappers);
569+
}
547570
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.index.mapper;
11+
12+
import org.elasticsearch.core.Nullable;
13+
14+
/**
15+
* SPI to inject additional rules around namespaces (top level fields) that are prohibited
16+
* in Elasticsearch mappings.
17+
*/
18+
public interface RootObjectMapperNamespaceValidator {
19+
/**
20+
* If the namespace in the mapper is not allowed, an Exception should be thrown.
21+
* @param subobjects Whether subobjects are enabled. Null is allowed
22+
* @param name namespace (field name) to validate
23+
*/
24+
void validateNamespace(@Nullable ObjectMapper.Subobjects subobjects, String name);
25+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,9 @@ static Map<String, RuntimeField> parseRuntimeFields(
189189
+ " Check the documentation."
190190
);
191191
}
192+
if (parserContext.getNamespaceValidator() != null) {
193+
parserContext.getNamespaceValidator().validateNamespace(null, fieldName);
194+
}
192195
runtimeFields.put(fieldName, builder.apply(typeParser.parse(fieldName, propNode, parserContext)));
193196
propNode.remove("type");
194197
MappingParser.checkNoRemainingFields(fieldName, propNode);

server/src/main/java/org/elasticsearch/indices/IndicesModule.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
import org.elasticsearch.index.mapper.ObjectMapper;
5858
import org.elasticsearch.index.mapper.PassThroughObjectMapper;
5959
import org.elasticsearch.index.mapper.RangeType;
60+
import org.elasticsearch.index.mapper.RootObjectMapperNamespaceValidator;
6061
import org.elasticsearch.index.mapper.RoutingFieldMapper;
6162
import org.elasticsearch.index.mapper.RuntimeField;
6263
import org.elasticsearch.index.mapper.SeqNoFieldMapper;
@@ -94,15 +95,20 @@
9495
public class IndicesModule extends AbstractModule {
9596
private final MapperRegistry mapperRegistry;
9697

97-
public IndicesModule(List<MapperPlugin> mapperPlugins) {
98+
public IndicesModule(List<MapperPlugin> mapperPlugins, RootObjectMapperNamespaceValidator namespaceValidator) {
9899
this.mapperRegistry = new MapperRegistry(
99100
getMappers(mapperPlugins),
100101
getRuntimeFields(mapperPlugins),
101102
getMetadataMappers(mapperPlugins),
102-
getFieldFilter(mapperPlugins)
103+
getFieldFilter(mapperPlugins),
104+
namespaceValidator
103105
);
104106
}
105107

108+
public IndicesModule(List<MapperPlugin> mapperPlugins) {
109+
this(mapperPlugins, null);
110+
}
111+
106112
public static List<NamedWriteableRegistry.Entry> getNamedWriteables() {
107113
return Arrays.asList(
108114
new NamedWriteableRegistry.Entry(Condition.class, MinAgeCondition.NAME, MinAgeCondition::new),

server/src/main/java/org/elasticsearch/node/NodeConstruction.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,9 @@
118118
import org.elasticsearch.index.SlowLogFields;
119119
import org.elasticsearch.index.analysis.AnalysisRegistry;
120120
import org.elasticsearch.index.engine.MergeMetrics;
121+
import org.elasticsearch.index.mapper.DefaultRootObjectMapperNamespaceValidator;
121122
import org.elasticsearch.index.mapper.MapperMetrics;
123+
import org.elasticsearch.index.mapper.RootObjectMapperNamespaceValidator;
122124
import org.elasticsearch.index.mapper.SourceFieldMetrics;
123125
import org.elasticsearch.index.search.stats.ShardSearchPhaseAPMMetrics;
124126
import org.elasticsearch.index.shard.SearchOperationListener;
@@ -698,6 +700,12 @@ private void construct(
698700

699701
modules.bindToInstance(Tracer.class, telemetryProvider.getTracer());
700702

703+
RootObjectMapperNamespaceValidator namespaceValidator = pluginsService.loadSingletonServiceProvider(
704+
RootObjectMapperNamespaceValidator.class,
705+
() -> new DefaultRootObjectMapperNamespaceValidator()
706+
);
707+
modules.bindToInstance(RootObjectMapperNamespaceValidator.class, namespaceValidator);
708+
701709
assert nodeEnvironment.nodeId() != null : "node ID must be set before constructing the Node";
702710
TaskManager taskManager = new TaskManager(
703711
settings,
@@ -814,7 +822,7 @@ private void construct(
814822
)::onNewInfo
815823
);
816824

817-
IndicesModule indicesModule = new IndicesModule(pluginsService.filterPlugins(MapperPlugin.class).toList());
825+
IndicesModule indicesModule = new IndicesModule(pluginsService.filterPlugins(MapperPlugin.class).toList(), namespaceValidator);
818826
modules.add(indicesModule);
819827

820828
modules.add(new GatewayModule());

0 commit comments

Comments
 (0)