From 6c7575e5f52d31a347755f13b5641aead1c25e16 Mon Sep 17 00:00:00 2001 From: Michael Peterson Date: Fri, 1 Aug 2025 16:30:43 -0400 Subject: [PATCH 01/15] Init commit: same as cps/prohibit-_project-mappings branch --- .../index/mapper/MapperRegistry.java | 22 +++++++++++++++++++ .../index/mapper/ObjectMapper.java | 7 +++++- .../index/mapper/RootObjectMapper.java | 17 ++++++++++++++ .../RootObjectMapperNamespaceValidator.java | 17 ++++++++++++++ .../elasticsearch/indices/IndicesModule.java | 21 +++++++++++++++++- 5 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapperNamespaceValidator.java diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MapperRegistry.java b/server/src/main/java/org/elasticsearch/index/mapper/MapperRegistry.java index 44f7def74ec0e..4b80ccc8b70bd 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/MapperRegistry.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/MapperRegistry.java @@ -31,12 +31,29 @@ public final class MapperRegistry { private final Map metadataMapperParsers6x; private final Map metadataMapperParsers5x; private final Function fieldFilter; + private final RootObjectMapperNamespaceValidator namespaceValidator; + // MP TODO: remove this? Or keep? public MapperRegistry( Map mapperParsers, Map runtimeFieldParsers, Map metadataMapperParsers, Function fieldFilter + ) { + // MP TODO: remove this no-op RootObjectMapperNamespaceValidator once we know how all this is going to work + this(mapperParsers, runtimeFieldParsers, metadataMapperParsers, fieldFilter, new RootObjectMapperNamespaceValidator() { + @Override + public void validateNamespace(ObjectMapper.Subobjects subobjects, Mapper mapper) {} + }); + } + + // MP TODO: need to move/create tests to use this + public MapperRegistry( + Map mapperParsers, + Map runtimeFieldParsers, + Map metadataMapperParsers, + Function fieldFilter, + RootObjectMapperNamespaceValidator namespaceValidator ) { this.mapperParsers = Collections.unmodifiableMap(new LinkedHashMap<>(mapperParsers)); this.runtimeFieldParsers = runtimeFieldParsers; @@ -50,6 +67,7 @@ public MapperRegistry( metadata5x.put(LegacyTypeFieldMapper.NAME, LegacyTypeFieldMapper.PARSER); this.metadataMapperParsers5x = metadata5x; this.fieldFilter = fieldFilter; + this.namespaceValidator = namespaceValidator; } /** @@ -72,6 +90,10 @@ public Map 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. diff --git a/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java index 33ed032730561..a00878ff072e8 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java @@ -548,12 +548,17 @@ public final Optional 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); } } + protected void validateSubField(Mapper mapper, MappingLookup mappers) { + mapper.validate(mappers); + } + protected MapperMergeContext createChildContext(MapperMergeContext mapperMergeContext, String name) { return mapperMergeContext.createChildContext(name, dynamic); } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java index 2fe82b4eacfc5..07f566193dca3 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java @@ -540,4 +540,21 @@ private static boolean processField( public int getTotalFieldsCount() { return super.getTotalFieldsCount() - 1 + runtimeFields.size(); } + + // MP TODO: - this needs to move to a serverless class, right? + private static final String RESERVED_NAMESPACE = "_project"; + + @Override + protected void validateSubField(Mapper mapper, MappingLookup mappers) { + if (subobjects() == Subobjects.ENABLED) { + if (mapper.leafName().equals(RESERVED_NAMESPACE)) { + throw new IllegalArgumentException("Reserved Namespace. Fields may not start with " + RESERVED_NAMESPACE); + } + } else { + if (mapper.leafName().startsWith(RESERVED_NAMESPACE)) { + throw new IllegalArgumentException("Reserved Namespace. Fields may not start with " + RESERVED_NAMESPACE); + } + } + super.validateSubField(mapper, mappers); + } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapperNamespaceValidator.java b/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapperNamespaceValidator.java new file mode 100644 index 0000000000000..43e2aec526b6e --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapperNamespaceValidator.java @@ -0,0 +1,17 @@ +/* + * 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; + +/** + * TODO: DOCUMENT ME + */ +public interface RootObjectMapperNamespaceValidator { + void validateNamespace(ObjectMapper.Subobjects subobjects, Mapper mapper); +} diff --git a/server/src/main/java/org/elasticsearch/indices/IndicesModule.java b/server/src/main/java/org/elasticsearch/indices/IndicesModule.java index 09be98630d5c4..8bb22bbbf0de2 100644 --- a/server/src/main/java/org/elasticsearch/indices/IndicesModule.java +++ b/server/src/main/java/org/elasticsearch/indices/IndicesModule.java @@ -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; @@ -94,12 +95,30 @@ public class IndicesModule extends AbstractModule { private final MapperRegistry mapperRegistry; + // TODO: this needs to be loaded from serverless somehow + private static final String RESERVED_NAMESPACE = "_project"; + public IndicesModule(List mapperPlugins) { this.mapperRegistry = new MapperRegistry( getMappers(mapperPlugins), getRuntimeFields(mapperPlugins), getMetadataMappers(mapperPlugins), - getFieldFilter(mapperPlugins) + getFieldFilter(mapperPlugins), + new RootObjectMapperNamespaceValidator() { + @Override + public void validateNamespace(ObjectMapper.Subobjects subobjects, Mapper mapper) { + // TODO: in the future, this will be a no-op on stateful and loaded somehow dynamically in serverless + if (subobjects == ObjectMapper.Subobjects.ENABLED) { + if (mapper.leafName().equals(RESERVED_NAMESPACE)) { + throw new IllegalArgumentException("xx reserved namespace: [" + RESERVED_NAMESPACE + ']'); + } + } else { + if (mapper.leafName().startsWith(RESERVED_NAMESPACE)) { + throw new IllegalArgumentException("xx reserved namespace: [" + RESERVED_NAMESPACE + ']'); + } + } + } + } ); } From 5c0c7ea9e7b4c088c45a6e75fed637ecad50b8e1 Mon Sep 17 00:00:00 2001 From: Michael Peterson Date: Tue, 19 Aug 2025 09:47:27 -0400 Subject: [PATCH 02/15] Most manual tests now pass. The two that still don't work right are: 1. One case with subobjects:false doesn't work as hoped: // this does NOT return an error - it just silently "fails" to create the _project entry PUT test333?error_trace=true { "mappings": { "subobjects": false, "properties" : { "_project" : { "type" : "object" } } } } // YAY - this one fails with correct error message PUT test333?error_trace=true { "mappings": { "subobjects": false, "properties": { "_project": { "type": "object", "properties": { "myfield": { "type": "keyword" } } } } } } 2. Creating a runtime mapping is not detected and prevented: PUT /rt-index { "mappings": { "runtime": { "_project": { "type": "keyword", "script": { "source": "emit(doc['existing_field'].value + ' some additional text')" } } } } } --- ...ultRootObjectMapperNamespaceValidator.java | 24 ++++++++ .../index/mapper/MapperService.java | 3 +- .../index/mapper/MappingParserContext.java | 39 ++++++++++++- .../index/mapper/RootObjectMapper.java | 34 +++++++----- .../RootObjectMapperNamespaceValidator.java | 6 +- ...essRootObjectMapperNamespaceValidator.java | 55 +++++++++++++++++++ .../elasticsearch/indices/IndicesModule.java | 42 +++++++------- .../elasticsearch/node/NodeConstruction.java | 13 ++++- 8 files changed, 178 insertions(+), 38 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/index/mapper/DefaultRootObjectMapperNamespaceValidator.java create mode 100644 server/src/main/java/org/elasticsearch/index/mapper/ServerlessRootObjectMapperNamespaceValidator.java diff --git a/server/src/main/java/org/elasticsearch/index/mapper/DefaultRootObjectMapperNamespaceValidator.java b/server/src/main/java/org/elasticsearch/index/mapper/DefaultRootObjectMapperNamespaceValidator.java new file mode 100644 index 0000000000000..56bdc1f692588 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/mapper/DefaultRootObjectMapperNamespaceValidator.java @@ -0,0 +1,24 @@ +/* + * 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, Mapper mapper) {} + + // MP FIXME remove + @Override + public String toString() { + return "I'm the DefaultRootObjectMapperNamespaceValidator{}"; + } +} diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java b/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java index 7958fd8e51525..e0a4aca3d83f0 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/MapperService.java @@ -245,7 +245,8 @@ public MapperService( indexAnalyzers, indexSettings, idFieldMapper, - bitSetProducer + bitSetProducer, + mapperRegistry.getNamespaceValidator() ); this.documentParser = new DocumentParser(parserConfiguration, this.mappingParserContextSupplier.get()); Map metadataMapperParsers = mapperRegistry.getMetadataMapperParsers( diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MappingParserContext.java b/server/src/main/java/org/elasticsearch/index/mapper/MappingParserContext.java index f74a257f32921..07676324cbb3c 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/MappingParserContext.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/MappingParserContext.java @@ -43,6 +43,7 @@ public class MappingParserContext { private final Function bitSetProducer; private final long mappingObjectDepthLimit; private long mappingObjectDepth = 0; + private final RootObjectMapperNamespaceValidator namespaceValidator; public MappingParserContext( Function similarityLookupService, @@ -55,7 +56,8 @@ public MappingParserContext( IndexAnalyzers indexAnalyzers, IndexSettings indexSettings, IdFieldMapper idFieldMapper, - Function bitSetProducer + Function bitSetProducer, + RootObjectMapperNamespaceValidator namespaceValidator ) { this.similarityLookupService = similarityLookupService; this.typeParsers = typeParsers; @@ -69,6 +71,41 @@ public MappingParserContext( this.idFieldMapper = idFieldMapper; this.mappingObjectDepthLimit = indexSettings.getMappingDepthLimit(); this.bitSetProducer = bitSetProducer; + this.namespaceValidator = namespaceValidator; + } + + // MP TODO: only used by tests, so remove this after tests are updated? + public MappingParserContext( + Function similarityLookupService, + Function typeParsers, + Function runtimeFieldParsers, + IndexVersion indexVersionCreated, + Supplier clusterTransportVersion, + Supplier searchExecutionContextSupplier, + ScriptCompiler scriptCompiler, + IndexAnalyzers indexAnalyzers, + IndexSettings indexSettings, + IdFieldMapper idFieldMapper, + Function bitSetProducer + ) { + this( + similarityLookupService, + typeParsers, + runtimeFieldParsers, + indexVersionCreated, + clusterTransportVersion, + searchExecutionContextSupplier, + scriptCompiler, + indexAnalyzers, + indexSettings, + idFieldMapper, + bitSetProducer, + null + ); + } + + public RootObjectMapperNamespaceValidator getNamespaceValidator() { + return namespaceValidator; } public IndexAnalyzers getIndexAnalyzers() { diff --git a/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java index 07f566193dca3..bd153b4e385c7 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java @@ -76,11 +76,17 @@ public static class Builder extends ObjectMapper.Builder { protected final Map runtimeFields = new HashMap<>(); protected Explicit dateDetection = Defaults.DATE_DETECTION; protected Explicit numericDetection = Defaults.NUMERIC_DETECTION; + protected RootObjectMapperNamespaceValidator namespaceValidator; public Builder(String name, Optional subobjects) { super(name, subobjects); } + public Builder addNamespaceValidator(RootObjectMapperNamespaceValidator namespaceValidator) { + this.namespaceValidator = namespaceValidator; + return this; + } + public Builder dynamicDateTimeFormatter(Collection dateTimeFormatters) { this.dynamicDateTimeFormatters = new Explicit<>(dateTimeFormatters.toArray(new DateFormatter[0]), true); return this; @@ -120,7 +126,8 @@ public RootObjectMapper build(MapperBuilderContext context) { dynamicDateTimeFormatters, dynamicTemplates, dateDetection, - numericDetection + numericDetection, + namespaceValidator ); } } @@ -130,6 +137,7 @@ public RootObjectMapper build(MapperBuilderContext context) { private final Explicit numericDetection; private final Explicit dynamicTemplates; private final Map runtimeFields; + private final RootObjectMapperNamespaceValidator namespaceValidator; RootObjectMapper( String name, @@ -142,7 +150,8 @@ public RootObjectMapper build(MapperBuilderContext context) { Explicit dynamicDateTimeFormatters, Explicit dynamicTemplates, Explicit dateDetection, - Explicit numericDetection + Explicit numericDetection, + RootObjectMapperNamespaceValidator namespaceValidator ) { super(name, name, enabled, subobjects, sourceKeepMode, dynamic, mappers); this.runtimeFields = runtimeFields; @@ -150,6 +159,7 @@ public RootObjectMapper build(MapperBuilderContext context) { this.dynamicDateTimeFormatters = dynamicDateTimeFormatters; this.dateDetection = dateDetection; this.numericDetection = numericDetection; + this.namespaceValidator = 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 + "]" @@ -178,7 +188,8 @@ RootObjectMapper withoutMappers() { dynamicDateTimeFormatters, dynamicTemplates, dateDetection, - numericDetection + numericDetection, + namespaceValidator ); } @@ -294,7 +305,8 @@ public RootObjectMapper merge(Mapper mergeWith, MapperMergeContext parentMergeCo dynamicDateTimeFormatters, dynamicTemplates, dateDetection, - numericDetection + numericDetection, + namespaceValidator ); } @@ -451,6 +463,7 @@ public static RootObjectMapper.Builder parse(String name, Map no throws MapperParsingException { Optional subobjects = parseSubobjects(node); RootObjectMapper.Builder builder = new Builder(name, subobjects); + builder.addNamespaceValidator(parserContext.getNamespaceValidator()); Iterator> iterator = node.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry entry = iterator.next(); @@ -541,19 +554,10 @@ public int getTotalFieldsCount() { return super.getTotalFieldsCount() - 1 + runtimeFields.size(); } - // MP TODO: - this needs to move to a serverless class, right? - private static final String RESERVED_NAMESPACE = "_project"; - @Override protected void validateSubField(Mapper mapper, MappingLookup mappers) { - if (subobjects() == Subobjects.ENABLED) { - if (mapper.leafName().equals(RESERVED_NAMESPACE)) { - throw new IllegalArgumentException("Reserved Namespace. Fields may not start with " + RESERVED_NAMESPACE); - } - } else { - if (mapper.leafName().startsWith(RESERVED_NAMESPACE)) { - throw new IllegalArgumentException("Reserved Namespace. Fields may not start with " + RESERVED_NAMESPACE); - } + if (namespaceValidator != null) { + namespaceValidator.validateNamespace(subobjects(), mapper); } super.validateSubField(mapper, mappers); } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapperNamespaceValidator.java b/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapperNamespaceValidator.java index 43e2aec526b6e..79167edc56b7e 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapperNamespaceValidator.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapperNamespaceValidator.java @@ -10,8 +10,12 @@ package org.elasticsearch.index.mapper; /** - * TODO: DOCUMENT ME + * SPI to inject additional rules around namespaces that are prohibited + * in Elasticsearch mappings. */ public interface RootObjectMapperNamespaceValidator { + /** + * If the namespace in the mapper is not allowed, an Exception should be thrown. + */ void validateNamespace(ObjectMapper.Subobjects subobjects, Mapper mapper); } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/ServerlessRootObjectMapperNamespaceValidator.java b/server/src/main/java/org/elasticsearch/index/mapper/ServerlessRootObjectMapperNamespaceValidator.java new file mode 100644 index 0000000000000..fd06bb38621dd --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/mapper/ServerlessRootObjectMapperNamespaceValidator.java @@ -0,0 +1,55 @@ +/* + * 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.common.Strings; + +/** + * In serverless, prohibits mappings that start with _project, including subfields such as _project.foo. + * The _project "namespace" in serverless is reserved in order to allow users to include project metadata tags + * (e.g., _project._region or _project._csp, etc.) which are useful in cross-project search and ES|QL queries. + */ +public class ServerlessRootObjectMapperNamespaceValidator implements RootObjectMapperNamespaceValidator { + private static final String SERVERLESS_RESERVED_NAMESPACE = "_project"; + + // MP TODO: we can also pass in a MappingLookup - would that help here? + @Override + public void validateNamespace(ObjectMapper.Subobjects subobjects, Mapper mapper) { // TODO: stop passing in Mapper and pass in String + // fieldName + if (mapper.leafName().equals(SERVERLESS_RESERVED_NAMESPACE)) { + System.err.println("XX: 1A: " + mapper.leafName()); + System.err.println("XX: 1B: " + mapper.fullPath()); + System.err.println("XX: 1C: " + mapper.typeName()); + System.err.println("XX: 1D: " + mapper); + throw new IllegalArgumentException(generateErrorMessage()); + } else if (subobjects != ObjectMapper.Subobjects.ENABLED) { + System.err.println("YYY: 2A: " + mapper.leafName()); + System.err.println("YYY: 2B: " + mapper.fullPath()); + System.err.println("YYY: 2C: " + mapper.typeName()); + System.err.println("YYY: 2D: " + mapper); + // leafName here will be something like _project.myfield, rather than just _project + if (mapper.leafName().startsWith(SERVERLESS_RESERVED_NAMESPACE + ".")) { + throw new IllegalArgumentException(generateErrorMessage(mapper.leafName())); + } + } + } + + private String generateErrorMessage(String fieldName) { + return Strings.format( + "Mapping rejected%s. No mappings of [%s] are allowed in order to avoid conflicts with project metadata tags in serverless", + fieldName == null ? "" : ": [" + fieldName + "]", + SERVERLESS_RESERVED_NAMESPACE + ); + } + + private String generateErrorMessage() { + return generateErrorMessage(null); + } +} diff --git a/server/src/main/java/org/elasticsearch/indices/IndicesModule.java b/server/src/main/java/org/elasticsearch/indices/IndicesModule.java index 8bb22bbbf0de2..3dc3000fc9354 100644 --- a/server/src/main/java/org/elasticsearch/indices/IndicesModule.java +++ b/server/src/main/java/org/elasticsearch/indices/IndicesModule.java @@ -95,33 +95,37 @@ public class IndicesModule extends AbstractModule { private final MapperRegistry mapperRegistry; - // TODO: this needs to be loaded from serverless somehow - private static final String RESERVED_NAMESPACE = "_project"; - - public IndicesModule(List mapperPlugins) { + public IndicesModule(List mapperPlugins, RootObjectMapperNamespaceValidator namespaceValidator) { + // this is the only place that the MapperRegistry is created this.mapperRegistry = new MapperRegistry( getMappers(mapperPlugins), getRuntimeFields(mapperPlugins), getMetadataMappers(mapperPlugins), getFieldFilter(mapperPlugins), - new RootObjectMapperNamespaceValidator() { - @Override - public void validateNamespace(ObjectMapper.Subobjects subobjects, Mapper mapper) { - // TODO: in the future, this will be a no-op on stateful and loaded somehow dynamically in serverless - if (subobjects == ObjectMapper.Subobjects.ENABLED) { - if (mapper.leafName().equals(RESERVED_NAMESPACE)) { - throw new IllegalArgumentException("xx reserved namespace: [" + RESERVED_NAMESPACE + ']'); - } - } else { - if (mapper.leafName().startsWith(RESERVED_NAMESPACE)) { - throw new IllegalArgumentException("xx reserved namespace: [" + RESERVED_NAMESPACE + ']'); - } - } - } - } + namespaceValidator + // new RootObjectMapperNamespaceValidator() { + // @Override + // public void validateNamespace(ObjectMapper.Subobjects subobjects, Mapper mapper) { + // // TODO: in the future, this will be a no-op on stateful and loaded somehow dynamically in serverless + // if (subobjects == ObjectMapper.Subobjects.ENABLED) { + // if (mapper.leafName().equals(RESERVED_NAMESPACE)) { + // throw new IllegalArgumentException("xx reserved namespace: [" + RESERVED_NAMESPACE + ']'); + // } + // } else { + // if (mapper.leafName().startsWith(RESERVED_NAMESPACE)) { + // throw new IllegalArgumentException("xx reserved namespace: [" + RESERVED_NAMESPACE + ']'); + // } + // } + // } + // } ); } + // MP TODO: remove this constructor once all tests have been updated + public IndicesModule(List mapperPlugins) { + this(mapperPlugins, null); + } + public static List getNamedWriteables() { return Arrays.asList( new NamedWriteableRegistry.Entry(Condition.class, MinAgeCondition.NAME, MinAgeCondition::new), diff --git a/server/src/main/java/org/elasticsearch/node/NodeConstruction.java b/server/src/main/java/org/elasticsearch/node/NodeConstruction.java index 3dfbe960a8e89..9ca375f6199e0 100644 --- a/server/src/main/java/org/elasticsearch/node/NodeConstruction.java +++ b/server/src/main/java/org/elasticsearch/node/NodeConstruction.java @@ -119,6 +119,8 @@ import org.elasticsearch.index.analysis.AnalysisRegistry; import org.elasticsearch.index.engine.MergeMetrics; import org.elasticsearch.index.mapper.MapperMetrics; +import org.elasticsearch.index.mapper.RootObjectMapperNamespaceValidator; +import org.elasticsearch.index.mapper.ServerlessRootObjectMapperNamespaceValidator; import org.elasticsearch.index.mapper.SourceFieldMetrics; import org.elasticsearch.index.search.stats.ShardSearchPhaseAPMMetrics; import org.elasticsearch.index.shard.SearchOperationListener; @@ -695,6 +697,15 @@ private void construct( modules.bindToInstance(Tracer.class, telemetryProvider.getTracer()); + // serverless deployments plug-in the namespace validator that prohibits mappings with "_project" + RootObjectMapperNamespaceValidator namespaceValidator = pluginsService.loadSingletonServiceProvider( + RootObjectMapperNamespaceValidator.class, + // () -> new DefaultRootObjectMapperNamespaceValidator() + () -> new ServerlessRootObjectMapperNamespaceValidator() // MP FIXME - for this testing branch only + ); + modules.bindToInstance(RootObjectMapperNamespaceValidator.class, namespaceValidator); + logger.warn("XXX namespaceValidator loaded: " + namespaceValidator); // MP FIXME remove + assert nodeEnvironment.nodeId() != null : "node ID must be set before constructing the Node"; TaskManager taskManager = new TaskManager( settings, @@ -806,7 +817,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()); From f931cefbf7c7e8d51528c72dfdc9d9cefe7d613c Mon Sep 17 00:00:00 2001 From: Michael Peterson Date: Tue, 19 Aug 2025 10:49:42 -0400 Subject: [PATCH 03/15] Hardcoded hack to get runtime fields with _project mappings to be detected - basic manual tests now pass --- .../index/mapper/RootObjectMapper.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java index bd153b4e385c7..75dc2fb88aec1 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java @@ -470,7 +470,7 @@ public static RootObjectMapper.Builder parse(String name, Map no String fieldName = entry.getKey(); Object fieldNode = entry.getValue(); if (parseObjectOrDocumentTypeProperties(fieldName, fieldNode, parserContext, builder) - || processField(builder, fieldName, fieldNode, parserContext)) { + || processField(builder, fieldName, fieldNode, parserContext, parserContext.getNamespaceValidator())) { iterator.remove(); } } @@ -482,7 +482,8 @@ private static boolean processField( RootObjectMapper.Builder builder, String fieldName, Object fieldNode, - MappingParserContext parserContext + MappingParserContext parserContext, + RootObjectMapperNamespaceValidator namespaceValidator ) { if (fieldName.equals("date_formats") || fieldName.equals("dynamic_date_formats")) { if (fieldNode instanceof List) { @@ -540,6 +541,16 @@ private static boolean processField( } else if (fieldName.equals("runtime")) { if (fieldNode instanceof Map) { Map fields = RuntimeField.parseRuntimeFields((Map) fieldNode, parserContext, true); + if (namespaceValidator != null) { + for (String runtimeField : fields.keySet()) { + if ("_project".equals(runtimeField) || runtimeField.startsWith("_project.")) { + throw new IllegalArgumentException( + "Runtime mapping rejected. No mappings of [_project] are allowed in order " + + "to avoid conflicts with project metadata tags in serverless." + ); + } + } + } builder.addRuntimeFields(fields); return true; } else { From 0350cacf48177e1f888740ba2030339f2aa6fda5 Mon Sep 17 00:00:00 2001 From: Michael Peterson Date: Tue, 19 Aug 2025 12:42:26 -0400 Subject: [PATCH 04/15] Refactored validateNamespace signature so that RootObjectMapper.processField can call it without a Mapper reference --- ...ultRootObjectMapperNamespaceValidator.java | 2 +- .../index/mapper/MapperRegistry.java | 2 +- .../index/mapper/RootObjectMapper.java | 9 ++------ .../RootObjectMapperNamespaceValidator.java | 2 +- ...essRootObjectMapperNamespaceValidator.java | 21 ++++++------------- 5 files changed, 11 insertions(+), 25 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/mapper/DefaultRootObjectMapperNamespaceValidator.java b/server/src/main/java/org/elasticsearch/index/mapper/DefaultRootObjectMapperNamespaceValidator.java index 56bdc1f692588..b76d22db1fc43 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/DefaultRootObjectMapperNamespaceValidator.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/DefaultRootObjectMapperNamespaceValidator.java @@ -14,7 +14,7 @@ */ public class DefaultRootObjectMapperNamespaceValidator implements RootObjectMapperNamespaceValidator { @Override - public void validateNamespace(ObjectMapper.Subobjects subobjects, Mapper mapper) {} + public void validateNamespace(ObjectMapper.Subobjects subobjects, String name) {} // MP FIXME remove @Override diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MapperRegistry.java b/server/src/main/java/org/elasticsearch/index/mapper/MapperRegistry.java index 4b80ccc8b70bd..7b37847638d0b 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/MapperRegistry.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/MapperRegistry.java @@ -43,7 +43,7 @@ public MapperRegistry( // MP TODO: remove this no-op RootObjectMapperNamespaceValidator once we know how all this is going to work this(mapperParsers, runtimeFieldParsers, metadataMapperParsers, fieldFilter, new RootObjectMapperNamespaceValidator() { @Override - public void validateNamespace(ObjectMapper.Subobjects subobjects, Mapper mapper) {} + public void validateNamespace(ObjectMapper.Subobjects subobjects, String name) {} }); } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java index 75dc2fb88aec1..dda603795a68d 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java @@ -543,12 +543,7 @@ private static boolean processField( Map fields = RuntimeField.parseRuntimeFields((Map) fieldNode, parserContext, true); if (namespaceValidator != null) { for (String runtimeField : fields.keySet()) { - if ("_project".equals(runtimeField) || runtimeField.startsWith("_project.")) { - throw new IllegalArgumentException( - "Runtime mapping rejected. No mappings of [_project] are allowed in order " - + "to avoid conflicts with project metadata tags in serverless." - ); - } + namespaceValidator.validateNamespace(null, runtimeField); } } builder.addRuntimeFields(fields); @@ -568,7 +563,7 @@ public int getTotalFieldsCount() { @Override protected void validateSubField(Mapper mapper, MappingLookup mappers) { if (namespaceValidator != null) { - namespaceValidator.validateNamespace(subobjects(), mapper); + namespaceValidator.validateNamespace(subobjects(), mapper.leafName()); } super.validateSubField(mapper, mappers); } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapperNamespaceValidator.java b/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapperNamespaceValidator.java index 79167edc56b7e..33b462cdf71e0 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapperNamespaceValidator.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapperNamespaceValidator.java @@ -17,5 +17,5 @@ public interface RootObjectMapperNamespaceValidator { /** * If the namespace in the mapper is not allowed, an Exception should be thrown. */ - void validateNamespace(ObjectMapper.Subobjects subobjects, Mapper mapper); + void validateNamespace(ObjectMapper.Subobjects subobjects, String name); } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/ServerlessRootObjectMapperNamespaceValidator.java b/server/src/main/java/org/elasticsearch/index/mapper/ServerlessRootObjectMapperNamespaceValidator.java index fd06bb38621dd..8237259465f97 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/ServerlessRootObjectMapperNamespaceValidator.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/ServerlessRootObjectMapperNamespaceValidator.java @@ -10,6 +10,7 @@ package org.elasticsearch.index.mapper; import org.elasticsearch.common.Strings; +import org.elasticsearch.core.Nullable; /** * In serverless, prohibits mappings that start with _project, including subfields such as _project.foo. @@ -19,24 +20,14 @@ public class ServerlessRootObjectMapperNamespaceValidator implements RootObjectMapperNamespaceValidator { private static final String SERVERLESS_RESERVED_NAMESPACE = "_project"; - // MP TODO: we can also pass in a MappingLookup - would that help here? @Override - public void validateNamespace(ObjectMapper.Subobjects subobjects, Mapper mapper) { // TODO: stop passing in Mapper and pass in String - // fieldName - if (mapper.leafName().equals(SERVERLESS_RESERVED_NAMESPACE)) { - System.err.println("XX: 1A: " + mapper.leafName()); - System.err.println("XX: 1B: " + mapper.fullPath()); - System.err.println("XX: 1C: " + mapper.typeName()); - System.err.println("XX: 1D: " + mapper); + public void validateNamespace(@Nullable ObjectMapper.Subobjects subobjects, String name) { + if (name.equals(SERVERLESS_RESERVED_NAMESPACE)) { throw new IllegalArgumentException(generateErrorMessage()); } else if (subobjects != ObjectMapper.Subobjects.ENABLED) { - System.err.println("YYY: 2A: " + mapper.leafName()); - System.err.println("YYY: 2B: " + mapper.fullPath()); - System.err.println("YYY: 2C: " + mapper.typeName()); - System.err.println("YYY: 2D: " + mapper); - // leafName here will be something like _project.myfield, rather than just _project - if (mapper.leafName().startsWith(SERVERLESS_RESERVED_NAMESPACE + ".")) { - throw new IllegalArgumentException(generateErrorMessage(mapper.leafName())); + // name here will be something like _project.my_field, rather than just _project + if (name.startsWith(SERVERLESS_RESERVED_NAMESPACE + ".")) { + throw new IllegalArgumentException(generateErrorMessage(name)); } } } From 71f2156a4ddb2b6a3a81d112def032ed37585e39 Mon Sep 17 00:00:00 2001 From: Michael Peterson Date: Wed, 20 Aug 2025 13:01:59 -0400 Subject: [PATCH 05/15] Added namespaceValidator check to IndexService.parseRuntimeMappings This prevents _project or _project.foo mappings in query-time runtime mappings. For example, these now fail with "Mapping rejected: [_project.foo]. No mappings of [_project] are allowed in order to avoid conflicts with project metadata tags in serverless" GET /blogs/_search { "runtime_mappings": { "_project": { "type": "keyword", "script": { "source": "emit('somevalue')" } } }, "query": { "match_all": {} }, "fields": ["_project"] } GET /blogs/_search { "runtime_mappings": { "_project.foo": { "type": "keyword", "script": { "source": "emit('somevalue')" } } }, "query": { "match_all": {} }, "fields": ["_project.foo"] } --- .../src/main/java/org/elasticsearch/index/IndexService.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/src/main/java/org/elasticsearch/index/IndexService.java b/server/src/main/java/org/elasticsearch/index/IndexService.java index 2ab766a1253a7..589bdfab79e83 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexService.java +++ b/server/src/main/java/org/elasticsearch/index/IndexService.java @@ -1409,6 +1409,11 @@ public static Map parseRuntimeMappings( // TODO add specific tests to SearchExecutionTests similar to the ones in FieldTypeLookupTests MappingParserContext parserContext = mapperService.parserContext(); Map runtimeFields = RuntimeField.parseRuntimeFields(new HashMap<>(runtimeMappings), parserContext, false); + if (parserContext.getNamespaceValidator() != null) { + for (String runtimeFieldName : runtimeFields.keySet()) { + parserContext.getNamespaceValidator().validateNamespace(null, runtimeFieldName); + } + } Map runtimeFieldTypes = RuntimeField.collectFieldTypes(runtimeFields.values()); if (false == indexSettings.getIndexMetadata().getRoutingPaths().isEmpty()) { for (String r : runtimeMappings.keySet()) { From 53ac90fc6fd93e5208453f1f354122f8109a42e3 Mon Sep 17 00:00:00 2001 From: Michael Peterson Date: Thu, 21 Aug 2025 14:48:00 -0400 Subject: [PATCH 06/15] Changed runtime field validation to be done in one place: RuntimeField.parseRuntimeFields. Removed checks from RootObjectMapper and IndexService. This checks both query-time and index-time runtime field mappings. --- .../java/org/elasticsearch/index/IndexService.java | 5 ----- .../elasticsearch/index/mapper/RootObjectMapper.java | 10 ++-------- .../org/elasticsearch/index/mapper/RuntimeField.java | 3 +++ 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/IndexService.java b/server/src/main/java/org/elasticsearch/index/IndexService.java index 589bdfab79e83..2ab766a1253a7 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexService.java +++ b/server/src/main/java/org/elasticsearch/index/IndexService.java @@ -1409,11 +1409,6 @@ public static Map parseRuntimeMappings( // TODO add specific tests to SearchExecutionTests similar to the ones in FieldTypeLookupTests MappingParserContext parserContext = mapperService.parserContext(); Map runtimeFields = RuntimeField.parseRuntimeFields(new HashMap<>(runtimeMappings), parserContext, false); - if (parserContext.getNamespaceValidator() != null) { - for (String runtimeFieldName : runtimeFields.keySet()) { - parserContext.getNamespaceValidator().validateNamespace(null, runtimeFieldName); - } - } Map runtimeFieldTypes = RuntimeField.collectFieldTypes(runtimeFields.values()); if (false == indexSettings.getIndexMetadata().getRoutingPaths().isEmpty()) { for (String r : runtimeMappings.keySet()) { diff --git a/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java index dda603795a68d..0236222deeaab 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java @@ -470,7 +470,7 @@ public static RootObjectMapper.Builder parse(String name, Map no String fieldName = entry.getKey(); Object fieldNode = entry.getValue(); if (parseObjectOrDocumentTypeProperties(fieldName, fieldNode, parserContext, builder) - || processField(builder, fieldName, fieldNode, parserContext, parserContext.getNamespaceValidator())) { + || processField(builder, fieldName, fieldNode, parserContext)) { iterator.remove(); } } @@ -482,8 +482,7 @@ private static boolean processField( RootObjectMapper.Builder builder, String fieldName, Object fieldNode, - MappingParserContext parserContext, - RootObjectMapperNamespaceValidator namespaceValidator + MappingParserContext parserContext ) { if (fieldName.equals("date_formats") || fieldName.equals("dynamic_date_formats")) { if (fieldNode instanceof List) { @@ -541,11 +540,6 @@ private static boolean processField( } else if (fieldName.equals("runtime")) { if (fieldNode instanceof Map) { Map fields = RuntimeField.parseRuntimeFields((Map) fieldNode, parserContext, true); - if (namespaceValidator != null) { - for (String runtimeField : fields.keySet()) { - namespaceValidator.validateNamespace(null, runtimeField); - } - } builder.addRuntimeFields(fields); return true; } else { diff --git a/server/src/main/java/org/elasticsearch/index/mapper/RuntimeField.java b/server/src/main/java/org/elasticsearch/index/mapper/RuntimeField.java index d7fbb8739d1f9..1e47948bff6e6 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/RuntimeField.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/RuntimeField.java @@ -189,6 +189,9 @@ static Map 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); From 4eb29e28445c0ebb2760648a29e3307802626080 Mon Sep 17 00:00:00 2001 From: Michael Peterson Date: Thu, 21 Aug 2025 15:02:47 -0400 Subject: [PATCH 07/15] Removed redundant call to mapper.validate(mappers) in ObjectMapper.validate --- .../java/org/elasticsearch/index/mapper/ObjectMapper.java | 1 - .../mapper/ServerlessRootObjectMapperNamespaceValidator.java | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java index a00878ff072e8..40ab05b55eb22 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java @@ -550,7 +550,6 @@ public final Optional sourceKeepMode() { @Override public final void validate(MappingLookup mappers) { for (Mapper mapper : this.mappers.values()) { - mapper.validate(mappers); validateSubField(mapper, mappers); } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/ServerlessRootObjectMapperNamespaceValidator.java b/server/src/main/java/org/elasticsearch/index/mapper/ServerlessRootObjectMapperNamespaceValidator.java index 8237259465f97..89ccb38e07b17 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/ServerlessRootObjectMapperNamespaceValidator.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/ServerlessRootObjectMapperNamespaceValidator.java @@ -20,6 +20,11 @@ public class ServerlessRootObjectMapperNamespaceValidator implements RootObjectMapperNamespaceValidator { private static final String SERVERLESS_RESERVED_NAMESPACE = "_project"; + /** + * + * @param subobjects if null, it will be interpreted as subobjects != ObjectMapper.Subobjects.ENABLED + * @param name + */ @Override public void validateNamespace(@Nullable ObjectMapper.Subobjects subobjects, String name) { if (name.equals(SERVERLESS_RESERVED_NAMESPACE)) { From 020f7cafeb9e0f75fbd56d2571dfc804b21ae2f4 Mon Sep 17 00:00:00 2001 From: Michael Peterson Date: Mon, 25 Aug 2025 15:32:26 -0400 Subject: [PATCH 08/15] Initial set of tests for index time mappings - both standard and runtime; no query time runtime mapping tests --- ...essRootObjectMapperNamespaceValidator.java | 5 +- .../index/mapper/RootObjectMapperTests.java | 245 ++++++++++++++++++ .../index/mapper/MapperServiceTestCase.java | 23 +- 3 files changed, 270 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/mapper/ServerlessRootObjectMapperNamespaceValidator.java b/server/src/main/java/org/elasticsearch/index/mapper/ServerlessRootObjectMapperNamespaceValidator.java index 89ccb38e07b17..8bd10c322387c 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/ServerlessRootObjectMapperNamespaceValidator.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/ServerlessRootObjectMapperNamespaceValidator.java @@ -21,9 +21,10 @@ public class ServerlessRootObjectMapperNamespaceValidator implements RootObjectM private static final String SERVERLESS_RESERVED_NAMESPACE = "_project"; /** - * + * Throws an error if a top level field with {@code SERVERLESS_RESERVED_NAMESPACE} is found. + * If subobjects = false, then it also checks for field names starting with "_project." * @param subobjects if null, it will be interpreted as subobjects != ObjectMapper.Subobjects.ENABLED - * @param name + * @param name field name to evaluation */ @Override public void validateNamespace(@Nullable ObjectMapper.Subobjects subobjects, String name) { diff --git a/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java index f20eaeb5037ba..f20210fe86522 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java @@ -9,6 +9,7 @@ package org.elasticsearch.index.mapper; +import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.common.Strings; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.core.CheckedConsumer; @@ -370,6 +371,250 @@ public void testEmptyType() throws Exception { assertThat(e.getMessage(), containsString("type cannot be an empty string")); } + public void testWithRootObjectMapperNamespaceValidator() throws Exception { + final String errorMessage = "error 1234"; + + RootObjectMapperNamespaceValidator validatorx = new RootObjectMapperNamespaceValidator() { + @Override + public void validateNamespace(ObjectMapper.Subobjects subobjects, String name) { + System.err.println(">>> XXX subobjects: " + subobjects.toString()); + String disallowed = "_project"; + if (name.equals(disallowed)) { + throw new IllegalArgumentException(errorMessage); + } else if (subobjects != ObjectMapper.Subobjects.ENABLED) { + System.err.println(">>> YYYYYYYYYYYYYYYYY subobjects: " + subobjects.toString()); + // name here will be something like _project.my_field, rather than just _project + if (name.startsWith(disallowed + ".")) { + throw new IllegalArgumentException(errorMessage); + } + } + } + }; + + String notNested = """ + { + "_doc": { + "properties": { + "": { + "type": "" + } + } + } + }"""; + + // _project should fail, regardless of type + { + String json = notNested.replace("", "_project"); + + String keyword = json.replace("", "keyword"); + Exception e = expectThrows(IllegalArgumentException.class, () -> createMapperServiceWithNamespaceValidator(keyword, validator)); + assertThat(e.getMessage(), equalTo(errorMessage)); + + String text = json.replace("", "text"); + e = expectThrows(IllegalArgumentException.class, () -> createMapperServiceWithNamespaceValidator(text, validator)); + assertThat(e.getMessage(), equalTo(errorMessage)); + + String object = json.replace("", "object"); + e = expectThrows(IllegalArgumentException.class, () -> createMapperServiceWithNamespaceValidator(object, validator)); + assertThat(e.getMessage(), equalTo(errorMessage)); + } + + // _project.subfield should fail + { + String json = notNested.replace("", "_project.subfield").replace("", randomFrom("text", "keyword", "object")); + Exception e = expectThrows(IllegalArgumentException.class, () -> createMapperServiceWithNamespaceValidator(json, validator)); + assertThat(e.getMessage(), equalTo(errorMessage)); + } + + // _projectx should pass + { + String json = notNested.replace("", "_projectx").replace("", randomFrom("text", "keyword", "object")); + MapperService mapperService = createMapperServiceWithNamespaceValidator(json, validator); + assertNotNull(mapperService); + } + + // _project_subfield should pass + { + String json = notNested.replace("", "_project_subfield"); + json = json.replace("", randomFrom("text", "keyword", "object")); + MapperService mapperService = createMapperServiceWithNamespaceValidator(json, validator); + assertNotNull(mapperService); + } + + // _projectx.subfield should pass + { + String json = notNested.replace("", "_projectx.subfield"); + json = json.replace("", randomFrom("text", "keyword", "object")); + MapperService mapperService = createMapperServiceWithNamespaceValidator(json, validator); + assertNotNull(mapperService); + } + + String nested = """ + { + "_doc": { + "properties": { + "": { + "type": "object", + "properties": { + "": { + "type": "keyword" + } + } + } + } + } + }"""; + + // TODO: this also works - what is the difference? + // String nested = """ + // { + // "mappings": { + // "properties": { + // "": { + // "type": "object", + // "properties": { + // "": { + // "type": "keyword" + // } + // } + // } + // } + // } + // }"""; + + // nested _project { my_field } should fail + { + String json = nested.replace("", "_project").replace("", "my_field"); + Exception e = expectThrows(IllegalArgumentException.class, () -> createMapperServiceWithNamespaceValidator(json, validator)); + assertThat(e.getMessage(), equalTo(errorMessage)); + } + + // nested my_field { _project } should succeed + { + String json = nested.replace("", "my_field").replace("", "_project"); + MapperService mapperService = createMapperServiceWithNamespaceValidator(json, validator); + assertNotNull(mapperService); + } + + // nested _projectx { _project } should succeed + { + String json = nested.replace("", "_projectx").replace("", "_project"); + MapperService mapperService = createMapperServiceWithNamespaceValidator(json, validator); + assertNotNull(mapperService); + } + + // TODO: I'm not allowed to change the subobjects setting for some reason + // test with subobjects setting + String withSubobjects = """ + { + "_doc": { + "subobjects": "", + "properties": { + "": { + "type": "object", + "properties": { + "my_field": { + "type": "keyword" + } + } + } + } + } + }"""; + + // String withSubobjects = """ + // { + // "mappings": { + // "subobjects": "", + // "properties": { + // "": { + // "type": "object", + // "properties": { + // "my_field": { + // "type": "keyword" + // } + // } + // } + // } + // } + // }"""; + // + // { + // String json = withSubobjects.replace("", "false")//randomFrom("true", "false", "auto")) + // .replace("", "abc"); + // MapperService mapperService = createMapperServiceWithNamespaceValidator(json, validator); + // assertNotNull(mapperService); + // } + + // { + // String json = withSubobjects.replace("", "false") + // .replace("", "_project"); + // // TODO: fails with org.elasticsearch.index.mapper.MapperException: the [subobjects] parameter can't be updated for the object + // mapping [_doc] + // MapperService mapperService = createMapperServiceWithNamespaceValidator(json, validator); + // assertNotNull(mapperService); + // } + } + + public void testRuntimeFieldInMappingWithNamespaceValidator() throws IOException { + final String errorMessage = "error 1234"; + + RootObjectMapperNamespaceValidator validator = new RootObjectMapperNamespaceValidator() { + @Override + public void validateNamespace(ObjectMapper.Subobjects subobjects, String name) { + System.err.println(">>> XXX subobjects: " + subobjects); + String disallowed = "_project"; + if (name.equals(disallowed)) { + throw new IllegalArgumentException(errorMessage); + } else if (subobjects != ObjectMapper.Subobjects.ENABLED) { + System.err.println(">>> YYYYYYYYYYYYYYYYY subobjects: " + subobjects); + // name here will be something like _project.my_field, rather than just _project + if (name.startsWith(disallowed + ".")) { + throw new IllegalArgumentException(errorMessage); + } + } + } + }; + + // ensure that things close to the disallowed fields that are allowed + { + String mapping = Strings.toString(runtimeMapping(builder -> { + builder.startObject("_project_x").field("type", "ip").endObject(); + builder.startObject("_projectx").field("type", "date").endObject(); + builder.startObject("field1._project").field("type", "double").endObject(); + })); + MapperService mapperService = createMapperServiceWithNamespaceValidator(mapping, validator); + assertEquals(mapping, mapperService.documentMapper().mappingSource().toString()); + assertEquals(3, mapperService.documentMapper().mapping().getRoot().getTotalFieldsCount()); + } + + // _project is rejected + { + String mapping = Strings.toString(runtimeMapping(builder -> { + builder.startObject("field1").field("type", "double").endObject(); + builder.startObject("_project").field("type", "date").endObject(); + builder.startObject("field3").field("type", "ip").endObject(); + })); + Exception e = expectThrows(MapperParsingException.class, () -> createMapperServiceWithNamespaceValidator(mapping, validator)); + Throwable cause = ExceptionsHelper.unwrap(e, IllegalArgumentException.class); + assertNotNull(cause); + assertThat(cause.getMessage(), equalTo(errorMessage)); + } + + // _project.my_sub_field is rejected + { + String mapping = Strings.toString(runtimeMapping(builder -> { + builder.startObject("field1").field("type", "double").endObject(); + builder.startObject("_project.my_sub_field").field("type", "keyword").endObject(); + builder.startObject("field3").field("type", "ip").endObject(); + })); + Exception e = expectThrows(MapperParsingException.class, () -> createMapperServiceWithNamespaceValidator(mapping, validator)); + Throwable cause = ExceptionsHelper.unwrap(e, IllegalArgumentException.class); + assertNotNull(cause); + assertThat(cause.getMessage(), equalTo(errorMessage)); + } + } + public void testSyntheticSourceKeepAllThrows() throws IOException { String mapping = Strings.toString( XContentFactory.jsonBuilder() diff --git a/test/framework/src/main/java/org/elasticsearch/index/mapper/MapperServiceTestCase.java b/test/framework/src/main/java/org/elasticsearch/index/mapper/MapperServiceTestCase.java index 45370ea6358fa..7f1dfa63c48f2 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/mapper/MapperServiceTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/index/mapper/MapperServiceTestCase.java @@ -234,6 +234,20 @@ protected final MapperService createMapperService(IndexVersion version, Settings return new TestMapperServiceBuilder().indexVersion(version).settings(settings).idFieldDataEnabled(idFieldDataEnabled).build(); } + public MapperService createMapperServiceWithNamespaceValidator(String mappings, RootObjectMapperNamespaceValidator validatorx) + throws IOException { + MapperService mapperService = new TestMapperServiceBuilder().indexVersion(getVersion()) + .settings(getIndexSettings()) + .idFieldDataEnabled(() -> true) + .namespaceValidator(validator) + .build(); + + // TODO: is this step necessary? + mapperService = withMapping(mapperService, mapping(b -> {})); + merge(mapperService, mappings); + return mapperService; + } + protected final MapperService withMapping(MapperService mapperService, XContentBuilder mapping) throws IOException { merge(mapperService, mapping); return mapperService; @@ -246,6 +260,7 @@ protected class TestMapperServiceBuilder { private ScriptCompiler scriptCompiler; private MapperMetrics mapperMetrics; private boolean applyDefaultMapping; + private RootObjectMapperNamespaceValidator namespaceValidator; public TestMapperServiceBuilder() { indexVersion = getVersion(); @@ -281,11 +296,17 @@ public TestMapperServiceBuilder applyDefaultMapping(boolean applyDefaultMapping) return this; } + public TestMapperServiceBuilder namespaceValidator(RootObjectMapperNamespaceValidator validator) { + this.namespaceValidator = validator; + return this; + } + public MapperService build() { IndexSettings indexSettings = createIndexSettings(indexVersion, settings); SimilarityService similarityService = new SimilarityService(indexSettings, null, Map.of()); MapperRegistry mapperRegistry = new IndicesModule( - getPlugins().stream().filter(p -> p instanceof MapperPlugin).map(p -> (MapperPlugin) p).collect(toList()) + getPlugins().stream().filter(p -> p instanceof MapperPlugin).map(p -> (MapperPlugin) p).collect(toList()), + namespaceValidator ).getMapperRegistry(); BitsetFilterCache bitsetFilterCache = new BitsetFilterCache(indexSettings, BitsetFilterCache.Listener.NOOP); From 0cd6b977cf8b61874a1c5b43abc1f9ed813dfd5c Mon Sep 17 00:00:00 2001 From: Michael Peterson Date: Tue, 26 Aug 2025 09:37:02 -0400 Subject: [PATCH 09/15] Added query-time runtime field tests with RootObjectMapperNamespaceValidator --- .../index/mapper/RootObjectMapperTests.java | 2 +- .../query/SearchExecutionContextTests.java | 109 +++++++++++++++++- .../index/mapper/MapperServiceTestCase.java | 2 +- 3 files changed, 108 insertions(+), 5 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java index f20210fe86522..3bf77a1238333 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java @@ -374,7 +374,7 @@ public void testEmptyType() throws Exception { public void testWithRootObjectMapperNamespaceValidator() throws Exception { final String errorMessage = "error 1234"; - RootObjectMapperNamespaceValidator validatorx = new RootObjectMapperNamespaceValidator() { + RootObjectMapperNamespaceValidator validator = new RootObjectMapperNamespaceValidator() { @Override public void validateNamespace(ObjectMapper.Subobjects subobjects, String name) { System.err.println(">>> XXX subobjects: " + subobjects.toString()); diff --git a/server/src/test/java/org/elasticsearch/index/query/SearchExecutionContextTests.java b/server/src/test/java/org/elasticsearch/index/query/SearchExecutionContextTests.java index 0c31ab703862f..ff73c64a788ac 100644 --- a/server/src/test/java/org/elasticsearch/index/query/SearchExecutionContextTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/SearchExecutionContextTests.java @@ -61,6 +61,7 @@ import org.elasticsearch.index.mapper.NumberFieldMapper; import org.elasticsearch.index.mapper.ObjectMapper; import org.elasticsearch.index.mapper.RootObjectMapper; +import org.elasticsearch.index.mapper.RootObjectMapperNamespaceValidator; import org.elasticsearch.index.mapper.RuntimeField; import org.elasticsearch.index.mapper.SourceFieldMapper; import org.elasticsearch.index.mapper.TestRuntimeField; @@ -331,6 +332,89 @@ public void testSearchRequestRuntimeFields() { assertThat(context.getMatchingFieldNames("*"), equalTo(Set.of("cat", "dog", "pig", "runtime"))); } + public void testSearchRequestRuntimeFieldsWithNamespaceValidator() { + final String errorMessage = "error 12345"; + final String disallowed = "_project"; + + RootObjectMapperNamespaceValidator validator = new RootObjectMapperNamespaceValidator() { + @Override + public void validateNamespace(ObjectMapper.Subobjects subobjects, String name) { + if (name.equals(disallowed)) { + throw new IllegalArgumentException(errorMessage); + } else if (subobjects != ObjectMapper.Subobjects.ENABLED) { + // name here will be something like _project.my_field, rather than just _project + if (name.startsWith(disallowed + ".")) { + throw new IllegalArgumentException(errorMessage); + } + } + } + }; + + { + Map runtimeMappings = Map.ofEntries( + Map.entry(disallowed, Map.of("type", randomFrom("keyword", "long"))), + Map.entry("dog", Map.of("type", "long")) + ); + + Exception e = expectThrows( + IllegalArgumentException.class, + () -> createSearchExecutionContext( + "uuid", + null, + createMappingLookup( + List.of(new MockFieldMapper.FakeFieldType("pig"), new MockFieldMapper.FakeFieldType("cat")), + List.of(new TestRuntimeField("runtime", "long")) + ), + runtimeMappings, + validator + ) + ); + assertThat(e.getMessage(), equalTo(errorMessage)); + } + + { + Map runtimeMappings = Map.ofEntries( + Map.entry(disallowed + ".subfield", Map.of("type", randomFrom("keyword", "long"))), + Map.entry("dog", Map.of("type", "long")) + ); + + Exception e = expectThrows( + IllegalArgumentException.class, + () -> createSearchExecutionContext( + "uuid", + null, + createMappingLookup( + List.of(new MockFieldMapper.FakeFieldType("pig"), new MockFieldMapper.FakeFieldType("cat")), + List.of(new TestRuntimeField("runtime", "long")) + ), + runtimeMappings, + validator + ) + ); + assertThat(e.getMessage(), equalTo(errorMessage)); + } + + // _projectx should be allowed + { + Map runtimeMappings = Map.ofEntries( + Map.entry(disallowed + "x", Map.of("type", "keyword")), + Map.entry("dog", Map.of("type", "long")) + ); + + SearchExecutionContext searchExecutionContext = createSearchExecutionContext( + "uuid", + null, + createMappingLookup( + List.of(new MockFieldMapper.FakeFieldType("pig"), new MockFieldMapper.FakeFieldType("cat")), + List.of(new TestRuntimeField("runtime", "long")) + ), + runtimeMappings, + validator + ); + assertNotNull(searchExecutionContext); + } + } + public void testSearchRequestRuntimeFieldsWrongFormat() { Map runtimeMappings = new HashMap<>(); runtimeMappings.put("field", Arrays.asList("test1", "test2")); @@ -500,12 +584,22 @@ private static SearchExecutionContext createSearchExecutionContext( String clusterAlias, MappingLookup mappingLookup, Map runtimeMappings + ) { + return createSearchExecutionContext(indexUuid, clusterAlias, mappingLookup, runtimeMappings, null); + } + + private static SearchExecutionContext createSearchExecutionContext( + String indexUuid, + String clusterAlias, + MappingLookup mappingLookup, + Map runtimeMappings, + RootObjectMapperNamespaceValidator namespaceValidator ) { IndexMetadata.Builder indexMetadataBuilder = new IndexMetadata.Builder("index"); indexMetadataBuilder.settings(indexSettings(IndexVersion.current(), 1, 1).put(IndexMetadata.SETTING_INDEX_UUID, indexUuid)); IndexMetadata indexMetadata = indexMetadataBuilder.build(); IndexSettings indexSettings = new IndexSettings(indexMetadata, Settings.EMPTY); - MapperService mapperService = createMapperService(indexSettings, mappingLookup); + MapperService mapperService = createMapperServiceWithNamespaceValidator(indexSettings, mappingLookup, namespaceValidator); final long nowInMillis = randomNonNegativeLong(); return new SearchExecutionContext( 0, @@ -532,8 +626,16 @@ private static SearchExecutionContext createSearchExecutionContext( } private static MapperService createMapperService(IndexSettings indexSettings, MappingLookup mappingLookup) { + return createMapperServiceWithNamespaceValidator(indexSettings, mappingLookup, null); + } + + private static MapperService createMapperServiceWithNamespaceValidator( + IndexSettings indexSettings, + MappingLookup mappingLookup, + RootObjectMapperNamespaceValidator namespaceValidator + ) { IndexAnalyzers indexAnalyzers = IndexAnalyzers.of(singletonMap("default", new NamedAnalyzer("default", AnalyzerScope.INDEX, null))); - IndicesModule indicesModule = new IndicesModule(Collections.emptyList()); + IndicesModule indicesModule = new IndicesModule(Collections.emptyList(), namespaceValidator); MapperRegistry mapperRegistry = indicesModule.getMapperRegistry(); Supplier searchExecutionContextSupplier = () -> { throw new UnsupportedOperationException(); }; MapperService mapperService = mock(MapperService.class); @@ -552,7 +654,8 @@ private static MapperService createMapperService(IndexSettings indexSettings, Ma indexSettings.getMode().buildIdFieldMapper(() -> true), query -> { throw new UnsupportedOperationException(); - } + }, + namespaceValidator ) ); when(mapperService.isMultiField(anyString())).then( diff --git a/test/framework/src/main/java/org/elasticsearch/index/mapper/MapperServiceTestCase.java b/test/framework/src/main/java/org/elasticsearch/index/mapper/MapperServiceTestCase.java index 7f1dfa63c48f2..5e844045c16a4 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/mapper/MapperServiceTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/index/mapper/MapperServiceTestCase.java @@ -234,7 +234,7 @@ protected final MapperService createMapperService(IndexVersion version, Settings return new TestMapperServiceBuilder().indexVersion(version).settings(settings).idFieldDataEnabled(idFieldDataEnabled).build(); } - public MapperService createMapperServiceWithNamespaceValidator(String mappings, RootObjectMapperNamespaceValidator validatorx) + public MapperService createMapperServiceWithNamespaceValidator(String mappings, RootObjectMapperNamespaceValidator validator) throws IOException { MapperService mapperService = new TestMapperServiceBuilder().indexVersion(getVersion()) .settings(getIndexSettings()) From 63c454968ac3749b187aa98be28d67cd07d0cd58 Mon Sep 17 00:00:00 2001 From: Michael Peterson Date: Tue, 26 Aug 2025 11:13:22 -0400 Subject: [PATCH 10/15] Initial cleanup for PR review --- .../mapper/DefaultRootObjectMapperNamespaceValidator.java | 6 ------ .../org/elasticsearch/index/mapper/MapperRegistry.java | 8 +------- .../java/org/elasticsearch/indices/IndicesModule.java | 1 - .../index/query/SearchExecutionContextTests.java | 4 ---- 4 files changed, 1 insertion(+), 18 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/mapper/DefaultRootObjectMapperNamespaceValidator.java b/server/src/main/java/org/elasticsearch/index/mapper/DefaultRootObjectMapperNamespaceValidator.java index b76d22db1fc43..ad7b356e48f3e 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/DefaultRootObjectMapperNamespaceValidator.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/DefaultRootObjectMapperNamespaceValidator.java @@ -15,10 +15,4 @@ public class DefaultRootObjectMapperNamespaceValidator implements RootObjectMapperNamespaceValidator { @Override public void validateNamespace(ObjectMapper.Subobjects subobjects, String name) {} - - // MP FIXME remove - @Override - public String toString() { - return "I'm the DefaultRootObjectMapperNamespaceValidator{}"; - } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MapperRegistry.java b/server/src/main/java/org/elasticsearch/index/mapper/MapperRegistry.java index 7b37847638d0b..03026655cdca4 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/MapperRegistry.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/MapperRegistry.java @@ -33,21 +33,15 @@ public final class MapperRegistry { private final Function fieldFilter; private final RootObjectMapperNamespaceValidator namespaceValidator; - // MP TODO: remove this? Or keep? public MapperRegistry( Map mapperParsers, Map runtimeFieldParsers, Map metadataMapperParsers, Function fieldFilter ) { - // MP TODO: remove this no-op RootObjectMapperNamespaceValidator once we know how all this is going to work - this(mapperParsers, runtimeFieldParsers, metadataMapperParsers, fieldFilter, new RootObjectMapperNamespaceValidator() { - @Override - public void validateNamespace(ObjectMapper.Subobjects subobjects, String name) {} - }); + this(mapperParsers, runtimeFieldParsers, metadataMapperParsers, fieldFilter, null); } - // MP TODO: need to move/create tests to use this public MapperRegistry( Map mapperParsers, Map runtimeFieldParsers, diff --git a/server/src/main/java/org/elasticsearch/indices/IndicesModule.java b/server/src/main/java/org/elasticsearch/indices/IndicesModule.java index 3dc3000fc9354..aff7c8f573138 100644 --- a/server/src/main/java/org/elasticsearch/indices/IndicesModule.java +++ b/server/src/main/java/org/elasticsearch/indices/IndicesModule.java @@ -121,7 +121,6 @@ public IndicesModule(List mapperPlugins, RootObjectMapperNamespace ); } - // MP TODO: remove this constructor once all tests have been updated public IndicesModule(List mapperPlugins) { this(mapperPlugins, null); } diff --git a/server/src/test/java/org/elasticsearch/index/query/SearchExecutionContextTests.java b/server/src/test/java/org/elasticsearch/index/query/SearchExecutionContextTests.java index ff73c64a788ac..cead4a4c45d16 100644 --- a/server/src/test/java/org/elasticsearch/index/query/SearchExecutionContextTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/SearchExecutionContextTests.java @@ -625,10 +625,6 @@ private static SearchExecutionContext createSearchExecutionContext( ); } - private static MapperService createMapperService(IndexSettings indexSettings, MappingLookup mappingLookup) { - return createMapperServiceWithNamespaceValidator(indexSettings, mappingLookup, null); - } - private static MapperService createMapperServiceWithNamespaceValidator( IndexSettings indexSettings, MappingLookup mappingLookup, From af4964fe1ee7638ff9bbcc020a830d67a77ee05a Mon Sep 17 00:00:00 2001 From: Michael Peterson Date: Tue, 26 Aug 2025 13:52:04 -0400 Subject: [PATCH 11/15] Cleanup for PR review - ready for next round review --- .../index/mapper/MappingParserContext.java | 7 ++-- .../RootObjectMapperNamespaceValidator.java | 8 +++-- .../elasticsearch/indices/IndicesModule.java | 16 --------- .../index/mapper/RootObjectMapperTests.java | 35 +++++++++---------- .../index/mapper/MapperServiceTestCase.java | 2 -- 5 files changed, 26 insertions(+), 42 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MappingParserContext.java b/server/src/main/java/org/elasticsearch/index/mapper/MappingParserContext.java index 07676324cbb3c..5e5488c5f9acd 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/MappingParserContext.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/MappingParserContext.java @@ -74,7 +74,6 @@ public MappingParserContext( this.namespaceValidator = namespaceValidator; } - // MP TODO: only used by tests, so remove this after tests are updated? public MappingParserContext( Function similarityLookupService, Function typeParsers, @@ -207,7 +206,8 @@ private static class MultiFieldParserContext extends MappingParserContext { in.indexAnalyzers, in.indexSettings, in.idFieldMapper, - in.bitSetProducer + in.bitSetProducer, + in.namespaceValidator ); } @@ -237,7 +237,8 @@ private static class DynamicTemplateParserContext extends MappingParserContext { in.indexAnalyzers, in.indexSettings, in.idFieldMapper, - in.bitSetProducer + in.bitSetProducer, + in.namespaceValidator ); this.dateFormatter = dateFormatter; } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapperNamespaceValidator.java b/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapperNamespaceValidator.java index 33b462cdf71e0..4c5ab9a7e9d1f 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapperNamespaceValidator.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapperNamespaceValidator.java @@ -9,13 +9,17 @@ package org.elasticsearch.index.mapper; +import org.elasticsearch.core.Nullable; + /** - * SPI to inject additional rules around namespaces that are prohibited + * 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(ObjectMapper.Subobjects subobjects, String name); + void validateNamespace(@Nullable ObjectMapper.Subobjects subobjects, String name); } diff --git a/server/src/main/java/org/elasticsearch/indices/IndicesModule.java b/server/src/main/java/org/elasticsearch/indices/IndicesModule.java index aff7c8f573138..98734e373ba17 100644 --- a/server/src/main/java/org/elasticsearch/indices/IndicesModule.java +++ b/server/src/main/java/org/elasticsearch/indices/IndicesModule.java @@ -96,28 +96,12 @@ public class IndicesModule extends AbstractModule { private final MapperRegistry mapperRegistry; public IndicesModule(List mapperPlugins, RootObjectMapperNamespaceValidator namespaceValidator) { - // this is the only place that the MapperRegistry is created this.mapperRegistry = new MapperRegistry( getMappers(mapperPlugins), getRuntimeFields(mapperPlugins), getMetadataMappers(mapperPlugins), getFieldFilter(mapperPlugins), namespaceValidator - // new RootObjectMapperNamespaceValidator() { - // @Override - // public void validateNamespace(ObjectMapper.Subobjects subobjects, Mapper mapper) { - // // TODO: in the future, this will be a no-op on stateful and loaded somehow dynamically in serverless - // if (subobjects == ObjectMapper.Subobjects.ENABLED) { - // if (mapper.leafName().equals(RESERVED_NAMESPACE)) { - // throw new IllegalArgumentException("xx reserved namespace: [" + RESERVED_NAMESPACE + ']'); - // } - // } else { - // if (mapper.leafName().startsWith(RESERVED_NAMESPACE)) { - // throw new IllegalArgumentException("xx reserved namespace: [" + RESERVED_NAMESPACE + ']'); - // } - // } - // } - // } ); } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java index 3bf77a1238333..0db275784a078 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java @@ -373,16 +373,14 @@ public void testEmptyType() throws Exception { public void testWithRootObjectMapperNamespaceValidator() throws Exception { final String errorMessage = "error 1234"; + final String disallowed = "_project"; RootObjectMapperNamespaceValidator validator = new RootObjectMapperNamespaceValidator() { @Override public void validateNamespace(ObjectMapper.Subobjects subobjects, String name) { - System.err.println(">>> XXX subobjects: " + subobjects.toString()); - String disallowed = "_project"; if (name.equals(disallowed)) { throw new IllegalArgumentException(errorMessage); } else if (subobjects != ObjectMapper.Subobjects.ENABLED) { - System.err.println(">>> YYYYYYYYYYYYYYYYY subobjects: " + subobjects.toString()); // name here will be something like _project.my_field, rather than just _project if (name.startsWith(disallowed + ".")) { throw new IllegalArgumentException(errorMessage); @@ -404,7 +402,7 @@ public void validateNamespace(ObjectMapper.Subobjects subobjects, String name) { // _project should fail, regardless of type { - String json = notNested.replace("", "_project"); + String json = notNested.replace("", disallowed); String keyword = json.replace("", "keyword"); Exception e = expectThrows(IllegalArgumentException.class, () -> createMapperServiceWithNamespaceValidator(keyword, validator)); @@ -421,21 +419,22 @@ public void validateNamespace(ObjectMapper.Subobjects subobjects, String name) { // _project.subfield should fail { - String json = notNested.replace("", "_project.subfield").replace("", randomFrom("text", "keyword", "object")); + String json = notNested.replace("", disallowed + ".subfield") + .replace("", randomFrom("text", "keyword", "object")); Exception e = expectThrows(IllegalArgumentException.class, () -> createMapperServiceWithNamespaceValidator(json, validator)); assertThat(e.getMessage(), equalTo(errorMessage)); } // _projectx should pass { - String json = notNested.replace("", "_projectx").replace("", randomFrom("text", "keyword", "object")); + String json = notNested.replace("", disallowed + "x").replace("", randomFrom("text", "keyword", "object")); MapperService mapperService = createMapperServiceWithNamespaceValidator(json, validator); assertNotNull(mapperService); } // _project_subfield should pass { - String json = notNested.replace("", "_project_subfield"); + String json = notNested.replace("", disallowed + "_subfield"); json = json.replace("", randomFrom("text", "keyword", "object")); MapperService mapperService = createMapperServiceWithNamespaceValidator(json, validator); assertNotNull(mapperService); @@ -443,7 +442,7 @@ public void validateNamespace(ObjectMapper.Subobjects subobjects, String name) { // _projectx.subfield should pass { - String json = notNested.replace("", "_projectx.subfield"); + String json = notNested.replace("", disallowed + "x.subfield"); json = json.replace("", randomFrom("text", "keyword", "object")); MapperService mapperService = createMapperServiceWithNamespaceValidator(json, validator); assertNotNull(mapperService); @@ -484,21 +483,21 @@ public void validateNamespace(ObjectMapper.Subobjects subobjects, String name) { // nested _project { my_field } should fail { - String json = nested.replace("", "_project").replace("", "my_field"); + String json = nested.replace("", disallowed).replace("", "my_field"); Exception e = expectThrows(IllegalArgumentException.class, () -> createMapperServiceWithNamespaceValidator(json, validator)); assertThat(e.getMessage(), equalTo(errorMessage)); } // nested my_field { _project } should succeed { - String json = nested.replace("", "my_field").replace("", "_project"); + String json = nested.replace("", "my_field").replace("", disallowed); MapperService mapperService = createMapperServiceWithNamespaceValidator(json, validator); assertNotNull(mapperService); } // nested _projectx { _project } should succeed { - String json = nested.replace("", "_projectx").replace("", "_project"); + String json = nested.replace("", disallowed + "x").replace("", disallowed); MapperService mapperService = createMapperServiceWithNamespaceValidator(json, validator); assertNotNull(mapperService); } @@ -558,16 +557,14 @@ public void validateNamespace(ObjectMapper.Subobjects subobjects, String name) { public void testRuntimeFieldInMappingWithNamespaceValidator() throws IOException { final String errorMessage = "error 1234"; + final String disallowed = "_project"; RootObjectMapperNamespaceValidator validator = new RootObjectMapperNamespaceValidator() { @Override public void validateNamespace(ObjectMapper.Subobjects subobjects, String name) { - System.err.println(">>> XXX subobjects: " + subobjects); - String disallowed = "_project"; if (name.equals(disallowed)) { throw new IllegalArgumentException(errorMessage); } else if (subobjects != ObjectMapper.Subobjects.ENABLED) { - System.err.println(">>> YYYYYYYYYYYYYYYYY subobjects: " + subobjects); // name here will be something like _project.my_field, rather than just _project if (name.startsWith(disallowed + ".")) { throw new IllegalArgumentException(errorMessage); @@ -579,9 +576,9 @@ public void validateNamespace(ObjectMapper.Subobjects subobjects, String name) { // ensure that things close to the disallowed fields that are allowed { String mapping = Strings.toString(runtimeMapping(builder -> { - builder.startObject("_project_x").field("type", "ip").endObject(); - builder.startObject("_projectx").field("type", "date").endObject(); - builder.startObject("field1._project").field("type", "double").endObject(); + builder.startObject(disallowed + "_x").field("type", "ip").endObject(); + builder.startObject(disallowed + "x").field("type", "date").endObject(); + builder.startObject("field1." + disallowed).field("type", "double").endObject(); })); MapperService mapperService = createMapperServiceWithNamespaceValidator(mapping, validator); assertEquals(mapping, mapperService.documentMapper().mappingSource().toString()); @@ -592,7 +589,7 @@ public void validateNamespace(ObjectMapper.Subobjects subobjects, String name) { { String mapping = Strings.toString(runtimeMapping(builder -> { builder.startObject("field1").field("type", "double").endObject(); - builder.startObject("_project").field("type", "date").endObject(); + builder.startObject(disallowed).field("type", "date").endObject(); builder.startObject("field3").field("type", "ip").endObject(); })); Exception e = expectThrows(MapperParsingException.class, () -> createMapperServiceWithNamespaceValidator(mapping, validator)); @@ -605,7 +602,7 @@ public void validateNamespace(ObjectMapper.Subobjects subobjects, String name) { { String mapping = Strings.toString(runtimeMapping(builder -> { builder.startObject("field1").field("type", "double").endObject(); - builder.startObject("_project.my_sub_field").field("type", "keyword").endObject(); + builder.startObject(disallowed + ".my_sub_field").field("type", "keyword").endObject(); builder.startObject("field3").field("type", "ip").endObject(); })); Exception e = expectThrows(MapperParsingException.class, () -> createMapperServiceWithNamespaceValidator(mapping, validator)); diff --git a/test/framework/src/main/java/org/elasticsearch/index/mapper/MapperServiceTestCase.java b/test/framework/src/main/java/org/elasticsearch/index/mapper/MapperServiceTestCase.java index 5e844045c16a4..62de134f3919e 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/mapper/MapperServiceTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/index/mapper/MapperServiceTestCase.java @@ -242,8 +242,6 @@ public MapperService createMapperServiceWithNamespaceValidator(String mappings, .namespaceValidator(validator) .build(); - // TODO: is this step necessary? - mapperService = withMapping(mapperService, mapping(b -> {})); merge(mapperService, mappings); return mapperService; } From ebaf9a204567a23f52b4f64923c7e1db91e0bb1f Mon Sep 17 00:00:00 2001 From: Michael Peterson Date: Thu, 28 Aug 2025 16:44:23 -0400 Subject: [PATCH 12/15] Got subobjects:false tests working in RootObjectMapperTests --- .../index/mapper/RootObjectMapper.java | 6 +- .../index/mapper/RootObjectMapperTests.java | 156 +++++++----------- 2 files changed, 66 insertions(+), 96 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java index 0236222deeaab..ed8700238a5b9 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java @@ -159,7 +159,7 @@ public RootObjectMapper build(MapperBuilderContext context) { this.dynamicDateTimeFormatters = dynamicDateTimeFormatters; this.dateDetection = dateDetection; this.numericDetection = numericDetection; - this.namespaceValidator = namespaceValidator; + 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 + "]" @@ -556,9 +556,7 @@ public int getTotalFieldsCount() { @Override protected void validateSubField(Mapper mapper, MappingLookup mappers) { - if (namespaceValidator != null) { - namespaceValidator.validateNamespace(subobjects(), mapper.leafName()); - } + namespaceValidator.validateNamespace(subobjects(), mapper.leafName()); super.validateSubField(mapper, mappers); } } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java index 0db275784a078..364a80b1ecbaf 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java @@ -372,22 +372,9 @@ public void testEmptyType() throws Exception { } public void testWithRootObjectMapperNamespaceValidator() throws Exception { - final String errorMessage = "error 1234"; - final String disallowed = "_project"; - - RootObjectMapperNamespaceValidator validator = new RootObjectMapperNamespaceValidator() { - @Override - public void validateNamespace(ObjectMapper.Subobjects subobjects, String name) { - if (name.equals(disallowed)) { - throw new IllegalArgumentException(errorMessage); - } else if (subobjects != ObjectMapper.Subobjects.ENABLED) { - // name here will be something like _project.my_field, rather than just _project - if (name.startsWith(disallowed + ".")) { - throw new IllegalArgumentException(errorMessage); - } - } - } - }; + String errorMessage = "error 1234"; + String disallowed = "_project"; + RootObjectMapperNamespaceValidator validator = new TestRootObjectMapperNamespaceValidator(disallowed, errorMessage); String notNested = """ { @@ -450,37 +437,20 @@ public void validateNamespace(ObjectMapper.Subobjects subobjects, String name) { String nested = """ { - "_doc": { + "_doc": { + "properties": { + "": { + "type": "object", "properties": { - "": { - "type": "object", - "properties": { - "": { - "type": "keyword" - } - } - } + "": { + "type": "keyword" + } } + } } + } }"""; - // TODO: this also works - what is the difference? - // String nested = """ - // { - // "mappings": { - // "properties": { - // "": { - // "type": "object", - // "properties": { - // "": { - // "type": "keyword" - // } - // } - // } - // } - // } - // }"""; - // nested _project { my_field } should fail { String json = nested.replace("", disallowed).replace("", "my_field"); @@ -501,8 +471,13 @@ public void validateNamespace(ObjectMapper.Subobjects subobjects, String name) { MapperService mapperService = createMapperServiceWithNamespaceValidator(json, validator); assertNotNull(mapperService); } + } + + public void testSubobjectsWithRootObjectMapperNamespaceValidator() throws Exception { + String errorMessage = "error 1234"; + String disallowed = "_project"; + RootObjectMapperNamespaceValidator validator = new TestRootObjectMapperNamespaceValidator(disallowed, errorMessage); - // TODO: I'm not allowed to change the subobjects setting for some reason // test with subobjects setting String withSubobjects = """ { @@ -520,58 +495,34 @@ public void validateNamespace(ObjectMapper.Subobjects subobjects, String name) { } } }"""; - - // String withSubobjects = """ - // { - // "mappings": { - // "subobjects": "", - // "properties": { - // "": { - // "type": "object", - // "properties": { - // "my_field": { - // "type": "keyword" - // } - // } - // } - // } - // } - // }"""; - // - // { - // String json = withSubobjects.replace("", "false")//randomFrom("true", "false", "auto")) - // .replace("", "abc"); - // MapperService mapperService = createMapperServiceWithNamespaceValidator(json, validator); - // assertNotNull(mapperService); - // } - - // { - // String json = withSubobjects.replace("", "false") - // .replace("", "_project"); - // // TODO: fails with org.elasticsearch.index.mapper.MapperException: the [subobjects] parameter can't be updated for the object - // mapping [_doc] - // MapperService mapperService = createMapperServiceWithNamespaceValidator(json, validator); - // assertNotNull(mapperService); - // } + { + String json = withSubobjects.replace("", "false").replace("", "_project"); + Exception e = expectThrows(IllegalArgumentException.class, () -> createMapperServiceWithNamespaceValidator(json, validator)); + assertThat(e.getMessage(), equalTo(errorMessage)); + } + { + String json = withSubobjects.replace("", randomFrom("true", "auto")).replace("", "_project"); + Exception e = expectThrows(IllegalArgumentException.class, () -> createMapperServiceWithNamespaceValidator(json, validator)); + assertThat(e.getMessage(), equalTo(errorMessage)); + } + { + String json = withSubobjects.replace("", randomFrom("false", "true", "auto")) + .replace("", "_project.foo"); + Exception e = expectThrows(IllegalArgumentException.class, () -> createMapperServiceWithNamespaceValidator(json, validator)); + assertThat(e.getMessage(), equalTo(errorMessage)); + } + { + String json = withSubobjects.replace("", randomFrom("false", "true", "auto")) + .replace("", "project.foo"); + MapperService mapperService = createMapperServiceWithNamespaceValidator(json, validator); + assertNotNull(mapperService); + } } public void testRuntimeFieldInMappingWithNamespaceValidator() throws IOException { - final String errorMessage = "error 1234"; - final String disallowed = "_project"; - - RootObjectMapperNamespaceValidator validator = new RootObjectMapperNamespaceValidator() { - @Override - public void validateNamespace(ObjectMapper.Subobjects subobjects, String name) { - if (name.equals(disallowed)) { - throw new IllegalArgumentException(errorMessage); - } else if (subobjects != ObjectMapper.Subobjects.ENABLED) { - // name here will be something like _project.my_field, rather than just _project - if (name.startsWith(disallowed + ".")) { - throw new IllegalArgumentException(errorMessage); - } - } - } - }; + String errorMessage = "error 1234"; + String disallowed = "_project"; + RootObjectMapperNamespaceValidator validator = new TestRootObjectMapperNamespaceValidator(disallowed, errorMessage); // ensure that things close to the disallowed fields that are allowed { @@ -672,4 +623,25 @@ private RootObjectMapper createRootObjectMapperWithAllParametersSet( return mapper.mapping().getRoot(); } + static class TestRootObjectMapperNamespaceValidator implements RootObjectMapperNamespaceValidator { + private final String disallowed; + private final String errorMessage; + + public TestRootObjectMapperNamespaceValidator(String disallowedNamespace, String errorMessage) { + this.disallowed = disallowedNamespace; + this.errorMessage = errorMessage; + } + + @Override + public void validateNamespace(ObjectMapper.Subobjects subobjects, String name) { + if (name.equals(disallowed)) { + throw new IllegalArgumentException(errorMessage); + } else if (subobjects != ObjectMapper.Subobjects.ENABLED) { + // name here will be something like _project.my_field, rather than just _project + if (name.startsWith(disallowed + ".")) { + throw new IllegalArgumentException(errorMessage); + } + } + } + } } From a8fcc03af0a62975f7a2716272545052aa613419 Mon Sep 17 00:00:00 2001 From: Michael Peterson Date: Thu, 28 Aug 2025 16:52:45 -0400 Subject: [PATCH 13/15] Removed ServerlessRootObjectMapperNamespaceValidator --- ...essRootObjectMapperNamespaceValidator.java | 52 ------------------- .../elasticsearch/node/NodeConstruction.java | 6 +-- 2 files changed, 2 insertions(+), 56 deletions(-) delete mode 100644 server/src/main/java/org/elasticsearch/index/mapper/ServerlessRootObjectMapperNamespaceValidator.java diff --git a/server/src/main/java/org/elasticsearch/index/mapper/ServerlessRootObjectMapperNamespaceValidator.java b/server/src/main/java/org/elasticsearch/index/mapper/ServerlessRootObjectMapperNamespaceValidator.java deleted file mode 100644 index 8bd10c322387c..0000000000000 --- a/server/src/main/java/org/elasticsearch/index/mapper/ServerlessRootObjectMapperNamespaceValidator.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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.common.Strings; -import org.elasticsearch.core.Nullable; - -/** - * In serverless, prohibits mappings that start with _project, including subfields such as _project.foo. - * The _project "namespace" in serverless is reserved in order to allow users to include project metadata tags - * (e.g., _project._region or _project._csp, etc.) which are useful in cross-project search and ES|QL queries. - */ -public class ServerlessRootObjectMapperNamespaceValidator implements RootObjectMapperNamespaceValidator { - private static final String SERVERLESS_RESERVED_NAMESPACE = "_project"; - - /** - * Throws an error if a top level field with {@code SERVERLESS_RESERVED_NAMESPACE} is found. - * If subobjects = false, then it also checks for field names starting with "_project." - * @param subobjects if null, it will be interpreted as subobjects != ObjectMapper.Subobjects.ENABLED - * @param name field name to evaluation - */ - @Override - public void validateNamespace(@Nullable ObjectMapper.Subobjects subobjects, String name) { - if (name.equals(SERVERLESS_RESERVED_NAMESPACE)) { - throw new IllegalArgumentException(generateErrorMessage()); - } else if (subobjects != ObjectMapper.Subobjects.ENABLED) { - // name here will be something like _project.my_field, rather than just _project - if (name.startsWith(SERVERLESS_RESERVED_NAMESPACE + ".")) { - throw new IllegalArgumentException(generateErrorMessage(name)); - } - } - } - - private String generateErrorMessage(String fieldName) { - return Strings.format( - "Mapping rejected%s. No mappings of [%s] are allowed in order to avoid conflicts with project metadata tags in serverless", - fieldName == null ? "" : ": [" + fieldName + "]", - SERVERLESS_RESERVED_NAMESPACE - ); - } - - private String generateErrorMessage() { - return generateErrorMessage(null); - } -} diff --git a/server/src/main/java/org/elasticsearch/node/NodeConstruction.java b/server/src/main/java/org/elasticsearch/node/NodeConstruction.java index fbca4ed5a23b2..0b75cca43d914 100644 --- a/server/src/main/java/org/elasticsearch/node/NodeConstruction.java +++ b/server/src/main/java/org/elasticsearch/node/NodeConstruction.java @@ -118,9 +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.ServerlessRootObjectMapperNamespaceValidator; import org.elasticsearch.index.mapper.SourceFieldMetrics; import org.elasticsearch.index.search.stats.ShardSearchPhaseAPMMetrics; import org.elasticsearch.index.shard.SearchOperationListener; @@ -700,11 +700,9 @@ private void construct( // serverless deployments plug-in the namespace validator that prohibits mappings with "_project" RootObjectMapperNamespaceValidator namespaceValidator = pluginsService.loadSingletonServiceProvider( RootObjectMapperNamespaceValidator.class, - // () -> new DefaultRootObjectMapperNamespaceValidator() - () -> new ServerlessRootObjectMapperNamespaceValidator() // MP FIXME - for this testing branch only + () -> new DefaultRootObjectMapperNamespaceValidator() ); modules.bindToInstance(RootObjectMapperNamespaceValidator.class, namespaceValidator); - logger.warn("XXX namespaceValidator loaded: " + namespaceValidator); // MP FIXME remove assert nodeEnvironment.nodeId() != null : "node ID must be set before constructing the Node"; TaskManager taskManager = new TaskManager( From 8665113d41657b8511161197cee514840cd3e947 Mon Sep 17 00:00:00 2001 From: Michael Peterson Date: Thu, 28 Aug 2025 19:54:30 -0400 Subject: [PATCH 14/15] Fixed checkstyle issue --- .../org/elasticsearch/index/mapper/RootObjectMapperTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java index 364a80b1ecbaf..5fe55b2212ec8 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/RootObjectMapperTests.java @@ -627,7 +627,7 @@ static class TestRootObjectMapperNamespaceValidator implements RootObjectMapperN private final String disallowed; private final String errorMessage; - public TestRootObjectMapperNamespaceValidator(String disallowedNamespace, String errorMessage) { + TestRootObjectMapperNamespaceValidator(String disallowedNamespace, String errorMessage) { this.disallowed = disallowedNamespace; this.errorMessage = errorMessage; } From ab5d0c2f500da3433b0de8c7b236e9de2f3edc7c Mon Sep 17 00:00:00 2001 From: Michael Peterson Date: Tue, 2 Sep 2025 10:07:42 -0400 Subject: [PATCH 15/15] Improved code comments --- .../java/org/elasticsearch/index/mapper/ObjectMapper.java | 5 +++++ .../org/elasticsearch/index/mapper/RootObjectMapper.java | 4 ++++ .../main/java/org/elasticsearch/node/NodeConstruction.java | 1 - 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java index 40ab05b55eb22..ec9910426cbc4 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java @@ -554,6 +554,11 @@ public final void validate(MappingLookup 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); } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java index ed8700238a5b9..9fa47e7ea2d13 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java @@ -554,6 +554,10 @@ 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()); diff --git a/server/src/main/java/org/elasticsearch/node/NodeConstruction.java b/server/src/main/java/org/elasticsearch/node/NodeConstruction.java index 0b75cca43d914..8920d3d49470c 100644 --- a/server/src/main/java/org/elasticsearch/node/NodeConstruction.java +++ b/server/src/main/java/org/elasticsearch/node/NodeConstruction.java @@ -697,7 +697,6 @@ private void construct( modules.bindToInstance(Tracer.class, telemetryProvider.getTracer()); - // serverless deployments plug-in the namespace validator that prohibits mappings with "_project" RootObjectMapperNamespaceValidator namespaceValidator = pluginsService.loadSingletonServiceProvider( RootObjectMapperNamespaceValidator.class, () -> new DefaultRootObjectMapperNamespaceValidator()