Skip to content

Commit 1f95cae

Browse files
authored
Do not allow modify aliases on followers (#43017)
Now that aliases are replicated by a follower from its leader, this commit prevents directly modifying aliases on follower indices.
1 parent dded863 commit 1f95cae

File tree

11 files changed

+274
-12
lines changed

11 files changed

+274
-12
lines changed

server/src/main/java/org/elasticsearch/action/ActionModule.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
import org.elasticsearch.action.admin.cluster.tasks.PendingClusterTasksAction;
8686
import org.elasticsearch.action.admin.cluster.tasks.TransportPendingClusterTasksAction;
8787
import org.elasticsearch.action.admin.indices.alias.IndicesAliasesAction;
88+
import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
8889
import org.elasticsearch.action.admin.indices.alias.TransportIndicesAliasesAction;
8990
import org.elasticsearch.action.admin.indices.alias.exists.AliasesExistAction;
9091
import org.elasticsearch.action.admin.indices.alias.exists.TransportAliasesExistAction;
@@ -361,6 +362,7 @@ public class ActionModule extends AbstractModule {
361362
private final DestructiveOperations destructiveOperations;
362363
private final RestController restController;
363364
private final RequestValidators<PutMappingRequest> mappingRequestValidators;
365+
private final RequestValidators<IndicesAliasesRequest> indicesAliasesRequestRequestValidators;
364366

365367
public ActionModule(Settings settings, IndexNameExpressionResolver indexNameExpressionResolver,
366368
IndexScopedSettings indexScopedSettings, ClusterSettings clusterSettings, SettingsFilter settingsFilter,
@@ -393,6 +395,8 @@ public ActionModule(Settings settings, IndexNameExpressionResolver indexNameExpr
393395
}
394396
mappingRequestValidators = new RequestValidators<>(
395397
actionPlugins.stream().flatMap(p -> p.mappingRequestValidators().stream()).collect(Collectors.toList()));
398+
indicesAliasesRequestRequestValidators = new RequestValidators<>(
399+
actionPlugins.stream().flatMap(p -> p.indicesAliasesRequestValidators().stream()).collect(Collectors.toList()));
396400

397401
restController = new RestController(headers, restWrapper, nodeClient, circuitBreakerService, usageService);
398402
}
@@ -686,6 +690,7 @@ protected void configure() {
686690
bind(ActionFilters.class).toInstance(actionFilters);
687691
bind(DestructiveOperations.class).toInstance(destructiveOperations);
688692
bind(new TypeLiteral<RequestValidators<PutMappingRequest>>() {}).toInstance(mappingRequestValidators);
693+
bind(new TypeLiteral<RequestValidators<IndicesAliasesRequest>>() {}).toInstance(indicesAliasesRequestRequestValidators);
689694
bind(AutoCreateIndex.class).toInstance(autoCreateIndex);
690695
bind(TransportLivenessAction.class).asEagerSingleton();
691696

server/src/main/java/org/elasticsearch/action/admin/indices/alias/IndicesAliasesRequest.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package org.elasticsearch.action.admin.indices.alias;
2121

2222
import org.elasticsearch.ElasticsearchGenerationException;
23+
import org.elasticsearch.Version;
2324
import org.elasticsearch.action.ActionRequestValidationException;
2425
import org.elasticsearch.action.AliasesRequest;
2526
import org.elasticsearch.action.support.IndicesOptions;
@@ -62,6 +63,7 @@
6263
public class IndicesAliasesRequest extends AcknowledgedRequest<IndicesAliasesRequest> implements ToXContentObject {
6364

6465
private List<AliasActions> allAliasActions = new ArrayList<>();
66+
private String origin = "";
6567

6668
// indices options that require every specified index to exist, expand wildcards only to open
6769
// indices, don't allow that no indices are resolved from wildcard expressions and resolve the
@@ -526,6 +528,15 @@ public int hashCode() {
526528
}
527529
}
528530

531+
public String origin() {
532+
return origin;
533+
}
534+
535+
public IndicesAliasesRequest origin(final String origin) {
536+
this.origin = Objects.requireNonNull(origin);
537+
return this;
538+
}
539+
529540
/**
530541
* Add the action to this request and validate it.
531542
*/
@@ -556,12 +567,23 @@ public ActionRequestValidationException validate() {
556567
public void readFrom(StreamInput in) throws IOException {
557568
super.readFrom(in);
558569
allAliasActions = in.readList(AliasActions::new);
570+
if (in.getVersion().onOrAfter(Version.V_8_0_0)) {
571+
origin = in.readOptionalString();
572+
} else {
573+
origin = null;
574+
}
559575
}
560576

561577
@Override
562578
public void writeTo(StreamOutput out) throws IOException {
563579
super.writeTo(out);
564580
out.writeList(allAliasActions);
581+
// noinspection StatementWithEmptyBody
582+
if (out.getVersion().onOrAfter(Version.V_8_0_0)) {
583+
out.writeOptionalString(origin);
584+
} else {
585+
// nothing to do here, here for symmetry with IndicesAliasesRequest#readFrom
586+
}
565587
}
566588

567589
public IndicesOptions indicesOptions() {

server/src/main/java/org/elasticsearch/action/admin/indices/alias/TransportIndicesAliasesAction.java

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import com.carrotsearch.hppc.cursors.ObjectCursor;
2323
import org.elasticsearch.action.ActionListener;
24+
import org.elasticsearch.action.RequestValidators;
2425
import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions;
2526
import org.elasticsearch.action.support.ActionFilters;
2627
import org.elasticsearch.action.support.master.AcknowledgedResponse;
@@ -37,6 +38,7 @@
3738
import org.elasticsearch.cluster.service.ClusterService;
3839
import org.elasticsearch.common.collect.ImmutableOpenMap;
3940
import org.elasticsearch.common.inject.Inject;
41+
import org.elasticsearch.index.Index;
4042
import org.elasticsearch.rest.action.admin.indices.AliasesNotFoundException;
4143
import org.elasticsearch.threadpool.ThreadPool;
4244
import org.elasticsearch.transport.TransportService;
@@ -45,6 +47,8 @@
4547
import java.util.Collections;
4648
import java.util.HashSet;
4749
import java.util.List;
50+
import java.util.Objects;
51+
import java.util.Optional;
4852
import java.util.Set;
4953

5054
import static java.util.Collections.unmodifiableList;
@@ -55,14 +59,21 @@
5559
public class TransportIndicesAliasesAction extends TransportMasterNodeAction<IndicesAliasesRequest, AcknowledgedResponse> {
5660

5761
private final MetaDataIndexAliasesService indexAliasesService;
62+
private final RequestValidators<IndicesAliasesRequest> requestValidators;
5863

5964
@Inject
60-
public TransportIndicesAliasesAction(TransportService transportService, ClusterService clusterService,
61-
ThreadPool threadPool, MetaDataIndexAliasesService indexAliasesService,
62-
ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver) {
65+
public TransportIndicesAliasesAction(
66+
final TransportService transportService,
67+
final ClusterService clusterService,
68+
final ThreadPool threadPool,
69+
final MetaDataIndexAliasesService indexAliasesService,
70+
final ActionFilters actionFilters,
71+
final IndexNameExpressionResolver indexNameExpressionResolver,
72+
final RequestValidators<IndicesAliasesRequest> requestValidators) {
6373
super(IndicesAliasesAction.NAME, transportService, clusterService, threadPool, actionFilters, indexNameExpressionResolver,
6474
IndicesAliasesRequest::new);
6575
this.indexAliasesService = indexAliasesService;
76+
this.requestValidators = Objects.requireNonNull(requestValidators);
6677
}
6778

6879
@Override
@@ -96,23 +107,28 @@ protected void masterOperation(final IndicesAliasesRequest request, final Cluste
96107
// Resolve all the AliasActions into AliasAction instances and gather all the aliases
97108
Set<String> aliases = new HashSet<>();
98109
for (AliasActions action : actions) {
99-
String[] concreteIndices = indexNameExpressionResolver.concreteIndexNames(state, request.indicesOptions(), action.indices());
110+
final Index[] concreteIndices = indexNameExpressionResolver.concreteIndices(state, request.indicesOptions(), action.indices());
111+
final Optional<Exception> maybeException = requestValidators.validateRequest(request, state, concreteIndices);
112+
if (maybeException.isPresent()) {
113+
listener.onFailure(maybeException.get());
114+
return;
115+
}
100116
Collections.addAll(aliases, action.getOriginalAliases());
101-
for (String index : concreteIndices) {
117+
for (final Index index : concreteIndices) {
102118
switch (action.actionType()) {
103119
case ADD:
104-
for (String alias : concreteAliases(action, state.metaData(), index)) {
105-
finalActions.add(new AliasAction.Add(index, alias, action.filter(), action.indexRouting(),
120+
for (String alias : concreteAliases(action, state.metaData(), index.getName())) {
121+
finalActions.add(new AliasAction.Add(index.getName(), alias, action.filter(), action.indexRouting(),
106122
action.searchRouting(), action.writeIndex()));
107123
}
108124
break;
109125
case REMOVE:
110-
for (String alias : concreteAliases(action, state.metaData(), index)) {
111-
finalActions.add(new AliasAction.Remove(index, alias));
126+
for (String alias : concreteAliases(action, state.metaData(), index.getName())) {
127+
finalActions.add(new AliasAction.Remove(index.getName(), alias));
112128
}
113129
break;
114130
case REMOVE_INDEX:
115-
finalActions.add(new AliasAction.RemoveIndex(index));
131+
finalActions.add(new AliasAction.RemoveIndex(index.getName()));
116132
break;
117133
default:
118134
throw new IllegalArgumentException("Unsupported action [" + action.actionType() + "]");

server/src/main/java/org/elasticsearch/action/admin/indices/mapping/put/PutMappingRequest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
package org.elasticsearch.action.admin.indices.mapping.put;
2121

2222
import com.carrotsearch.hppc.ObjectHashSet;
23-
2423
import org.elasticsearch.ElasticsearchGenerationException;
2524
import org.elasticsearch.action.ActionRequestValidationException;
2625
import org.elasticsearch.action.IndicesRequest;

server/src/main/java/org/elasticsearch/plugins/ActionPlugin.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.elasticsearch.action.ActionRequest;
2424
import org.elasticsearch.action.ActionResponse;
2525
import org.elasticsearch.action.RequestValidators;
26+
import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
2627
import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
2728
import org.elasticsearch.action.support.ActionFilter;
2829
import org.elasticsearch.action.support.TransportAction;
@@ -190,4 +191,8 @@ default Collection<RequestValidators.RequestValidator<PutMappingRequest>> mappin
190191
return Collections.emptyList();
191192
}
192193

194+
default Collection<RequestValidators.RequestValidator<IndicesAliasesRequest>> indicesAliasesRequestValidators() {
195+
return Collections.emptyList();
196+
}
197+
193198
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.action.admin.indices.alias;
21+
22+
import org.elasticsearch.action.RequestValidators;
23+
import org.elasticsearch.action.admin.indices.alias.exists.AliasesExistResponse;
24+
import org.elasticsearch.action.admin.indices.alias.get.GetAliasesRequest;
25+
import org.elasticsearch.action.admin.indices.alias.get.GetAliasesResponse;
26+
import org.elasticsearch.cluster.metadata.AliasMetaData;
27+
import org.elasticsearch.common.settings.Setting;
28+
import org.elasticsearch.common.settings.Settings;
29+
import org.elasticsearch.index.Index;
30+
import org.elasticsearch.plugins.ActionPlugin;
31+
import org.elasticsearch.plugins.Plugin;
32+
import org.elasticsearch.test.ESSingleNodeTestCase;
33+
34+
import java.util.Collection;
35+
import java.util.Collections;
36+
import java.util.List;
37+
import java.util.Locale;
38+
import java.util.Optional;
39+
import java.util.function.Function;
40+
41+
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
42+
import static org.hamcrest.Matchers.containsString;
43+
import static org.hamcrest.Matchers.equalTo;
44+
import static org.hamcrest.Matchers.hasSize;
45+
import static org.hamcrest.Matchers.hasToString;
46+
47+
public class ValidateIndiesAliasesRequestIT extends ESSingleNodeTestCase {
48+
49+
public static class IndicesAliasesPlugin extends Plugin implements ActionPlugin {
50+
51+
static final Setting<List<String>> ALLOWED_ORIGINS_SETTING = Setting.listSetting(
52+
"index.aliases.allowed_origins",
53+
Collections.emptyList(),
54+
Function.identity(),
55+
Setting.Property.IndexScope,
56+
Setting.Property.Dynamic);
57+
58+
@Override
59+
public List<Setting<?>> getSettings() {
60+
return Collections.singletonList(ALLOWED_ORIGINS_SETTING);
61+
}
62+
63+
@Override
64+
public Collection<RequestValidators.RequestValidator<IndicesAliasesRequest>> indicesAliasesRequestValidators() {
65+
return Collections.singletonList((request, state, indices) -> {
66+
for (final Index index : indices) {
67+
final List<String> allowedOrigins = ALLOWED_ORIGINS_SETTING.get(state.metaData().index(index).getSettings());
68+
if (allowedOrigins.contains(request.origin()) == false) {
69+
final String message = String.format(
70+
Locale.ROOT,
71+
"origin [%s] not allowed for index [%s]",
72+
request.origin(),
73+
index.getName());
74+
return Optional.of(new IllegalStateException(message));
75+
}
76+
}
77+
return Optional.empty();
78+
});
79+
}
80+
81+
}
82+
83+
@Override
84+
protected Collection<Class<? extends Plugin>> getPlugins() {
85+
return Collections.singletonList(IndicesAliasesPlugin.class);
86+
}
87+
88+
public void testAllowed() {
89+
final Settings settings = Settings.builder()
90+
.putList(IndicesAliasesPlugin.ALLOWED_ORIGINS_SETTING.getKey(), Collections.singletonList("allowed"))
91+
.build();
92+
createIndex("index", settings);
93+
final IndicesAliasesRequest request = new IndicesAliasesRequest().origin("allowed");
94+
request.addAliasAction(IndicesAliasesRequest.AliasActions.add().index("index").alias("alias"));
95+
assertAcked(client().admin().indices().aliases(request).actionGet());
96+
final GetAliasesResponse response = client().admin().indices().getAliases(new GetAliasesRequest("alias")).actionGet();
97+
assertThat(response.getAliases().keys().size(), equalTo(1));
98+
assertThat(response.getAliases().keys().iterator().next().value, equalTo("index"));
99+
final List<AliasMetaData> aliasMetaData = response.getAliases().get("index");
100+
assertThat(aliasMetaData, hasSize(1));
101+
assertThat(aliasMetaData.get(0).alias(), equalTo("alias"));
102+
}
103+
104+
public void testNotAllowed() {
105+
final Settings settings = Settings.builder()
106+
.putList(IndicesAliasesPlugin.ALLOWED_ORIGINS_SETTING.getKey(), Collections.singletonList("allowed"))
107+
.build();
108+
createIndex("index", settings);
109+
final String origin = randomFrom("", "not-allowed");
110+
final IndicesAliasesRequest request = new IndicesAliasesRequest().origin(origin);
111+
request.addAliasAction(IndicesAliasesRequest.AliasActions.add().index("index").alias("alias"));
112+
final Exception e = expectThrows(IllegalStateException.class, () -> client().admin().indices().aliases(request).actionGet());
113+
assertThat(e, hasToString(containsString("origin [" + origin + "] not allowed for index [index]")));
114+
}
115+
116+
public void testSomeAllowed() {
117+
final Settings fooIndexSettings = Settings.builder()
118+
.putList(IndicesAliasesPlugin.ALLOWED_ORIGINS_SETTING.getKey(), Collections.singletonList("foo_allowed"))
119+
.build();
120+
createIndex("foo", fooIndexSettings);
121+
final Settings barIndexSettings = Settings.builder()
122+
.putList(IndicesAliasesPlugin.ALLOWED_ORIGINS_SETTING.getKey(), Collections.singletonList("bar_allowed"))
123+
.build();
124+
createIndex("bar", barIndexSettings);
125+
final String origin = randomFrom("foo_allowed", "bar_allowed");
126+
final IndicesAliasesRequest request = new IndicesAliasesRequest().origin(origin);
127+
request.addAliasAction(IndicesAliasesRequest.AliasActions.add().index("foo").alias("alias"));
128+
request.addAliasAction(IndicesAliasesRequest.AliasActions.add().index("bar").alias("alias"));
129+
final Exception e = expectThrows(IllegalStateException.class, () -> client().admin().indices().aliases(request).actionGet());
130+
final String index = "foo_allowed".equals(origin) ? "bar" : "foo";
131+
assertThat(e, hasToString(containsString("origin [" + origin + "] not allowed for index [" + index + "]")));
132+
final AliasesExistResponse response = client().admin().indices().aliasesExist(new GetAliasesRequest("alias")).actionGet();
133+
assertFalse(response.exists());
134+
}
135+
136+
}

x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.elasticsearch.action.ActionRequest;
1111
import org.elasticsearch.action.ActionResponse;
1212
import org.elasticsearch.action.RequestValidators;
13+
import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
1314
import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
1415
import org.elasticsearch.client.Client;
1516
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
@@ -349,4 +350,8 @@ public Collection<RequestValidators.RequestValidator<PutMappingRequest>> mapping
349350
return Collections.singletonList(CcrRequests.CCR_PUT_MAPPING_REQUEST_VALIDATOR);
350351
}
351352

353+
@Override
354+
public Collection<RequestValidators.RequestValidator<IndicesAliasesRequest>> indicesAliasesRequestValidators() {
355+
return Collections.singletonList(CcrRequests.CCR_INDICES_ALIASES_REQUEST_VALIDATOR);
356+
}
352357
}

x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/CcrRequests.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.elasticsearch.action.ActionListener;
1010
import org.elasticsearch.action.RequestValidators;
1111
import org.elasticsearch.action.admin.cluster.state.ClusterStateRequest;
12+
import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
1213
import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
1314
import org.elasticsearch.client.Client;
1415
import org.elasticsearch.cluster.metadata.IndexMetaData;
@@ -105,4 +106,23 @@ public static void getIndexMetadata(Client client, Index index, long mappingVers
105106
return Optional.empty();
106107
};
107108

109+
public static final RequestValidators.RequestValidator<IndicesAliasesRequest> CCR_INDICES_ALIASES_REQUEST_VALIDATOR =
110+
(request, state, indices) -> {
111+
if (request.origin() == null) {
112+
return Optional.empty(); // an indices aliases request on old versions does not have origin
113+
}
114+
final List<Index> followingIndices = Arrays.stream(indices)
115+
.filter(index -> {
116+
final IndexMetaData indexMetaData = state.metaData().index(index);
117+
return indexMetaData != null && CcrSettings.CCR_FOLLOWING_INDEX_SETTING.get(indexMetaData.getSettings());
118+
}).collect(Collectors.toList());
119+
if (followingIndices.isEmpty() == false && "ccr".equals(request.origin()) == false) {
120+
final String errorMessage = "can't modify aliases on indices "
121+
+ "[" + followingIndices.stream().map(Index::getName).collect(Collectors.joining(", ")) + "]; "
122+
+ "aliases of following indices are self-replicated from their leader indices";
123+
return Optional.of(new ElasticsearchStatusException(errorMessage, RestStatus.FORBIDDEN));
124+
}
125+
return Optional.empty();
126+
};
127+
108128
}

0 commit comments

Comments
 (0)