Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6c7575e
Init commit: same as cps/prohibit-_project-mappings branch
quux00 Aug 1, 2025
5c0c7ea
Most manual tests now pass. The two that still don't work right are:
quux00 Aug 19, 2025
f931cef
Hardcoded hack to get runtime fields with _project mappings to be det…
quux00 Aug 19, 2025
0350cac
Refactored validateNamespace signature so that RootObjectMapper.proce…
quux00 Aug 19, 2025
71f2156
Added namespaceValidator check to IndexService.parseRuntimeMappings
quux00 Aug 20, 2025
53ac90f
Changed runtime field validation to be done in one place: RuntimeFiel…
quux00 Aug 21, 2025
4eb29e2
Removed redundant call to mapper.validate(mappers) in ObjectMapper.va…
quux00 Aug 21, 2025
020f7ca
Initial set of tests for index time mappings - both standard and runt…
quux00 Aug 25, 2025
0cd6b97
Added query-time runtime field tests with RootObjectMapperNamespaceVa…
quux00 Aug 26, 2025
63c4549
Initial cleanup for PR review
quux00 Aug 26, 2025
af4964f
Cleanup for PR review - ready for next round review
quux00 Aug 26, 2025
ebaf9a2
Got subobjects:false tests working in RootObjectMapperTests
quux00 Aug 28, 2025
0aa546b
Merge remote-tracking branch 'elastic/main' into cross-project/prohib…
quux00 Aug 28, 2025
a8fcc03
Removed ServerlessRootObjectMapperNamespaceValidator
quux00 Aug 28, 2025
8665113
Fixed checkstyle issue
quux00 Aug 28, 2025
8843a78
Merge remote-tracking branch 'elastic/main' into cross-project/prohib…
quux00 Aug 28, 2025
ab5d0c2
Improved code comments
quux00 Sep 2, 2025
1524439
Merge remote-tracking branch 'elastic/main' into cross-project/prohib…
quux00 Sep 2, 2025
5b90e78
Merge remote-tracking branch 'elastic/main' into cross-project/prohib…
quux00 Sep 5, 2025
6ac6566
Merge remote-tracking branch 'elastic/main' into cross-project/prohib…
quux00 Sep 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

package org.elasticsearch.index.mapper;

/**
* No-op Default of RootObjectMapperNamespaceValidator used in non-serverless Elasticsearch envs.
*/
public class DefaultRootObjectMapperNamespaceValidator implements RootObjectMapperNamespaceValidator {
@Override
public void validateNamespace(ObjectMapper.Subobjects subobjects, String name) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,23 @@ public final class MapperRegistry {
private final Map<String, MetadataFieldMapper.TypeParser> metadataMapperParsers6x;
private final Map<String, MetadataFieldMapper.TypeParser> metadataMapperParsers5x;
private final Function<String, FieldPredicate> fieldFilter;
private final RootObjectMapperNamespaceValidator namespaceValidator;

public MapperRegistry(
Map<String, Mapper.TypeParser> mapperParsers,
Map<String, RuntimeField.Parser> runtimeFieldParsers,
Map<String, MetadataFieldMapper.TypeParser> metadataMapperParsers,
Function<String, FieldPredicate> fieldFilter
) {
this(mapperParsers, runtimeFieldParsers, metadataMapperParsers, fieldFilter, null);
}

public MapperRegistry(
Map<String, Mapper.TypeParser> mapperParsers,
Map<String, RuntimeField.Parser> runtimeFieldParsers,
Map<String, MetadataFieldMapper.TypeParser> metadataMapperParsers,
Function<String, FieldPredicate> fieldFilter,
RootObjectMapperNamespaceValidator namespaceValidator
) {
this.mapperParsers = Collections.unmodifiableMap(new LinkedHashMap<>(mapperParsers));
this.runtimeFieldParsers = runtimeFieldParsers;
Expand All @@ -50,6 +61,7 @@ public MapperRegistry(
metadata5x.put(LegacyTypeFieldMapper.NAME, LegacyTypeFieldMapper.PARSER);
this.metadataMapperParsers5x = metadata5x;
this.fieldFilter = fieldFilter;
this.namespaceValidator = namespaceValidator;
}

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

public RootObjectMapperNamespaceValidator getNamespaceValidator() {
return namespaceValidator;
}

/**
* Return a map of the meta mappers that have been registered. The
* returned map uses the name of the field as a key.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,8 @@ public MapperService(
indexAnalyzers,
indexSettings,
idFieldMapper,
bitSetProducer
bitSetProducer,
mapperRegistry.getNamespaceValidator()
);
this.documentParser = new DocumentParser(parserConfiguration, this.mappingParserContextSupplier.get());
Map<String, MetadataFieldMapper.TypeParser> metadataMapperParsers = mapperRegistry.getMetadataMapperParsers(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public class MappingParserContext {
private final Function<Query, BitSetProducer> bitSetProducer;
private final long mappingObjectDepthLimit;
private long mappingObjectDepth = 0;
private final RootObjectMapperNamespaceValidator namespaceValidator;

public MappingParserContext(
Function<String, SimilarityProvider> similarityLookupService,
Expand All @@ -55,7 +56,8 @@ public MappingParserContext(
IndexAnalyzers indexAnalyzers,
IndexSettings indexSettings,
IdFieldMapper idFieldMapper,
Function<Query, BitSetProducer> bitSetProducer
Function<Query, BitSetProducer> bitSetProducer,
RootObjectMapperNamespaceValidator namespaceValidator
) {
this.similarityLookupService = similarityLookupService;
this.typeParsers = typeParsers;
Expand All @@ -69,6 +71,40 @@ public MappingParserContext(
this.idFieldMapper = idFieldMapper;
this.mappingObjectDepthLimit = indexSettings.getMappingDepthLimit();
this.bitSetProducer = bitSetProducer;
this.namespaceValidator = namespaceValidator;
}

public MappingParserContext(
Function<String, SimilarityProvider> similarityLookupService,
Function<String, Mapper.TypeParser> typeParsers,
Function<String, RuntimeField.Parser> runtimeFieldParsers,
IndexVersion indexVersionCreated,
Supplier<TransportVersion> clusterTransportVersion,
Supplier<SearchExecutionContext> searchExecutionContextSupplier,
ScriptCompiler scriptCompiler,
IndexAnalyzers indexAnalyzers,
IndexSettings indexSettings,
IdFieldMapper idFieldMapper,
Function<Query, BitSetProducer> bitSetProducer
) {
this(
similarityLookupService,
typeParsers,
runtimeFieldParsers,
indexVersionCreated,
clusterTransportVersion,
searchExecutionContextSupplier,
scriptCompiler,
indexAnalyzers,
indexSettings,
idFieldMapper,
bitSetProducer,
null
);
}

public RootObjectMapperNamespaceValidator getNamespaceValidator() {
return namespaceValidator;
}

public IndexAnalyzers getIndexAnalyzers() {
Expand Down Expand Up @@ -170,7 +206,8 @@ private static class MultiFieldParserContext extends MappingParserContext {
in.indexAnalyzers,
in.indexSettings,
in.idFieldMapper,
in.bitSetProducer
in.bitSetProducer,
in.namespaceValidator
);
}

Expand Down Expand Up @@ -200,7 +237,8 @@ private static class DynamicTemplateParserContext extends MappingParserContext {
in.indexAnalyzers,
in.indexSettings,
in.idFieldMapper,
in.bitSetProducer
in.bitSetProducer,
in.namespaceValidator
);
this.dateFormatter = dateFormatter;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -548,12 +548,21 @@ public final Optional<SourceKeepMode> sourceKeepMode() {
}

@Override
public void validate(MappingLookup mappers) {
public final void validate(MappingLookup mappers) {
for (Mapper mapper : this.mappers.values()) {
mapper.validate(mappers);
validateSubField(mapper, mappers);
}
}

/**
* This method is separated out to allow subclasses (such as RootObjectMapper) to
* override it and add in additional validations beyond what the mapper.validate()
* method will check on each mapping.
*/
protected void validateSubField(Mapper mapper, MappingLookup mappers) {
mapper.validate(mappers);
}

protected MapperMergeContext createChildContext(MapperMergeContext mapperMergeContext, String name) {
return mapperMergeContext.createChildContext(name, dynamic);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public static class Builder extends ObjectMapper.Builder {
protected final Map<String, RuntimeField> runtimeFields = new HashMap<>();
protected Explicit<Boolean> dateDetection = Defaults.DATE_DETECTION;
protected Explicit<Boolean> numericDetection = Defaults.NUMERIC_DETECTION;
protected RootObjectMapperNamespaceValidator namespaceValidator;

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

public Builder addNamespaceValidator(RootObjectMapperNamespaceValidator namespaceValidator) {
this.namespaceValidator = namespaceValidator;
return this;
}

public Builder dynamicDateTimeFormatter(Collection<DateFormatter> dateTimeFormatters) {
this.dynamicDateTimeFormatters = new Explicit<>(dateTimeFormatters.toArray(new DateFormatter[0]), true);
return this;
Expand Down Expand Up @@ -124,7 +130,8 @@ public RootObjectMapper build(MapperBuilderContext context) {
dynamicDateTimeFormatters,
dynamicTemplates,
dateDetection,
numericDetection
numericDetection,
namespaceValidator
);
}
}
Expand All @@ -134,6 +141,7 @@ public RootObjectMapper build(MapperBuilderContext context) {
private final Explicit<Boolean> numericDetection;
private final Explicit<DynamicTemplate[]> dynamicTemplates;
private final Map<String, RuntimeField> runtimeFields;
private final RootObjectMapperNamespaceValidator namespaceValidator;

RootObjectMapper(
String name,
Expand All @@ -146,14 +154,16 @@ public RootObjectMapper build(MapperBuilderContext context) {
Explicit<DateFormatter[]> dynamicDateTimeFormatters,
Explicit<DynamicTemplate[]> dynamicTemplates,
Explicit<Boolean> dateDetection,
Explicit<Boolean> numericDetection
Explicit<Boolean> numericDetection,
RootObjectMapperNamespaceValidator namespaceValidator
) {
super(name, name, enabled, subobjects, sourceKeepMode, dynamic, mappers);
this.runtimeFields = runtimeFields;
this.dynamicTemplates = dynamicTemplates;
this.dynamicDateTimeFormatters = dynamicDateTimeFormatters;
this.dateDetection = dateDetection;
this.numericDetection = numericDetection;
this.namespaceValidator = namespaceValidator == null ? new DefaultRootObjectMapperNamespaceValidator() : namespaceValidator;
if (sourceKeepMode.orElse(SourceKeepMode.NONE) == SourceKeepMode.ALL) {
throw new MapperParsingException(
"root object can't be configured with [" + Mapper.SYNTHETIC_SOURCE_KEEP_PARAM + ":" + SourceKeepMode.ALL + "]"
Expand Down Expand Up @@ -182,7 +192,8 @@ RootObjectMapper withoutMappers() {
dynamicDateTimeFormatters,
dynamicTemplates,
dateDetection,
numericDetection
numericDetection,
namespaceValidator
);
}

Expand Down Expand Up @@ -298,7 +309,8 @@ public RootObjectMapper merge(Mapper mergeWith, MapperMergeContext parentMergeCo
dynamicDateTimeFormatters,
dynamicTemplates,
dateDetection,
numericDetection
numericDetection,
namespaceValidator
);
}

Expand Down Expand Up @@ -455,6 +467,7 @@ public static RootObjectMapper.Builder parse(String name, Map<String, Object> no
throws MapperParsingException {
Explicit<Subobjects> subobjects = parseSubobjects(node);
RootObjectMapper.Builder builder = new Builder(name, subobjects);
builder.addNamespaceValidator(parserContext.getNamespaceValidator());
Iterator<Map.Entry<String, Object>> iterator = node.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Object> entry = iterator.next();
Expand Down Expand Up @@ -544,4 +557,14 @@ private static boolean processField(
public int getTotalFieldsCount() {
return super.getTotalFieldsCount() - 1 + runtimeFields.size();
}

/**
* Overrides in order to run the namespace validator first and then delegates to the
* standard validateSubField on the parent class
*/
@Override
protected void validateSubField(Mapper mapper, MappingLookup mappers) {
namespaceValidator.validateNamespace(subobjects(), mapper.leafName());
super.validateSubField(mapper, mappers);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

package org.elasticsearch.index.mapper;

import org.elasticsearch.core.Nullable;

/**
* SPI to inject additional rules around namespaces (top level fields) that are prohibited
* in Elasticsearch mappings.
*/
public interface RootObjectMapperNamespaceValidator {
/**
* If the namespace in the mapper is not allowed, an Exception should be thrown.
* @param subobjects Whether subobjects are enabled. Null is allowed
* @param name namespace (field name) to validate
*/
void validateNamespace(@Nullable ObjectMapper.Subobjects subobjects, String name);
}
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@ static Map<String, RuntimeField> parseRuntimeFields(
+ " Check the documentation."
);
}
if (parserContext.getNamespaceValidator() != null) {
parserContext.getNamespaceValidator().validateNamespace(null, fieldName);
}
runtimeFields.put(fieldName, builder.apply(typeParser.parse(fieldName, propNode, parserContext)));
propNode.remove("type");
MappingParser.checkNoRemainingFields(fieldName, propNode);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
import org.elasticsearch.index.mapper.ObjectMapper;
import org.elasticsearch.index.mapper.PassThroughObjectMapper;
import org.elasticsearch.index.mapper.RangeType;
import org.elasticsearch.index.mapper.RootObjectMapperNamespaceValidator;
import org.elasticsearch.index.mapper.RoutingFieldMapper;
import org.elasticsearch.index.mapper.RuntimeField;
import org.elasticsearch.index.mapper.SeqNoFieldMapper;
Expand Down Expand Up @@ -94,15 +95,20 @@
public class IndicesModule extends AbstractModule {
private final MapperRegistry mapperRegistry;

public IndicesModule(List<MapperPlugin> mapperPlugins) {
public IndicesModule(List<MapperPlugin> mapperPlugins, RootObjectMapperNamespaceValidator namespaceValidator) {
this.mapperRegistry = new MapperRegistry(
getMappers(mapperPlugins),
getRuntimeFields(mapperPlugins),
getMetadataMappers(mapperPlugins),
getFieldFilter(mapperPlugins)
getFieldFilter(mapperPlugins),
namespaceValidator
);
}

public IndicesModule(List<MapperPlugin> mapperPlugins) {
this(mapperPlugins, null);
}

public static List<NamedWriteableRegistry.Entry> getNamedWriteables() {
return Arrays.asList(
new NamedWriteableRegistry.Entry(Condition.class, MinAgeCondition.NAME, MinAgeCondition::new),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,9 @@
import org.elasticsearch.index.SlowLogFields;
import org.elasticsearch.index.analysis.AnalysisRegistry;
import org.elasticsearch.index.engine.MergeMetrics;
import org.elasticsearch.index.mapper.DefaultRootObjectMapperNamespaceValidator;
import org.elasticsearch.index.mapper.MapperMetrics;
import org.elasticsearch.index.mapper.RootObjectMapperNamespaceValidator;
import org.elasticsearch.index.mapper.SourceFieldMetrics;
import org.elasticsearch.index.search.stats.ShardSearchPhaseAPMMetrics;
import org.elasticsearch.index.shard.SearchOperationListener;
Expand Down Expand Up @@ -698,6 +700,12 @@ private void construct(

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

RootObjectMapperNamespaceValidator namespaceValidator = pluginsService.loadSingletonServiceProvider(
RootObjectMapperNamespaceValidator.class,
() -> new DefaultRootObjectMapperNamespaceValidator()
);
modules.bindToInstance(RootObjectMapperNamespaceValidator.class, namespaceValidator);

assert nodeEnvironment.nodeId() != null : "node ID must be set before constructing the Node";
TaskManager taskManager = new TaskManager(
settings,
Expand Down Expand Up @@ -814,7 +822,7 @@ private void construct(
)::onNewInfo
);

IndicesModule indicesModule = new IndicesModule(pluginsService.filterPlugins(MapperPlugin.class).toList());
IndicesModule indicesModule = new IndicesModule(pluginsService.filterPlugins(MapperPlugin.class).toList(), namespaceValidator);
modules.add(indicesModule);

modules.add(new GatewayModule());
Expand Down
Loading