Skip to content
Open
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
Expand Up @@ -45,6 +45,11 @@ public class IndexPaginationStrategy implements PaginationStrategy<String> {
private final List<String> requestedIndices;

public IndexPaginationStrategy(PageParams pageParams, ClusterState clusterState) {
this(pageParams, clusterState, metadata -> true);
}

public IndexPaginationStrategy(PageParams pageParams, ClusterState clusterState, Predicate<IndexMetadata> authorizationFilter) {
Objects.requireNonNull(authorizationFilter, "authorizationFilter must not be null");

IndexStrategyToken requestedToken = Objects.isNull(pageParams.getRequestedToken()) || pageParams.getRequestedToken().isEmpty()
? null
Expand All @@ -54,7 +59,8 @@ public IndexPaginationStrategy(PageParams pageParams, ClusterState clusterState)
clusterState,
pageParams.getSort(),
Objects.isNull(requestedToken) ? null : requestedToken.lastIndexName,
Objects.isNull(requestedToken) ? null : requestedToken.lastIndexCreationTime
Objects.isNull(requestedToken) ? null : requestedToken.lastIndexCreationTime,
authorizationFilter
);

// Trim sortedIndicesList to get the list of indices metadata to be sent as response
Expand All @@ -72,20 +78,12 @@ private static List<IndexMetadata> getEligibleIndices(
ClusterState clusterState,
String sortOrder,
String lastIndexName,
Long lastIndexCreationTime
Long lastIndexCreationTime,
Predicate<IndexMetadata> authorizationFilter
) {
if (Objects.isNull(lastIndexName) || Objects.isNull(lastIndexCreationTime)) {
return PaginationStrategy.getSortedIndexMetadata(
clusterState,
PageParams.PARAM_ASC_SORT_VALUE.equals(sortOrder) ? ASC_COMPARATOR : DESC_COMPARATOR
);
} else {
return PaginationStrategy.getSortedIndexMetadata(
clusterState,
getMetadataFilter(sortOrder, lastIndexName, lastIndexCreationTime),
PageParams.PARAM_ASC_SORT_VALUE.equals(sortOrder) ? ASC_COMPARATOR : DESC_COMPARATOR
);
}
Comparator<IndexMetadata> comparator = PageParams.PARAM_ASC_SORT_VALUE.equals(sortOrder) ? ASC_COMPARATOR : DESC_COMPARATOR;
Predicate<IndexMetadata> paginationFilter = getMetadataFilter(sortOrder, lastIndexName, lastIndexCreationTime);
return PaginationStrategy.getSortedIndexMetadata(clusterState, paginationFilter.and(authorizationFilter), comparator);
}

private static Predicate<IndexMetadata> getMetadataFilter(String sortOrder, String lastIndexName, Long lastIndexCreationTime) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,10 @@ public void onResponse(final GetSettingsResponse getSettingsResponse) {
@Override
public void onResponse(ClusterStateResponse clusterStateResponse) {
validateRequestLimit(clusterStateResponse, listener);
IndexPaginationStrategy paginationStrategy = getPaginationStrategy(clusterStateResponse);
IndexPaginationStrategy paginationStrategy = getPaginationStrategy(
clusterStateResponse,
getSettingsResponse
);
// For non-paginated queries, indicesToBeQueried would be same as indices retrieved from
// rest request and unresolved, while for paginated queries, it would be a list of indices
// already resolved by ClusterStateRequest and to be displayed in a page.
Expand Down Expand Up @@ -1239,7 +1242,10 @@ public boolean isActionPaginated() {
return false;
}

protected IndexPaginationStrategy getPaginationStrategy(ClusterStateResponse clusterStateResponse) {
protected IndexPaginationStrategy getPaginationStrategy(
ClusterStateResponse clusterStateResponse,
GetSettingsResponse getSettingsResponse
) {
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
package org.opensearch.rest.action.list;

import org.opensearch.action.admin.cluster.state.ClusterStateResponse;
import org.opensearch.action.admin.indices.settings.get.GetSettingsResponse;
import org.opensearch.action.pagination.IndexPaginationStrategy;
import org.opensearch.action.pagination.PageParams;
import org.opensearch.cluster.metadata.IndexMetadata;
import org.opensearch.common.breaker.ResponseLimitSettings;
import org.opensearch.common.collect.Tuple;
import org.opensearch.common.settings.Settings;
Expand All @@ -21,6 +23,8 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;

import static java.util.Arrays.asList;
import static java.util.Collections.unmodifiableList;
Expand Down Expand Up @@ -84,8 +88,13 @@ protected int defaultPageSize() {
}

@Override
protected IndexPaginationStrategy getPaginationStrategy(ClusterStateResponse clusterStateResponse) {
return new IndexPaginationStrategy(pageParams, clusterStateResponse.getState());
protected IndexPaginationStrategy getPaginationStrategy(
ClusterStateResponse clusterStateResponse,
GetSettingsResponse getSettingsResponse
) {
Set<String> authorizedIndices = getSettingsResponse.getIndexToSettings().keySet();
Predicate<IndexMetadata> authorizationFilter = metadata -> authorizedIndices.contains(metadata.getIndex().getName());
return new IndexPaginationStrategy(pageParams, clusterStateResponse.getState(), authorizationFilter);
}

// Public for testing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.opensearch.action.pagination.PageParams.PARAM_ASC_SORT_VALUE;
Expand Down Expand Up @@ -352,6 +354,67 @@ public void testCreatingIndexStrategyPageTokenWithNameOfLastRespondedIndexNull()
}
}

public void testNullAuthorizationFilterThrowsException() {
ClusterState clusterState = getRandomClusterState(List.of(1, 2, 3));
PageParams pageParams = new PageParams(null, PARAM_ASC_SORT_VALUE, 10);
expectThrows(NullPointerException.class, () -> new IndexPaginationStrategy(pageParams, clusterState, null));
}

public void testFilteredPaginationExcludesUnauthorizedIndices() {
// Create a cluster state with regular indices and system-like indices
ClusterState clusterState = getRandomClusterState(List.of(1, 2, 3, 4, 5));
clusterState = addIndexToClusterState(clusterState, ".system-index", 6);
clusterState = addIndexToClusterState(clusterState, ".opendistro_security", 7);

// Simulate an authorization filter that only allows test-index-* indices
Set<String> authorizedIndices = Set.of("test-index-1", "test-index-2", "test-index-3", "test-index-4", "test-index-5");
Predicate<IndexMetadata> authFilter = metadata -> authorizedIndices.contains(metadata.getIndex().getName());

// First page: size=3, ascending
PageParams pageParams = new PageParams(null, PARAM_ASC_SORT_VALUE, 3);
IndexPaginationStrategy strategy = new IndexPaginationStrategy(pageParams, clusterState, authFilter);
assertEquals(3, strategy.getRequestedEntities().size());
assertEquals("test-index-1", strategy.getRequestedEntities().get(0));
assertEquals("test-index-2", strategy.getRequestedEntities().get(1));
assertEquals("test-index-3", strategy.getRequestedEntities().get(2));
assertNotNull(strategy.getResponseToken().getNextToken());
// System indices should not appear
assertFalse(strategy.getRequestedEntities().contains(".system-index"));
assertFalse(strategy.getRequestedEntities().contains(".opendistro_security"));

// Second page: should contain remaining authorized indices
pageParams = new PageParams(strategy.getResponseToken().getNextToken(), PARAM_ASC_SORT_VALUE, 3);
strategy = new IndexPaginationStrategy(pageParams, clusterState, authFilter);
assertEquals(2, strategy.getRequestedEntities().size());
assertEquals("test-index-4", strategy.getRequestedEntities().get(0));
assertEquals("test-index-5", strategy.getRequestedEntities().get(1));
assertNull(strategy.getResponseToken().getNextToken());
assertFalse(strategy.getRequestedEntities().contains(".system-index"));
assertFalse(strategy.getRequestedEntities().contains(".opendistro_security"));
}

public void testFilteredPaginationWithDescOrder() {
ClusterState clusterState = getRandomClusterState(List.of(1, 2, 3));
clusterState = addIndexToClusterState(clusterState, ".security-index", 4);

Set<String> authorizedIndices = Set.of("test-index-1", "test-index-2", "test-index-3");
Predicate<IndexMetadata> authFilter = metadata -> authorizedIndices.contains(metadata.getIndex().getName());

PageParams pageParams = new PageParams(null, PARAM_DESC_SORT_VALUE, 2);
IndexPaginationStrategy strategy = new IndexPaginationStrategy(pageParams, clusterState, authFilter);
assertEquals(2, strategy.getRequestedEntities().size());
assertEquals("test-index-3", strategy.getRequestedEntities().get(0));
assertEquals("test-index-2", strategy.getRequestedEntities().get(1));
assertNotNull(strategy.getResponseToken().getNextToken());
assertFalse(strategy.getRequestedEntities().contains(".security-index"));

pageParams = new PageParams(strategy.getResponseToken().getNextToken(), PARAM_DESC_SORT_VALUE, 2);
strategy = new IndexPaginationStrategy(pageParams, clusterState, authFilter);
assertEquals(1, strategy.getRequestedEntities().size());
assertEquals("test-index-1", strategy.getRequestedEntities().get(0));
assertNull(strategy.getResponseToken().getNextToken());
}

/**
* @param indexNumbers would be used to create indices having names with integer appended after foo, like foo1, foo2.
* @return random clusterState consisting of indices having their creation times set to the integer used to name them.
Expand All @@ -368,9 +431,13 @@ private ClusterState getRandomClusterState(List<Integer> indexNumbers) {
}

private ClusterState addIndexToClusterState(ClusterState clusterState, int indexNumber) {
IndexMetadata indexMetadata = IndexMetadata.builder("test-index-" + indexNumber)
return addIndexToClusterState(clusterState, "test-index-" + indexNumber, indexNumber);
}

private ClusterState addIndexToClusterState(ClusterState clusterState, String indexName, int creationOrder) {
IndexMetadata indexMetadata = IndexMetadata.builder(indexName)
.settings(
settings(Version.CURRENT).put(SETTING_CREATION_DATE, Instant.now().plus(indexNumber, ChronoUnit.SECONDS).toEpochMilli())
settings(Version.CURRENT).put(SETTING_CREATION_DATE, Instant.now().plus(creationOrder, ChronoUnit.SECONDS).toEpochMilli())
)
.numberOfShards(between(1, 10))
.numberOfReplicas(randomInt(20))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -309,4 +309,12 @@ public void testLastIndexRequestTimestampColumns() {
fail("Timestamp string is not a valid ISO-8601 date: " + timestampString);
}
}

public void testGetPaginationStrategyReturnsNullByDefault() {
final ClusterSettings clusterSettings = new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS);
final Settings settings = Settings.builder().build();
final ResponseLimitSettings responseLimitSettings = new ResponseLimitSettings(clusterSettings, settings);
final RestIndicesAction action = new RestIndicesAction(responseLimitSettings);
assertNull(action.getPaginationStrategy(null, null));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/

package org.opensearch.rest.action.list;

import org.opensearch.Version;
import org.opensearch.action.admin.cluster.state.ClusterStateResponse;
import org.opensearch.action.admin.indices.settings.get.GetSettingsResponse;
import org.opensearch.action.pagination.IndexPaginationStrategy;
import org.opensearch.action.pagination.PageParams;
import org.opensearch.cluster.ClusterName;
import org.opensearch.cluster.ClusterState;
import org.opensearch.cluster.metadata.IndexMetadata;
import org.opensearch.cluster.metadata.Metadata;
import org.opensearch.cluster.routing.IndexRoutingTable;
import org.opensearch.cluster.routing.RoutingTable;
import org.opensearch.common.breaker.ResponseLimitSettings;
import org.opensearch.common.settings.ClusterSettings;
import org.opensearch.common.settings.Settings;
import org.opensearch.test.OpenSearchTestCase;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import static org.opensearch.action.pagination.PageParams.PARAM_ASC_SORT_VALUE;
import static org.opensearch.cluster.metadata.IndexMetadata.SETTING_CREATION_DATE;

public class RestIndicesListActionTests extends OpenSearchTestCase {

private RestIndicesListAction createAction() {
final ClusterSettings clusterSettings = new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS);
final ResponseLimitSettings responseLimitSettings = new ResponseLimitSettings(clusterSettings, Settings.EMPTY);
return new RestIndicesListAction(responseLimitSettings);
}

public void testGetPaginationStrategyFiltersByGetSettingsResponse() {
// Build cluster state with 3 regular indices and 2 system indices
ClusterState clusterState = ClusterState.builder(new ClusterName("test"))
.metadata(Metadata.builder().build())
.routingTable(RoutingTable.builder().build())
.build();
clusterState = addIndex(clusterState, "test-index-1", 1);
clusterState = addIndex(clusterState, "test-index-2", 2);
clusterState = addIndex(clusterState, "test-index-3", 3);
clusterState = addIndex(clusterState, ".opendistro_security", 4);
clusterState = addIndex(clusterState, ".system-index", 5);

ClusterStateResponse clusterStateResponse = new ClusterStateResponse(new ClusterName("test"), clusterState, false);

// GetSettingsResponse only contains authorized indices (simulating security plugin filtering)
Map<String, Settings> authorizedSettings = new HashMap<>();
authorizedSettings.put("test-index-1", Settings.EMPTY);
authorizedSettings.put("test-index-2", Settings.EMPTY);
authorizedSettings.put("test-index-3", Settings.EMPTY);
GetSettingsResponse getSettingsResponse = new GetSettingsResponse(authorizedSettings, Collections.emptyMap());

// Set pageParams and call getPaginationStrategy
RestIndicesListAction action = createAction();
action.pageParams = new PageParams(null, PARAM_ASC_SORT_VALUE, 10);

IndexPaginationStrategy strategy = action.getPaginationStrategy(clusterStateResponse, getSettingsResponse);

// Only authorized indices should be returned, system indices should be filtered out
assertNotNull(strategy);
assertEquals(3, strategy.getRequestedEntities().size());
assertTrue(strategy.getRequestedEntities().contains("test-index-1"));
assertTrue(strategy.getRequestedEntities().contains("test-index-2"));
assertTrue(strategy.getRequestedEntities().contains("test-index-3"));
assertFalse(strategy.getRequestedEntities().contains(".opendistro_security"));
assertFalse(strategy.getRequestedEntities().contains(".system-index"));
// All indices fit in one page, so no next token
assertNull(strategy.getResponseToken().getNextToken());
}

public void testGetPaginationStrategyPaginatesFilteredIndicesCorrectly() {
// Build cluster state with 5 regular indices and 2 system indices
ClusterState clusterState = ClusterState.builder(new ClusterName("test"))
.metadata(Metadata.builder().build())
.routingTable(RoutingTable.builder().build())
.build();
clusterState = addIndex(clusterState, "test-index-1", 1);
clusterState = addIndex(clusterState, "test-index-2", 2);
clusterState = addIndex(clusterState, ".opendistro_security", 3);
clusterState = addIndex(clusterState, "test-index-3", 4);
clusterState = addIndex(clusterState, ".system-index", 5);
clusterState = addIndex(clusterState, "test-index-4", 6);
clusterState = addIndex(clusterState, "test-index-5", 7);

ClusterStateResponse clusterStateResponse = new ClusterStateResponse(new ClusterName("test"), clusterState, false);

Map<String, Settings> authorizedSettings = new HashMap<>();
authorizedSettings.put("test-index-1", Settings.EMPTY);
authorizedSettings.put("test-index-2", Settings.EMPTY);
authorizedSettings.put("test-index-3", Settings.EMPTY);
authorizedSettings.put("test-index-4", Settings.EMPTY);
authorizedSettings.put("test-index-5", Settings.EMPTY);
GetSettingsResponse getSettingsResponse = new GetSettingsResponse(authorizedSettings, Collections.emptyMap());

RestIndicesListAction action = createAction();

// First page: size=2
action.pageParams = new PageParams(null, PARAM_ASC_SORT_VALUE, 2);
IndexPaginationStrategy strategy = action.getPaginationStrategy(clusterStateResponse, getSettingsResponse);

assertEquals(2, strategy.getRequestedEntities().size());
assertEquals("test-index-1", strategy.getRequestedEntities().get(0));
assertEquals("test-index-2", strategy.getRequestedEntities().get(1));
assertNotNull(strategy.getResponseToken().getNextToken());

// Second page
action.pageParams = new PageParams(strategy.getResponseToken().getNextToken(), PARAM_ASC_SORT_VALUE, 2);
strategy = action.getPaginationStrategy(clusterStateResponse, getSettingsResponse);

assertEquals(2, strategy.getRequestedEntities().size());
assertEquals("test-index-3", strategy.getRequestedEntities().get(0));
assertEquals("test-index-4", strategy.getRequestedEntities().get(1));
assertNotNull(strategy.getResponseToken().getNextToken());

// Third page (last)
action.pageParams = new PageParams(strategy.getResponseToken().getNextToken(), PARAM_ASC_SORT_VALUE, 2);
strategy = action.getPaginationStrategy(clusterStateResponse, getSettingsResponse);

assertEquals(1, strategy.getRequestedEntities().size());
assertEquals("test-index-5", strategy.getRequestedEntities().get(0));
assertNull(strategy.getResponseToken().getNextToken());
}

private ClusterState addIndex(ClusterState clusterState, String indexName, int creationOrder) {
IndexMetadata indexMetadata = IndexMetadata.builder(indexName)
.settings(
settings(Version.CURRENT).put(SETTING_CREATION_DATE, Instant.now().plus(creationOrder, ChronoUnit.SECONDS).toEpochMilli())
)
.numberOfShards(1)
.numberOfReplicas(0)
.build();
IndexRoutingTable.Builder indexRoutingTableBuilder = new IndexRoutingTable.Builder(indexMetadata.getIndex());
return ClusterState.builder(clusterState)
.metadata(Metadata.builder(clusterState.metadata()).put(indexMetadata, true).build())
.routingTable(RoutingTable.builder(clusterState.routingTable()).add(indexRoutingTableBuilder).build())
.build();
}
}