diff --git a/server/src/main/java/org/elasticsearch/action/ActionModule.java b/server/src/main/java/org/elasticsearch/action/ActionModule.java index 96ef3235d4c..e991d3f443e 100644 --- a/server/src/main/java/org/elasticsearch/action/ActionModule.java +++ b/server/src/main/java/org/elasticsearch/action/ActionModule.java @@ -85,6 +85,7 @@ import org.elasticsearch.action.admin.cluster.storedscripts.TransportPutStoredSc import org.elasticsearch.action.admin.cluster.tasks.PendingClusterTasksAction; import org.elasticsearch.action.admin.cluster.tasks.TransportPendingClusterTasksAction; import org.elasticsearch.action.admin.indices.alias.IndicesAliasesAction; +import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; import org.elasticsearch.action.admin.indices.alias.TransportIndicesAliasesAction; import org.elasticsearch.action.admin.indices.alias.exists.AliasesExistAction; import org.elasticsearch.action.admin.indices.alias.exists.TransportAliasesExistAction; @@ -362,6 +363,7 @@ public class ActionModule extends AbstractModule { private final DestructiveOperations destructiveOperations; private final RestController restController; private final RequestValidators mappingRequestValidators; + private final RequestValidators indicesAliasesRequestRequestValidators; public ActionModule(boolean transportClient, Settings settings, IndexNameExpressionResolver indexNameExpressionResolver, IndexScopedSettings indexScopedSettings, ClusterSettings clusterSettings, SettingsFilter settingsFilter, @@ -395,6 +397,8 @@ public class ActionModule extends AbstractModule { } mappingRequestValidators = new RequestValidators<>( actionPlugins.stream().flatMap(p -> p.mappingRequestValidators().stream()).collect(Collectors.toList())); + indicesAliasesRequestRequestValidators = new RequestValidators<>( + actionPlugins.stream().flatMap(p -> p.indicesAliasesRequestValidators().stream()).collect(Collectors.toList())); if (transportClient) { restController = null; @@ -692,6 +696,7 @@ public class ActionModule extends AbstractModule { bind(ActionFilters.class).toInstance(actionFilters); bind(DestructiveOperations.class).toInstance(destructiveOperations); bind(new TypeLiteral>() {}).toInstance(mappingRequestValidators); + bind(new TypeLiteral>() {}).toInstance(indicesAliasesRequestRequestValidators); if (false == transportClient) { // Supporting classes only used when not a transport client diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/alias/IndicesAliasesRequest.java b/server/src/main/java/org/elasticsearch/action/admin/indices/alias/IndicesAliasesRequest.java index 8ef16012cf3..5810a3cf62f 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/alias/IndicesAliasesRequest.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/alias/IndicesAliasesRequest.java @@ -63,6 +63,7 @@ import static org.elasticsearch.common.xcontent.ObjectParser.fromList; public class IndicesAliasesRequest extends AcknowledgedRequest implements ToXContentObject { private List allAliasActions = new ArrayList<>(); + private String origin = ""; // indices options that require every specified index to exist, expand wildcards only to open // indices, don't allow that no indices are resolved from wildcard expressions and resolve the @@ -535,6 +536,15 @@ public class IndicesAliasesRequest extends AcknowledgedRequest { private final MetaDataIndexAliasesService indexAliasesService; + private final RequestValidators requestValidators; @Inject - public TransportIndicesAliasesAction(TransportService transportService, ClusterService clusterService, - ThreadPool threadPool, MetaDataIndexAliasesService indexAliasesService, - ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver) { + public TransportIndicesAliasesAction( + final TransportService transportService, + final ClusterService clusterService, + final ThreadPool threadPool, + final MetaDataIndexAliasesService indexAliasesService, + final ActionFilters actionFilters, + final IndexNameExpressionResolver indexNameExpressionResolver, + final RequestValidators requestValidators) { super(IndicesAliasesAction.NAME, transportService, clusterService, threadPool, actionFilters, indexNameExpressionResolver, IndicesAliasesRequest::new); this.indexAliasesService = indexAliasesService; + this.requestValidators = Objects.requireNonNull(requestValidators); } @Override @@ -96,23 +107,28 @@ public class TransportIndicesAliasesAction extends TransportMasterNodeAction aliases = new HashSet<>(); for (AliasActions action : actions) { - String[] concreteIndices = indexNameExpressionResolver.concreteIndexNames(state, request.indicesOptions(), action.indices()); + final Index[] concreteIndices = indexNameExpressionResolver.concreteIndices(state, request.indicesOptions(), action.indices()); + final Optional maybeException = requestValidators.validateRequest(request, state, concreteIndices); + if (maybeException.isPresent()) { + listener.onFailure(maybeException.get()); + return; + } Collections.addAll(aliases, action.getOriginalAliases()); - for (String index : concreteIndices) { + for (final Index index : concreteIndices) { switch (action.actionType()) { case ADD: - for (String alias : concreteAliases(action, state.metaData(), index)) { - finalActions.add(new AliasAction.Add(index, alias, action.filter(), action.indexRouting(), + for (String alias : concreteAliases(action, state.metaData(), index.getName())) { + finalActions.add(new AliasAction.Add(index.getName(), alias, action.filter(), action.indexRouting(), action.searchRouting(), action.writeIndex())); } break; case REMOVE: - for (String alias : concreteAliases(action, state.metaData(), index)) { - finalActions.add(new AliasAction.Remove(index, alias)); + for (String alias : concreteAliases(action, state.metaData(), index.getName())) { + finalActions.add(new AliasAction.Remove(index.getName(), alias)); } break; case REMOVE_INDEX: - finalActions.add(new AliasAction.RemoveIndex(index)); + finalActions.add(new AliasAction.RemoveIndex(index.getName())); break; default: throw new IllegalArgumentException("Unsupported action [" + action.actionType() + "]"); diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/mapping/put/PutMappingRequest.java b/server/src/main/java/org/elasticsearch/action/admin/indices/mapping/put/PutMappingRequest.java index ce1efb998d5..52858d8b9d3 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/mapping/put/PutMappingRequest.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/mapping/put/PutMappingRequest.java @@ -20,7 +20,6 @@ package org.elasticsearch.action.admin.indices.mapping.put; import com.carrotsearch.hppc.ObjectHashSet; - import org.elasticsearch.ElasticsearchGenerationException; import org.elasticsearch.Version; import org.elasticsearch.action.ActionRequestValidationException; diff --git a/server/src/main/java/org/elasticsearch/plugins/ActionPlugin.java b/server/src/main/java/org/elasticsearch/plugins/ActionPlugin.java index 44400549657..b85904178a2 100644 --- a/server/src/main/java/org/elasticsearch/plugins/ActionPlugin.java +++ b/server/src/main/java/org/elasticsearch/plugins/ActionPlugin.java @@ -23,6 +23,7 @@ import org.elasticsearch.action.Action; import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.action.RequestValidators; +import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest; import org.elasticsearch.action.support.ActionFilter; import org.elasticsearch.action.support.TransportAction; @@ -190,4 +191,8 @@ public interface ActionPlugin { return Collections.emptyList(); } + default Collection> indicesAliasesRequestValidators() { + return Collections.emptyList(); + } + } diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/alias/ValidateIndiesAliasesRequestIT.java b/server/src/test/java/org/elasticsearch/action/admin/indices/alias/ValidateIndiesAliasesRequestIT.java new file mode 100644 index 00000000000..ad73e82078f --- /dev/null +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/alias/ValidateIndiesAliasesRequestIT.java @@ -0,0 +1,136 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.action.admin.indices.alias; + +import org.elasticsearch.action.RequestValidators; +import org.elasticsearch.action.admin.indices.alias.exists.AliasesExistResponse; +import org.elasticsearch.action.admin.indices.alias.get.GetAliasesRequest; +import org.elasticsearch.action.admin.indices.alias.get.GetAliasesResponse; +import org.elasticsearch.cluster.metadata.AliasMetaData; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.Index; +import org.elasticsearch.plugins.ActionPlugin; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.test.ESSingleNodeTestCase; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.function.Function; + +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.hasToString; + +public class ValidateIndiesAliasesRequestIT extends ESSingleNodeTestCase { + + public static class IndicesAliasesPlugin extends Plugin implements ActionPlugin { + + static final Setting> ALLOWED_ORIGINS_SETTING = Setting.listSetting( + "index.aliases.allowed_origins", + Collections.emptyList(), + Function.identity(), + Setting.Property.IndexScope, + Setting.Property.Dynamic); + + @Override + public List> getSettings() { + return Collections.singletonList(ALLOWED_ORIGINS_SETTING); + } + + @Override + public Collection> indicesAliasesRequestValidators() { + return Collections.singletonList((request, state, indices) -> { + for (final Index index : indices) { + final List allowedOrigins = ALLOWED_ORIGINS_SETTING.get(state.metaData().index(index).getSettings()); + if (allowedOrigins.contains(request.origin()) == false) { + final String message = String.format( + Locale.ROOT, + "origin [%s] not allowed for index [%s]", + request.origin(), + index.getName()); + return Optional.of(new IllegalStateException(message)); + } + } + return Optional.empty(); + }); + } + + } + + @Override + protected Collection> getPlugins() { + return Collections.singletonList(IndicesAliasesPlugin.class); + } + + public void testAllowed() { + final Settings settings = Settings.builder() + .putList(IndicesAliasesPlugin.ALLOWED_ORIGINS_SETTING.getKey(), Collections.singletonList("allowed")) + .build(); + createIndex("index", settings); + final IndicesAliasesRequest request = new IndicesAliasesRequest().origin("allowed"); + request.addAliasAction(IndicesAliasesRequest.AliasActions.add().index("index").alias("alias")); + assertAcked(client().admin().indices().aliases(request).actionGet()); + final GetAliasesResponse response = client().admin().indices().getAliases(new GetAliasesRequest("alias")).actionGet(); + assertThat(response.getAliases().keys().size(), equalTo(1)); + assertThat(response.getAliases().keys().iterator().next().value, equalTo("index")); + final List aliasMetaData = response.getAliases().get("index"); + assertThat(aliasMetaData, hasSize(1)); + assertThat(aliasMetaData.get(0).alias(), equalTo("alias")); + } + + public void testNotAllowed() { + final Settings settings = Settings.builder() + .putList(IndicesAliasesPlugin.ALLOWED_ORIGINS_SETTING.getKey(), Collections.singletonList("allowed")) + .build(); + createIndex("index", settings); + final String origin = randomFrom("", "not-allowed"); + final IndicesAliasesRequest request = new IndicesAliasesRequest().origin(origin); + request.addAliasAction(IndicesAliasesRequest.AliasActions.add().index("index").alias("alias")); + final Exception e = expectThrows(IllegalStateException.class, () -> client().admin().indices().aliases(request).actionGet()); + assertThat(e, hasToString(containsString("origin [" + origin + "] not allowed for index [index]"))); + } + + public void testSomeAllowed() { + final Settings fooIndexSettings = Settings.builder() + .putList(IndicesAliasesPlugin.ALLOWED_ORIGINS_SETTING.getKey(), Collections.singletonList("foo_allowed")) + .build(); + createIndex("foo", fooIndexSettings); + final Settings barIndexSettings = Settings.builder() + .putList(IndicesAliasesPlugin.ALLOWED_ORIGINS_SETTING.getKey(), Collections.singletonList("bar_allowed")) + .build(); + createIndex("bar", barIndexSettings); + final String origin = randomFrom("foo_allowed", "bar_allowed"); + final IndicesAliasesRequest request = new IndicesAliasesRequest().origin(origin); + request.addAliasAction(IndicesAliasesRequest.AliasActions.add().index("foo").alias("alias")); + request.addAliasAction(IndicesAliasesRequest.AliasActions.add().index("bar").alias("alias")); + final Exception e = expectThrows(IllegalStateException.class, () -> client().admin().indices().aliases(request).actionGet()); + final String index = "foo_allowed".equals(origin) ? "bar" : "foo"; + assertThat(e, hasToString(containsString("origin [" + origin + "] not allowed for index [" + index + "]"))); + final AliasesExistResponse response = client().admin().indices().aliasesExist(new GetAliasesRequest("alias")).actionGet(); + assertFalse(response.exists()); + } + +} diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java index 53986b076b6..808e397192f 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java @@ -10,6 +10,7 @@ import org.apache.lucene.util.SetOnce; import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.action.RequestValidators; +import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest; import org.elasticsearch.client.Client; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; @@ -355,4 +356,8 @@ public class Ccr extends Plugin implements ActionPlugin, PersistentTaskPlugin, E return Collections.singletonList(CcrRequests.CCR_PUT_MAPPING_REQUEST_VALIDATOR); } + @Override + public Collection> indicesAliasesRequestValidators() { + return Collections.singletonList(CcrRequests.CCR_INDICES_ALIASES_REQUEST_VALIDATOR); + } } diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/CcrRequests.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/CcrRequests.java index a50e3c301bd..8bb29ce3982 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/CcrRequests.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/CcrRequests.java @@ -9,6 +9,7 @@ import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.RequestValidators; import org.elasticsearch.action.admin.cluster.state.ClusterStateRequest; +import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest; import org.elasticsearch.client.Client; import org.elasticsearch.cluster.metadata.IndexMetaData; @@ -105,4 +106,23 @@ public final class CcrRequests { return Optional.empty(); }; + public static final RequestValidators.RequestValidator CCR_INDICES_ALIASES_REQUEST_VALIDATOR = + (request, state, indices) -> { + if (request.origin() == null) { + return Optional.empty(); // an indices aliases request on old versions does not have origin + } + final List followingIndices = Arrays.stream(indices) + .filter(index -> { + final IndexMetaData indexMetaData = state.metaData().index(index); + return indexMetaData != null && CcrSettings.CCR_FOLLOWING_INDEX_SETTING.get(indexMetaData.getSettings()); + }).collect(Collectors.toList()); + if (followingIndices.isEmpty() == false && "ccr".equals(request.origin()) == false) { + final String errorMessage = "can't modify aliases on indices " + + "[" + followingIndices.stream().map(Index::getName).collect(Collectors.joining(", ")) + "]; " + + "aliases of following indices are self-replicated from their leader indices"; + return Optional.of(new ElasticsearchStatusException(errorMessage, RestStatus.FORBIDDEN)); + } + return Optional.empty(); + }; + } diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowTasksExecutor.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowTasksExecutor.java index fee95e44828..a542f363b04 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowTasksExecutor.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowTasksExecutor.java @@ -305,10 +305,11 @@ public class ShardFollowTasksExecutor extends PersistentTasksExecutor followerClient().admin().indices().aliases(request).actionGet()); + assertThat( + e, + hasToString(containsString("can't modify aliases on indices [follower]; " + + "aliases of following indices are self-replicated from their leader indices"))); + assertThat(e.status(), equalTo(RestStatus.FORBIDDEN)); + } + + public void testAddAliasAfterUnfollow() throws Exception { + final String leaderIndexSettings = + getIndexSettings(between(1, 2), between(0, 1), singletonMap(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true")); + assertAcked(leaderClient().admin().indices().prepareCreate("leader").setSource(leaderIndexSettings, XContentType.JSON)); + followerClient().execute(PutFollowAction.INSTANCE, putFollow("leader", "follower")).get(); + pauseFollow("follower"); + followerClient().admin().indices().close(new CloseIndexRequest("follower")).actionGet(); + assertAcked(followerClient().execute(UnfollowAction.INSTANCE, new UnfollowAction.Request("follower")).actionGet()); + followerClient().admin().indices().open(new OpenIndexRequest("follower")).actionGet(); + final IndicesAliasesRequest request = new IndicesAliasesRequest() + .addAliasAction(IndicesAliasesRequest.AliasActions.add().index("follower").alias("follower_alias")); + assertAcked(followerClient().admin().indices().aliases(request).actionGet()); + final GetAliasesResponse response = + followerClient().admin().indices().getAliases(new GetAliasesRequest("follower_alias")).actionGet(); + assertThat(response.getAliases().keys().size(), equalTo(1)); + assertThat(response.getAliases().keys().iterator().next().value, equalTo("follower")); + final List aliasMetaData = response.getAliases().get("follower"); + assertThat(aliasMetaData, hasSize(1)); + assertThat(aliasMetaData.get(0).alias(), equalTo("follower_alias")); + } + public void testFollowIndex_backlog() throws Exception { int numberOfShards = between(1, 5); String leaderIndexSettings = getIndexSettings(numberOfShards, between(0, 1), diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java index 9caf304a4f5..2157cbd38ca 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.core; import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.action.RequestValidators; +import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest; import org.elasticsearch.action.support.ActionFilter; import org.elasticsearch.bootstrap.BootstrapCheck; @@ -439,8 +440,17 @@ public class LocalStateCompositeXPackPlugin extends XPackPlugin implements Scrip .collect(Collectors.toList()); } + @Override + public Collection> indicesAliasesRequestValidators() { + return filterPlugins(ActionPlugin.class) + .stream() + .flatMap(p -> p.indicesAliasesRequestValidators().stream()) + .collect(Collectors.toList()); + } + private List filterPlugins(Class type) { return plugins.stream().filter(x -> type.isAssignableFrom(x.getClass())).map(p -> ((T)p)) .collect(Collectors.toList()); } + }