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/MapperRegistry.java b/server/src/main/java/org/elasticsearch/index/mapper/MapperRegistry.java index 44f7def74ec0e..651a2e8da47cf 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/MapperRegistry.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/MapperRegistry.java @@ -24,6 +24,8 @@ */ public final class MapperRegistry { + // there is one per node of MapperRegistery -. MapperSErvice can get the reserved namesapce from here + private final Map mapperParsers; private final Map runtimeFieldParsers; private final Map metadataMapperParsers; @@ -31,12 +33,27 @@ public final class MapperRegistry { private final Map metadataMapperParsers6x; private final Map metadataMapperParsers5x; private final Function fieldFilter; + private final RootObjectMapperNamespaceValidator namespaceValidator; + // MP TODO: remove this no-op RootObjectMapperNamespaceValidator once we know how all this is going to work public MapperRegistry( Map mapperParsers, Map runtimeFieldParsers, Map metadataMapperParsers, Function fieldFilter + ) { + this(mapperParsers, runtimeFieldParsers, metadataMapperParsers, fieldFilter, new RootObjectMapperNamespaceValidator() { + @Override + public void validateNamespace(ObjectMapper.Subobjects subobjects, Mapper mapper) {} + }); + } + + public MapperRegistry( + Map mapperParsers, + Map runtimeFieldParsers, + Map metadataMapperParsers, + Function fieldFilter, + RootObjectMapperNamespaceValidator namespaceValidator // MP TODO: new SPI for serverless ) { 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; } /** @@ -68,6 +86,10 @@ public Mapper.TypeParser getMapperParser(String type, IndexVersion indexVersionC } } + public RootObjectMapperNamespaceValidator getNamespaceValidator() { + return namespaceValidator; + } + public Map getRuntimeFieldParsers() { return runtimeFieldParsers; } 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..bf7693908d2b0 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/MappingParserContext.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/MappingParserContext.java @@ -42,6 +42,7 @@ public class MappingParserContext { private final IdFieldMapper idFieldMapper; private final Function bitSetProducer; private final long mappingObjectDepthLimit; + private final RootObjectMapperNamespaceValidator namespaceValidator; private long mappingObjectDepth = 0; public MappingParserContext( @@ -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/ObjectMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java index 3c71f6bf2db5b..1bce8bae53e58 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java @@ -547,12 +547,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..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(); @@ -540,4 +553,12 @@ private static boolean processField( public int getTotalFieldsCount() { return super.getTotalFieldsCount() - 1 + runtimeFields.size(); } + + @Override + protected void validateSubField(Mapper mapper, MappingLookup mappers) { + 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 new file mode 100644 index 0000000000000..852a112cc7bac --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapperNamespaceValidator.java @@ -0,0 +1,22 @@ +/* + * 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; + +/** + * 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/indices/IndicesModule.java b/server/src/main/java/org/elasticsearch/indices/IndicesModule.java index 09be98630d5c4..4f2bae91b1457 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,15 +95,22 @@ public class IndicesModule extends AbstractModule { private final MapperRegistry mapperRegistry; - 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) + getFieldFilter(mapperPlugins), + namespaceValidator ); } + // MP TODO: so 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 7b7d2b257c424..f73058b07e7ea 100644 --- a/server/src/main/java/org/elasticsearch/node/NodeConstruction.java +++ b/server/src/main/java/org/elasticsearch/node/NodeConstruction.java @@ -115,7 +115,9 @@ import org.elasticsearch.index.SlowLogFieldProvider; import org.elasticsearch.index.SlowLogFields; import org.elasticsearch.index.analysis.AnalysisRegistry; +import org.elasticsearch.index.mapper.DefaultRootObjectMapperNamespaceValidator; import org.elasticsearch.index.mapper.MapperMetrics; +import org.elasticsearch.index.mapper.RootObjectMapperNamespaceValidator; import org.elasticsearch.index.mapper.SourceFieldMetrics; import org.elasticsearch.index.search.stats.ShardSearchPhaseAPMMetrics; import org.elasticsearch.index.shard.SearchOperationListener; @@ -684,6 +686,15 @@ private void construct( () -> ProjectResolverFactory.DEFAULT ).create(); modules.bindToInstance(ProjectResolver.class, projectResolver); + + // serverless deployments plug-in the namespace validator that prohibits mappings with "_project" + RootObjectMapperNamespaceValidator namespaceValidator = pluginsService.loadSingletonServiceProvider( + RootObjectMapperNamespaceValidator.class, + () -> new DefaultRootObjectMapperNamespaceValidator() + ); + modules.bindToInstance(RootObjectMapperNamespaceValidator.class, namespaceValidator); + logger.warn("XXX namespaceValidator loaded: " + namespaceValidator); // MP FIXME remove + ClusterService clusterService = createClusterService(settingsModule, threadPool, taskManager); clusterService.addStateApplier(scriptService); @@ -772,7 +783,8 @@ private void construct( )::onNewInfo ); - IndicesModule indicesModule = new IndicesModule(pluginsService.filterPlugins(MapperPlugin.class).toList()); + // this is the sole place that the IndicesModule is created + IndicesModule indicesModule = new IndicesModule(pluginsService.filterPlugins(MapperPlugin.class).toList(), namespaceValidator); modules.add(indicesModule); modules.add(new GatewayModule());