Skip to content

RootObjectMapperNamespaceValidator SPI allows prohibiting mapping namespaces #127266

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,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{}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,36 @@
*/
public final class MapperRegistry {

// there is one per node of MapperRegistery -. MapperSErvice can get the reserved namesapce from here

private final Map<String, Mapper.TypeParser> mapperParsers;
private final Map<String, RuntimeField.Parser> runtimeFieldParsers;
private final Map<String, MetadataFieldMapper.TypeParser> metadataMapperParsers;
private final Map<String, MetadataFieldMapper.TypeParser> metadataMapperParsers7x;
private final Map<String, MetadataFieldMapper.TypeParser> metadataMapperParsers6x;
private final Map<String, MetadataFieldMapper.TypeParser> metadataMapperParsers5x;
private final Function<String, FieldPredicate> 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<String, Mapper.TypeParser> mapperParsers,
Map<String, RuntimeField.Parser> runtimeFieldParsers,
Map<String, MetadataFieldMapper.TypeParser> metadataMapperParsers,
Function<String, FieldPredicate> fieldFilter
) {
this(mapperParsers, runtimeFieldParsers, metadataMapperParsers, fieldFilter, new RootObjectMapperNamespaceValidator() {
@Override
public void validateNamespace(ObjectMapper.Subobjects subobjects, Mapper mapper) {}
});
}

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

/**
Expand All @@ -68,6 +86,10 @@ public Mapper.TypeParser getMapperParser(String type, IndexVersion indexVersionC
}
}

public RootObjectMapperNamespaceValidator getNamespaceValidator() {
return namespaceValidator;
}

public Map<String, RuntimeField.Parser> getRuntimeFieldParsers() {
return runtimeFieldParsers;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,8 @@ public MapperService(
indexAnalyzers,
indexSettings,
idFieldMapper,
bitSetProducer
bitSetProducer,
mapperRegistry.getNamespaceValidator()
);
this.documentParser = new DocumentParser(parserConfiguration, this.mappingParserContextSupplier.get());
Map<String, MetadataFieldMapper.TypeParser> metadataMapperParsers = mapperRegistry.getMetadataMapperParsers(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public class MappingParserContext {
private final IdFieldMapper idFieldMapper;
private final Function<Query, BitSetProducer> bitSetProducer;
private final long mappingObjectDepthLimit;
private final RootObjectMapperNamespaceValidator namespaceValidator;
private long mappingObjectDepth = 0;

public MappingParserContext(
Expand All @@ -55,7 +56,8 @@ public MappingParserContext(
IndexAnalyzers indexAnalyzers,
IndexSettings indexSettings,
IdFieldMapper idFieldMapper,
Function<Query, BitSetProducer> bitSetProducer
Function<Query, BitSetProducer> bitSetProducer,
RootObjectMapperNamespaceValidator namespaceValidator
) {
this.similarityLookupService = similarityLookupService;
this.typeParsers = typeParsers;
Expand All @@ -69,6 +71,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<String, SimilarityProvider> similarityLookupService,
Function<String, Mapper.TypeParser> typeParsers,
Function<String, RuntimeField.Parser> runtimeFieldParsers,
IndexVersion indexVersionCreated,
Supplier<TransportVersion> clusterTransportVersion,
Supplier<SearchExecutionContext> searchExecutionContextSupplier,
ScriptCompiler scriptCompiler,
IndexAnalyzers indexAnalyzers,
IndexSettings indexSettings,
IdFieldMapper idFieldMapper,
Function<Query, BitSetProducer> bitSetProducer
) {
this(
similarityLookupService,
typeParsers,
runtimeFieldParsers,
indexVersionCreated,
clusterTransportVersion,
searchExecutionContextSupplier,
scriptCompiler,
indexAnalyzers,
indexSettings,
idFieldMapper,
bitSetProducer,
null
);
}

public RootObjectMapperNamespaceValidator getNamespaceValidator() {
return namespaceValidator;
}

public IndexAnalyzers getIndexAnalyzers() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -547,12 +547,17 @@ public final Optional<SourceKeepMode> sourceKeepMode() {
}

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

protected void validateSubField(Mapper mapper, MappingLookup mappers) {
mapper.validate(mappers);
}

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

public Builder(String name, Optional<Subobjects> subobjects) {
super(name, subobjects);
}

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

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

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

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

Expand Down Expand Up @@ -451,6 +463,7 @@ public static RootObjectMapper.Builder parse(String name, Map<String, Object> no
throws MapperParsingException {
Optional<Subobjects> subobjects = parseSubobjects(node);
RootObjectMapper.Builder builder = new Builder(name, subobjects);
builder.addNamespaceValidator(parserContext.getNamespaceValidator());
Iterator<Map.Entry<String, Object>> iterator = node.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Object> entry = iterator.next();
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
12 changes: 10 additions & 2 deletions server/src/main/java/org/elasticsearch/indices/IndicesModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
import org.elasticsearch.index.mapper.ObjectMapper;
import org.elasticsearch.index.mapper.PassThroughObjectMapper;
import org.elasticsearch.index.mapper.RangeType;
import org.elasticsearch.index.mapper.RootObjectMapperNamespaceValidator;
import org.elasticsearch.index.mapper.RoutingFieldMapper;
import org.elasticsearch.index.mapper.RuntimeField;
import org.elasticsearch.index.mapper.SeqNoFieldMapper;
Expand Down Expand Up @@ -94,15 +95,22 @@
public class IndicesModule extends AbstractModule {
private final MapperRegistry mapperRegistry;

public IndicesModule(List<MapperPlugin> mapperPlugins) {
public IndicesModule(List<MapperPlugin> 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<MapperPlugin> mapperPlugins) {
this(mapperPlugins, null);
}

public static List<NamedWriteableRegistry.Entry> getNamedWriteables() {
return Arrays.asList(
new NamedWriteableRegistry.Entry(Condition.class, MinAgeCondition.NAME, MinAgeCondition::new),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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());
Expand Down