Skip to content

Commit 6243a97

Browse files
_cat/indices with Security, hide names when wildcard (#38824)
This changes the output of the `_cat/indices` API with `Security` enabled. It is possible to only display the index name (and possibly the index health, depending on the request options) but not its stats (doc count, merges, size, etc). This is the case for closed indices which have index metadata in the cluster state but no associated shards, hence no shard stats. However, when `Security` is enabled, and the request contains wildcards, **open** indices without stats are a common occurrence. This is because the index names in the response table are picked up directly from the cluster state which is not filtered by `Security`'s _indexNameExpressionResolver_, unlike the stats data which is populated by the indices stats API which does go through the index name resolver. This is a bug, because it is circumventing `Security`'s function to hide unauthorized indices. This has been fixed by displaying the index names as they are resolved by the indices stats API. The outputs of these two APIs is now very similar: same index names, similar data but different format. Closes #37190
1 parent 7d78f46 commit 6243a97

File tree

3 files changed

+315
-57
lines changed

3 files changed

+315
-57
lines changed

server/src/main/java/org/elasticsearch/rest/action/cat/RestIndicesAction.java

+55-25
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@
3535
import org.elasticsearch.cluster.health.ClusterIndexHealth;
3636
import org.elasticsearch.cluster.metadata.IndexMetaData;
3737
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
38-
import org.elasticsearch.cluster.metadata.MetaData;
3938
import org.elasticsearch.common.Strings;
4039
import org.elasticsearch.common.Table;
40+
import org.elasticsearch.common.collect.ImmutableOpenMap;
4141
import org.elasticsearch.common.settings.Settings;
4242
import org.elasticsearch.common.time.DateFormatter;
4343
import org.elasticsearch.index.Index;
@@ -95,13 +95,8 @@ public RestChannelConsumer doCatRequest(final RestRequest request, final NodeCli
9595
return channel -> client.admin().cluster().state(clusterStateRequest, new RestActionListener<ClusterStateResponse>(channel) {
9696
@Override
9797
public void processResponse(final ClusterStateResponse clusterStateResponse) {
98-
final ClusterState state = clusterStateResponse.getState();
99-
final Index[] concreteIndices = indexNameExpressionResolver.concreteIndices(state, strictExpandIndicesOptions, indices);
100-
// concreteIndices should contain exactly the indices in state.metaData() that were selected by clusterStateRequest using
101-
// IndicesOptions.strictExpand(). We select the indices again here so that they can be displayed in the resulting table
102-
// in the requesting order.
103-
assert concreteIndices.length == state.metaData().getIndices().size();
104-
98+
final ClusterState clusterState = clusterStateResponse.getState();
99+
final IndexMetaData[] indicesMetaData = getOrderedIndexMetaData(indices, clusterState, strictExpandIndicesOptions);
105100
// Indices that were successfully resolved during the cluster state request might be deleted when the subsequent cluster
106101
// health and indices stats requests execute. We have to distinguish two cases:
107102
// 1) the deleted index was explicitly passed as parameter to the /_cat/indices request. In this case we want the subsequent
@@ -111,24 +106,24 @@ public void processResponse(final ClusterStateResponse clusterStateResponse) {
111106
// This behavior can be ensured by letting the cluster health and indices stats requests re-resolve the index names with the
112107
// same indices options that we used for the initial cluster state request (strictExpand). Unfortunately cluster health
113108
// requests hard-code their indices options and the best we can do is apply strictExpand to the indices stats request.
114-
ClusterHealthRequest clusterHealthRequest = Requests.clusterHealthRequest(indices);
109+
final ClusterHealthRequest clusterHealthRequest = Requests.clusterHealthRequest(indices);
115110
clusterHealthRequest.local(request.paramAsBoolean("local", clusterHealthRequest.local()));
111+
116112
client.admin().cluster().health(clusterHealthRequest, new RestActionListener<ClusterHealthResponse>(channel) {
117113
@Override
118114
public void processResponse(final ClusterHealthResponse clusterHealthResponse) {
119-
IndicesStatsRequest indicesStatsRequest = new IndicesStatsRequest();
115+
final IndicesStatsRequest indicesStatsRequest = new IndicesStatsRequest();
120116
indicesStatsRequest.indices(indices);
121117
indicesStatsRequest.indicesOptions(strictExpandIndicesOptions);
122118
indicesStatsRequest.all();
119+
123120
client.admin().indices().stats(indicesStatsRequest, new RestResponseListener<IndicesStatsResponse>(channel) {
124121
@Override
125122
public RestResponse buildResponse(IndicesStatsResponse indicesStatsResponse) throws Exception {
126-
Table tab = buildTable(request, concreteIndices, clusterHealthResponse,
127-
indicesStatsResponse, state.metaData());
123+
final Table tab = buildTable(request, indicesMetaData, clusterHealthResponse, indicesStatsResponse);
128124
return RestTable.buildResponse(tab, channel);
129125
}
130126
});
131-
132127
}
133128
});
134129
}
@@ -388,8 +383,7 @@ protected Table getTableWithHeader(final RestRequest request) {
388383
}
389384

390385
// package private for testing
391-
Table buildTable(RestRequest request, Index[] indices, ClusterHealthResponse response,
392-
IndicesStatsResponse stats, MetaData indexMetaDatas) {
386+
Table buildTable(RestRequest request, IndexMetaData[] indicesMetaData, ClusterHealthResponse response, IndicesStatsResponse stats) {
393387
final String healthParam = request.param("health");
394388
final ClusterHealthStatus status;
395389
if (healthParam != null) {
@@ -400,31 +394,50 @@ Table buildTable(RestRequest request, Index[] indices, ClusterHealthResponse res
400394

401395
Table table = getTableWithHeader(request);
402396

403-
for (final Index index : indices) {
404-
final String indexName = index.getName();
397+
for (IndexMetaData indexMetaData : indicesMetaData) {
398+
final String indexName = indexMetaData.getIndex().getName();
405399
ClusterIndexHealth indexHealth = response.getIndices().get(indexName);
406400
IndexStats indexStats = stats.getIndices().get(indexName);
407-
IndexMetaData indexMetaData = indexMetaDatas.getIndices().get(indexName);
408401
IndexMetaData.State state = indexMetaData.getState();
409402
boolean searchThrottled = IndexSettings.INDEX_SEARCH_THROTTLED.get(indexMetaData.getSettings());
410403

411404
if (status != null) {
412405
if (state == IndexMetaData.State.CLOSE ||
413-
(indexHealth == null && !ClusterHealthStatus.RED.equals(status)) ||
414-
!indexHealth.getStatus().equals(status)) {
406+
(indexHealth == null && false == ClusterHealthStatus.RED.equals(status)) ||
407+
false == indexHealth.getStatus().equals(status)) {
415408
continue;
416409
}
417410
}
418411

419-
final CommonStats primaryStats = indexStats == null ? new CommonStats() : indexStats.getPrimaries();
420-
final CommonStats totalStats = indexStats == null ? new CommonStats() : indexStats.getTotal();
412+
// the open index is present in the cluster state but is not returned in the indices stats API
413+
if (indexStats == null && state != IndexMetaData.State.CLOSE) {
414+
// the index stats API is called last, after cluster state and cluster health. If the index stats
415+
// has not resolved the same open indices as the initial cluster state call, then the indices might
416+
// have been removed in the meantime or, more likely, are unauthorized. This is because the cluster
417+
// state exposes everything, even unauthorized indices, which are not exposed in APIs.
418+
// We ignore such an index instead of displaying it with an empty stats.
419+
continue;
420+
}
421+
422+
final CommonStats primaryStats;
423+
final CommonStats totalStats;
424+
425+
if (state == IndexMetaData.State.CLOSE) {
426+
// empty stats for closed indices, but their names are displayed
427+
assert indexStats == null;
428+
primaryStats = new CommonStats();
429+
totalStats = new CommonStats();
430+
} else {
431+
primaryStats = indexStats.getPrimaries();
432+
totalStats = indexStats.getTotal();
433+
}
421434

422435
table.startRow();
423436
table.addCell(state == IndexMetaData.State.OPEN ?
424437
(indexHealth == null ? "red*" : indexHealth.getStatus().toString().toLowerCase(Locale.ROOT)) : null);
425438
table.addCell(state.toString().toLowerCase(Locale.ROOT));
426439
table.addCell(indexName);
427-
table.addCell(index.getUUID());
440+
table.addCell(indexMetaData.getIndexUUID());
428441
table.addCell(indexHealth == null ? null : indexHealth.getNumberOfShards());
429442
table.addCell(indexHealth == null ? null : indexHealth.getNumberOfReplicas());
430443

@@ -606,8 +619,8 @@ Table buildTable(RestRequest request, Index[] indices, ClusterHealthResponse res
606619
table.addCell(totalStats.getSearch() == null ? null : totalStats.getSearch().getTotal().getSuggestCount());
607620
table.addCell(primaryStats.getSearch() == null ? null : primaryStats.getSearch().getTotal().getSuggestCount());
608621

609-
table.addCell(indexStats == null ? null : indexStats.getTotal().getTotalMemory());
610-
table.addCell(indexStats == null ? null : indexStats.getPrimaries().getTotalMemory());
622+
table.addCell(totalStats.getTotalMemory());
623+
table.addCell(primaryStats.getTotalMemory());
611624

612625
table.addCell(searchThrottled);
613626

@@ -616,4 +629,21 @@ Table buildTable(RestRequest request, Index[] indices, ClusterHealthResponse res
616629

617630
return table;
618631
}
632+
633+
// package private for testing
634+
IndexMetaData[] getOrderedIndexMetaData(String[] indicesExpression, ClusterState clusterState, IndicesOptions indicesOptions) {
635+
final Index[] concreteIndices = indexNameExpressionResolver.concreteIndices(clusterState, indicesOptions, indicesExpression);
636+
// concreteIndices should contain exactly the indices in state.metaData() that were selected by clusterStateRequest using the
637+
// same indices option (IndicesOptions.strictExpand()). We select the indices again here so that they can be displayed in the
638+
// resulting table in the requesting order.
639+
assert concreteIndices.length == clusterState.metaData().getIndices().size();
640+
final ImmutableOpenMap<String, IndexMetaData> indexMetaDataMap = clusterState.metaData().getIndices();
641+
final IndexMetaData[] indicesMetaData = new IndexMetaData[concreteIndices.length];
642+
// select the index metadata in the requested order, so that the response can display the indices in the resulting table
643+
// in the requesting order.
644+
for (int i = 0; i < concreteIndices.length; i++) {
645+
indicesMetaData[i] = indexMetaDataMap.get(concreteIndices[i].getName());
646+
}
647+
return indicesMetaData;
648+
}
619649
}

server/src/test/java/org/elasticsearch/rest/action/cat/RestIndicesActionTests.java

+60-32
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse;
2626
import org.elasticsearch.action.admin.indices.stats.IndicesStatsTests;
2727
import org.elasticsearch.action.admin.indices.stats.ShardStats;
28+
import org.elasticsearch.action.support.IndicesOptions;
2829
import org.elasticsearch.cluster.ClusterName;
2930
import org.elasticsearch.cluster.ClusterState;
3031
import org.elasticsearch.cluster.metadata.IndexMetaData;
@@ -38,7 +39,6 @@
3839
import org.elasticsearch.common.UUIDs;
3940
import org.elasticsearch.common.settings.Settings;
4041
import org.elasticsearch.common.unit.TimeValue;
41-
import org.elasticsearch.index.Index;
4242
import org.elasticsearch.index.cache.query.QueryCacheStats;
4343
import org.elasticsearch.index.cache.request.RequestCacheStats;
4444
import org.elasticsearch.index.engine.SegmentsStats;
@@ -62,6 +62,7 @@
6262

6363
import java.nio.file.Path;
6464
import java.util.ArrayList;
65+
import java.util.Arrays;
6566
import java.util.Collections;
6667
import java.util.List;
6768

@@ -73,44 +74,61 @@
7374
*/
7475
public class RestIndicesActionTests extends ESTestCase {
7576

76-
public void testBuildTable() {
77-
final Settings settings = Settings.EMPTY;
78-
UsageService usageService = new UsageService();
79-
final RestController restController = new RestController(Collections.emptySet(), null, null, null, usageService);
80-
final RestIndicesAction action = new RestIndicesAction(settings, restController, new IndexNameExpressionResolver());
81-
77+
private IndexMetaData[] buildRandomIndicesMetaData(int numIndices) {
8278
// build a (semi-)random table
83-
final int numIndices = randomIntBetween(0, 5);
84-
Index[] indices = new Index[numIndices];
79+
final IndexMetaData[] indicesMetaData = new IndexMetaData[numIndices];
8580
for (int i = 0; i < numIndices; i++) {
86-
indices[i] = new Index(randomAlphaOfLength(5), UUIDs.randomBase64UUID());
87-
}
88-
89-
final MetaData.Builder metaDataBuilder = MetaData.builder();
90-
for (final Index index : indices) {
91-
metaDataBuilder.put(IndexMetaData.builder(index.getName())
81+
indicesMetaData[i] = IndexMetaData.builder(randomAlphaOfLength(5) + i)
9282
.settings(Settings.builder()
93-
.put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)
94-
.put(IndexMetaData.SETTING_INDEX_UUID, index.getUUID()))
83+
.put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT)
84+
.put(IndexMetaData.SETTING_INDEX_UUID, UUIDs.randomBase64UUID()))
9585
.creationDate(System.currentTimeMillis())
9686
.numberOfShards(1)
9787
.numberOfReplicas(1)
98-
.state(IndexMetaData.State.OPEN));
88+
.state(IndexMetaData.State.OPEN)
89+
.build();
9990
}
100-
final MetaData metaData = metaDataBuilder.build();
91+
return indicesMetaData;
92+
}
10193

94+
private ClusterState buildClusterState(IndexMetaData[] indicesMetaData) {
95+
final MetaData.Builder metaDataBuilder = MetaData.builder();
96+
for (IndexMetaData indexMetaData : indicesMetaData) {
97+
metaDataBuilder.put(indexMetaData, false);
98+
}
99+
final MetaData metaData = metaDataBuilder.build();
102100
final ClusterState clusterState = ClusterState.builder(ClusterName.CLUSTER_NAME_SETTING.getDefault(Settings.EMPTY))
103101
.metaData(metaData)
104102
.build();
105-
final String[] indicesStr = new String[indices.length];
106-
for (int i = 0; i < indices.length; i++) {
107-
indicesStr[i] = indices[i].getName();
103+
return clusterState;
104+
}
105+
106+
private ClusterHealthResponse buildClusterHealthResponse(ClusterState clusterState, IndexMetaData[] indicesMetaData) {
107+
final String[] indicesStr = new String[indicesMetaData.length];
108+
for (int i = 0; i < indicesMetaData.length; i++) {
109+
indicesStr[i] = indicesMetaData[i].getIndex().getName();
108110
}
109-
final ClusterHealthResponse clusterHealth = new ClusterHealthResponse(
111+
final ClusterHealthResponse clusterHealthResponse = new ClusterHealthResponse(
110112
clusterState.getClusterName().value(), indicesStr, clusterState, 0, 0, 0, TimeValue.timeValueMillis(1000L)
111113
);
114+
return clusterHealthResponse;
115+
}
112116

113-
final Table table = action.buildTable(new FakeRestRequest(), indices, clusterHealth, randomIndicesStatsResponse(indices), metaData);
117+
public void testBuildTable() {
118+
final Settings settings = Settings.EMPTY;
119+
UsageService usageService = new UsageService();
120+
final RestController restController = new RestController(Collections.emptySet(), null, null, null, usageService);
121+
final RestIndicesAction action = new RestIndicesAction(settings, restController, new IndexNameExpressionResolver());
122+
123+
final IndexMetaData[] generatedIndicesMetaData = buildRandomIndicesMetaData(randomIntBetween(1, 5));
124+
final ClusterState clusterState = buildClusterState(generatedIndicesMetaData);
125+
final ClusterHealthResponse clusterHealthResponse = buildClusterHealthResponse(clusterState, generatedIndicesMetaData);
126+
127+
final IndexMetaData[] sortedIndicesMetaData = action.getOrderedIndexMetaData(new String[0], clusterState,
128+
IndicesOptions.strictExpand());
129+
final IndexMetaData[] smallerSortedIndicesMetaData = removeRandomElement(sortedIndicesMetaData);
130+
final Table table = action.buildTable(new FakeRestRequest(), sortedIndicesMetaData, clusterHealthResponse,
131+
randomIndicesStatsResponse(smallerSortedIndicesMetaData));
114132

115133
// now, verify the table is correct
116134
int count = 0;
@@ -121,27 +139,27 @@ public void testBuildTable() {
121139
assertThat(headers.get(count++).value, equalTo("uuid"));
122140

123141
List<List<Table.Cell>> rows = table.getRows();
124-
assertThat(rows.size(), equalTo(indices.length));
142+
assertThat(rows.size(), equalTo(smallerSortedIndicesMetaData.length));
125143
// TODO: more to verify (e.g. randomize cluster health, num primaries, num replicas, etc)
126144
for (int i = 0; i < rows.size(); i++) {
127145
count = 0;
128146
final List<Table.Cell> row = rows.get(i);
129147
assertThat(row.get(count++).value, equalTo("red*")); // all are red because cluster state doesn't have routing entries
130148
assertThat(row.get(count++).value, equalTo("open")); // all are OPEN for now
131-
assertThat(row.get(count++).value, equalTo(indices[i].getName()));
132-
assertThat(row.get(count++).value, equalTo(indices[i].getUUID()));
149+
assertThat(row.get(count++).value, equalTo(smallerSortedIndicesMetaData[i].getIndex().getName()));
150+
assertThat(row.get(count++).value, equalTo(smallerSortedIndicesMetaData[i].getIndexUUID()));
133151
}
134152
}
135153

136-
private IndicesStatsResponse randomIndicesStatsResponse(final Index[] indices) {
154+
private IndicesStatsResponse randomIndicesStatsResponse(final IndexMetaData[] indices) {
137155
List<ShardStats> shardStats = new ArrayList<>();
138-
for (final Index index : indices) {
139-
int numShards = randomInt(5);
156+
for (final IndexMetaData index : indices) {
157+
int numShards = randomIntBetween(1, 3);
140158
int primaryIdx = randomIntBetween(-1, numShards - 1); // -1 means there is no primary shard.
141159
for (int i = 0; i < numShards; i++) {
142-
ShardId shardId = new ShardId(index, i);
160+
ShardId shardId = new ShardId(index.getIndex(), i);
143161
boolean primary = (i == primaryIdx);
144-
Path path = createTempDir().resolve("indices").resolve(index.getUUID()).resolve(String.valueOf(i));
162+
Path path = createTempDir().resolve("indices").resolve(index.getIndexUUID()).resolve(String.valueOf(i));
145163
ShardRouting shardRouting = ShardRouting.newUnassigned(shardId, primary,
146164
primary ? RecoverySource.EmptyStoreRecoverySource.INSTANCE : PeerRecoverySource.INSTANCE,
147165
new UnassignedInfo(UnassignedInfo.Reason.INDEX_CREATED, null)
@@ -170,4 +188,14 @@ private IndicesStatsResponse randomIndicesStatsResponse(final Index[] indices) {
170188
shardStats.toArray(new ShardStats[shardStats.size()]), shardStats.size(), shardStats.size(), 0, emptyList()
171189
);
172190
}
191+
192+
private IndexMetaData[] removeRandomElement(IndexMetaData[] array) {
193+
assert array != null;
194+
assert array.length > 0;
195+
final List<IndexMetaData> collectionLessAnItem = new ArrayList<>();
196+
collectionLessAnItem.addAll(Arrays.asList(array));
197+
final int toRemoveIndex = randomIntBetween(0, array.length - 1);
198+
collectionLessAnItem.remove(toRemoveIndex);
199+
return collectionLessAnItem.toArray(new IndexMetaData[0]);
200+
}
173201
}

0 commit comments

Comments
 (0)